@taskless-app/swarm 0.6.18 → 0.6.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +201 -16
  2. package/dist/overmind.js +558 -57
  3. package/package.json +2 -2
package/dist/overmind.js CHANGED
@@ -311,8 +311,8 @@ function computeReviewPhase(input) {
311
311
  }
312
312
  if (ciRed) {
313
313
  const jobs = (input.ciFailing ?? []).filter((j) => j && j !== "?");
314
- const label2 = jobs.length ? `${REVIEW_PHASE_TEXT.ci_fixing}: ${jobs.join(", ")}` : REVIEW_PHASE_TEXT.ci_fixing;
315
- return { kind: "ci_fixing", label: label2, ready: false };
314
+ const label3 = jobs.length ? `${REVIEW_PHASE_TEXT.ci_fixing}: ${jobs.join(", ")}` : REVIEW_PHASE_TEXT.ci_fixing;
315
+ return { kind: "ci_fixing", label: label3, ready: false };
316
316
  }
317
317
  if (reviewVerdict === "changes") {
318
318
  return { kind: "changes", label: REVIEW_PHASE_TEXT.changes, ready: false };
@@ -327,8 +327,8 @@ function computeReviewPhase(input) {
327
327
  if (p && p.total > 0 && p.pass >= p.total) {
328
328
  return { kind: "awaiting_reviewer", label: REVIEW_PHASE_TEXT.awaiting_reviewer, ready: false };
329
329
  }
330
- const label = p && p.total > 0 ? `${REVIEW_PHASE_TEXT.checks} \u2014 CI ${p.pass}/${p.total}` : REVIEW_PHASE_TEXT.checks;
331
- return { kind: "checks", label, ready: false };
330
+ const label2 = p && p.total > 0 ? `${REVIEW_PHASE_TEXT.checks} \u2014 CI ${p.pass}/${p.total}` : REVIEW_PHASE_TEXT.checks;
331
+ return { kind: "checks", label: label2, ready: false };
332
332
  }
333
333
  var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
334
334
  var init_reviewPhase = __esm({
@@ -439,6 +439,92 @@ var init_mergeGate = __esm({
439
439
  }
440
440
  });
441
441
 
442
+ // ../swarm-core/src/mergeQueue.ts
443
+ function orderMergeQueue(ready) {
444
+ return [...ready].sort((a, b) => a.prNumber - b.prNumber || a.runId - b.runId);
445
+ }
446
+ function label(e) {
447
+ return e.slug ? `#${e.prNumber} (${e.slug})` : `#${e.prNumber}`;
448
+ }
449
+ function computeMergeQueue(input) {
450
+ const queue = orderMergeQueue(input.ready);
451
+ const size = queue.length;
452
+ const idx = queue.findIndex((e) => e.runId === input.entry.runId && e.prNumber === input.entry.prNumber);
453
+ const position = idx + 1;
454
+ const head = queue[0];
455
+ if (head && idx > 0) {
456
+ return {
457
+ allowed: false,
458
+ code: "queue_wait",
459
+ reason: `\u0440\u043E\u0439 \u0432\u043B\u0438\u0432\u0430\u0435\u0442 PR \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443: \u0441\u0435\u0439\u0447\u0430\u0441 \u043E\u0447\u0435\u0440\u0435\u0434\u044C PR ${label(head)}, \u044D\u0442\u043E\u0442 \u2014 ${position}-\u0439 \u0438\u0437 ${size}. \u041C\u0435\u0440\u0436\u0438\u0442\u044C \u0435\u0433\u043E \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u043B\u044C\u0437\u044F \u2014 \u0434\u043E\u0436\u0434\u0438\u0441\u044C, \u043F\u043E\u043A\u0430 \u043F\u0435\u0440\u0435\u0434 \u043D\u0438\u043C \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u0441\u044F (TASWM-472)`,
460
+ position,
461
+ size
462
+ };
463
+ }
464
+ if (input.behindBy == null) {
465
+ return {
466
+ allowed: false,
467
+ code: "base_unknown",
468
+ reason: "\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0443\u0437\u043D\u0430\u0442\u044C, \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 \u043B\u0438 \u0432\u0435\u0442\u043A\u0430 PR \u0442\u0435\u043A\u0443\u0449\u0438\u0439 main \u2014 \u0430 \u0431\u0435\u0437 \u044D\u0442\u043E\u0433\u043E \u0437\u0435\u043B\u0451\u043D\u044B\u0439 CI \u043D\u0435 \u0433\u043E\u0432\u043E\u0440\u0438\u0442 \u043D\u0438\u0447\u0435\u0433\u043E \u043F\u0440\u043E main \u043F\u043E\u0441\u043B\u0435 \u0432\u043B\u0438\u0432\u0430\u043D\u0438\u044F. \u042D\u0442\u043E \u0442\u0440\u0430\u043D\u0437\u0438\u0435\u043D\u0442\u043D\u043E (gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B): \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u0447\u0435\u0440\u0435\u0437 \u043C\u0438\u043D\u0443\u0442\u0443 (TASWM-472)",
469
+ position,
470
+ size
471
+ };
472
+ }
473
+ if (input.behindBy > 0) {
474
+ return {
475
+ allowed: false,
476
+ code: "base_stale",
477
+ reason: `\u0431\u0430\u0437\u0430 \u0443\u0435\u0445\u0430\u043B\u0430 \u0432\u043F\u0435\u0440\u0451\u0434 \u043D\u0430 ${input.behindBy} \u043A\u043E\u043C\u043C\u0438\u0442(\u043E\u0432) \u0441 \u043C\u043E\u043C\u0435\u043D\u0442\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u2014 \u0437\u0435\u043B\u0451\u043D\u044B\u0439 CI \u043D\u0430 \u0441\u0442\u0430\u0440\u043E\u0439 \u0431\u0430\u0437\u0435 \u043D\u0435 \u0434\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442, \u0447\u0442\u043E main \u043E\u0441\u0442\u0430\u043D\u0435\u0442\u0441\u044F \u0437\u0435\u043B\u0451\u043D\u044B\u043C. \u0412\u043B\u0435\u0439 main \u0432 \u0432\u0435\u0442\u043A\u0443 PR \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u0437\u0435\u043B\u0451\u043D\u044B\u0445 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u043D\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D\u043D\u043E\u0439 \u0431\u0430\u0437\u0435 (TASWM-472)`,
478
+ position,
479
+ size
480
+ };
481
+ }
482
+ if (input.checks === null) {
483
+ return {
484
+ allowed: false,
485
+ code: "checks_unknown",
486
+ reason: "\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 PR \u2014 \u0430 \u043D\u0430 \u0441\u0432\u0435\u0436\u0435\u0439 \u0431\u0430\u0437\u0435 \u0438\u043C\u0435\u043D\u043D\u043E \u043E\u043D\u0438 \u0438 \u043E\u0442\u0432\u0435\u0447\u0430\u044E\u0442, \u043E\u0441\u0442\u0430\u043D\u0435\u0442\u0441\u044F \u043B\u0438 main \u0437\u0435\u043B\u0451\u043D\u044B\u043C. \u042D\u0442\u043E \u0442\u0440\u0430\u043D\u0437\u0438\u0435\u043D\u0442\u043D\u043E (gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B): \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u0447\u0435\u0440\u0435\u0437 \u043C\u0438\u043D\u0443\u0442\u0443 (TASWM-472)",
487
+ position,
488
+ size
489
+ };
490
+ }
491
+ if (input.checks === "fail") {
492
+ return {
493
+ allowed: false,
494
+ code: "checks_red",
495
+ reason: "\u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043D\u0430 \u041E\u0411\u041D\u041E\u0412\u041B\u0401\u041D\u041D\u041E\u0419 \u0431\u0430\u0437\u0435 \u043A\u0440\u0430\u0441\u043D\u044B\u0435 \u2014 \u044D\u0442\u043E\u0442 PR \u043B\u043E\u043C\u0430\u0435\u0442 main \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0442\u0435\u043A\u0443\u0449\u0438\u043C \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435\u043C \u0431\u0430\u0437\u044B. \u041C\u0435\u0440\u0436\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F: \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0430 (TASWM-472)",
496
+ position,
497
+ size
498
+ };
499
+ }
500
+ if (input.checks === "pending") {
501
+ return {
502
+ allowed: false,
503
+ code: "checks_pending",
504
+ reason: "\u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043D\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D\u043D\u043E\u0439 \u0431\u0430\u0437\u0435 \u0435\u0449\u0451 \u0438\u0434\u0443\u0442 \u2014 \u0438\u0445 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \u0438 \u0435\u0441\u0442\u044C \u043E\u0442\u0432\u0435\u0442 \xAB\u043E\u0441\u0442\u0430\u043D\u0435\u0442\u0441\u044F \u043B\u0438 main \u0437\u0435\u043B\u0451\u043D\u044B\u043C\xBB. \u0414\u043E\u0436\u0434\u0438\u0441\u044C (TASWM-472)",
505
+ position,
506
+ size
507
+ };
508
+ }
509
+ return {
510
+ allowed: true,
511
+ code: "queue_head",
512
+ reason: "\u0433\u043E\u043B\u043E\u0432\u0430 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u043C\u0435\u0440\u0436\u0430, \u0431\u0430\u0437\u0430 \u043F\u043E\u0434 PR \u0441\u0432\u0435\u0436\u0430\u044F, \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043D\u0430 \u043D\u0435\u0439 \u043D\u0435 \u043A\u0440\u0430\u0441\u043D\u044B\u0435 \u2014 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043C\u043E\u0436\u043D\u043E",
513
+ position,
514
+ size
515
+ };
516
+ }
517
+ function computeMergeDecision(gate, queue) {
518
+ if (!gate.allowed || gate.code === "unknown_pr" || !queue) return { ...gate, queue: queue ?? null };
519
+ if (!queue.allowed) return { allowed: false, code: queue.code, reason: queue.reason, queue };
520
+ return { ...gate, reason: `${gate.reason}; ${queue.reason}`, queue };
521
+ }
522
+ var init_mergeQueue = __esm({
523
+ "../swarm-core/src/mergeQueue.ts"() {
524
+ "use strict";
525
+ }
526
+ });
527
+
442
528
  // ../swarm-core/src/modelPolicy.ts
443
529
  function findProvider(id) {
444
530
  const key = typeof id === "string" ? id.trim().toLowerCase() : "";
@@ -1545,6 +1631,7 @@ var init_src = __esm({
1545
1631
  init_timeline();
1546
1632
  init_reviewPhase();
1547
1633
  init_mergeGate();
1634
+ init_mergeQueue();
1548
1635
  init_verifyVerdict();
1549
1636
  init_modelPolicy();
1550
1637
  init_status();
@@ -2161,6 +2248,7 @@ __export(config_exports, {
2161
2248
  SWARM_MAX_FD_LOAD_FACTOR: () => SWARM_MAX_FD_LOAD_FACTOR,
2162
2249
  SWARM_MAX_IN_FLIGHT: () => SWARM_MAX_IN_FLIGHT,
2163
2250
  SWARM_MAX_LOAD_FACTOR: () => SWARM_MAX_LOAD_FACTOR,
2251
+ SWARM_MERGE_QUEUE: () => SWARM_MERGE_QUEUE,
2164
2252
  SWARM_MIN_FREE_GB: () => SWARM_MIN_FREE_GB,
2165
2253
  SWARM_OUTBOX: () => SWARM_OUTBOX,
2166
2254
  SWARM_PROD_HEARTBEAT_MS: () => SWARM_PROD_HEARTBEAT_MS,
@@ -2187,6 +2275,7 @@ __export(config_exports, {
2187
2275
  SWARM_VERIFY: () => SWARM_VERIFY,
2188
2276
  SWARM_VERSION_CHECK_MS: () => SWARM_VERSION_CHECK_MS,
2189
2277
  SWARM_WATCHDOG_STALL_MS: () => SWARM_WATCHDOG_STALL_MS,
2278
+ SWARM_WORKTREE_REAP_INTERVAL_MS: () => SWARM_WORKTREE_REAP_INTERVAL_MS,
2190
2279
  TASKLESS_API: () => TASKLESS_API,
2191
2280
  TASKLESS_MCP_PATH: () => TASKLESS_MCP_PATH,
2192
2281
  TASKLESS_TASK_URL_BASE: () => TASKLESS_TASK_URL_BASE,
@@ -2283,7 +2372,7 @@ function reviewCutoffAt() {
2283
2372
  }
2284
2373
  return at;
2285
2374
  }
2286
- 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, 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;
2287
2376
  var init_config = __esm({
2288
2377
  "src/config.ts"() {
2289
2378
  "use strict";
@@ -2332,6 +2421,7 @@ var init_config = __esm({
2332
2421
  SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
2333
2422
  SWARM_VERSION_CHECK_MS = envNum("SWARM_VERSION_CHECK_MS", 5 * 6e4);
2334
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);
2335
2425
  DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
2336
2426
  DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
2337
2427
  DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
@@ -2356,6 +2446,7 @@ var init_config = __esm({
2356
2446
  SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
2357
2447
  SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
2358
2448
  SWARM_AUTO_VERIFY = process.env.SWARM_AUTO_VERIFY !== "0";
2449
+ SWARM_MERGE_QUEUE = process.env.SWARM_MERGE_QUEUE !== "0";
2359
2450
  REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join5(SWARM_HOME, "review-cutoff.json");
2360
2451
  SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
2361
2452
  SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
@@ -2401,12 +2492,12 @@ function isTransientResourceError(e) {
2401
2492
  const code = e?.code;
2402
2493
  return code === "ENFILE" || code === "EMFILE" || code === "EAGAIN";
2403
2494
  }
2404
- function logTransientSpawnError(label, e) {
2495
+ function logTransientSpawnError(label2, e) {
2405
2496
  if (isTransientResourceError(e)) {
2406
2497
  const code = e.code;
2407
- console.warn(`[overmind] ${label}: \u041E\u0421 \u043D\u0435 \u0434\u0430\u0451\u0442 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u044B (${code}) \u2014 \u043F\u0440\u043E\u043F\u0443\u0441\u043A\u0430\u044E \u0441\u0438\u0433\u043D\u0430\u043B \u044D\u0442\u043E\u0433\u043E \u0442\u0438\u043A\u0430, \u043D\u0435 \u043F\u0430\u0434\u0430\u044E`);
2498
+ console.warn(`[overmind] ${label2}: \u041E\u0421 \u043D\u0435 \u0434\u0430\u0451\u0442 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u044B (${code}) \u2014 \u043F\u0440\u043E\u043F\u0443\u0441\u043A\u0430\u044E \u0441\u0438\u0433\u043D\u0430\u043B \u044D\u0442\u043E\u0433\u043E \u0442\u0438\u043A\u0430, \u043D\u0435 \u043F\u0430\u0434\u0430\u044E`);
2408
2499
  } else {
2409
- console.warn(`[overmind] ${label} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F (\u043D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445 \u0437\u0430 \u044D\u0442\u043E\u0442 \u0442\u0438\u043A): ${String(e)}`);
2500
+ console.warn(`[overmind] ${label2} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F (\u043D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445 \u0437\u0430 \u044D\u0442\u043E\u0442 \u0442\u0438\u043A): ${String(e)}`);
2410
2501
  }
2411
2502
  }
2412
2503
  function resourcesOk(load1, cpus, freeGb, maxLoadFactor = SWARM_MAX_LOAD_FACTOR, minFreeGb = SWARM_MIN_FREE_GB, fdLoadRatio = null, maxFdLoadFactor = SWARM_MAX_FD_LOAD_FACTOR, cpuBusy = null, maxCpuBusy = SWARM_MAX_CPU_BUSY) {
@@ -2417,8 +2508,8 @@ function resourcesOk(load1, cpus, freeGb, maxLoadFactor = SWARM_MAX_LOAD_FACTOR,
2417
2508
  }
2418
2509
  function parseVmStatAvailableBytes(vmStatOut) {
2419
2510
  const page = Number(vmStatOut.match(/page size of (\d+) bytes/)?.[1]) || 4096;
2420
- const pages = (label) => {
2421
- const m = vmStatOut.match(new RegExp(`Pages ${label}:\\s+(\\d+)`));
2511
+ const pages = (label2) => {
2512
+ const m = vmStatOut.match(new RegExp(`Pages ${label2}:\\s+(\\d+)`));
2422
2513
  return m ? Number(m[1]) : 0;
2423
2514
  };
2424
2515
  return (pages("free") + pages("inactive") + pages("speculative") + pages("purgeable")) * page;
@@ -2870,8 +2961,8 @@ function describeError(e, maxDepth = 4) {
2870
2961
  const msg = (cur instanceof Error ? cur.message : typeof cur === "string" ? cur : String(cur)).trim();
2871
2962
  const code = cur.code;
2872
2963
  const codeText = code === void 0 || code === null ? "" : String(code);
2873
- const label = codeText && !msg.includes(codeText) ? `${msg} [code=${codeText}]` : msg;
2874
- if (label && !parts.includes(label)) parts.push(label);
2964
+ const label2 = codeText && !msg.includes(codeText) ? `${msg} [code=${codeText}]` : msg;
2965
+ if (label2 && !parts.includes(label2)) parts.push(label2);
2875
2966
  cur = cur.cause;
2876
2967
  }
2877
2968
  return parts.join(" \u2190 ") || String(e);
@@ -3397,12 +3488,18 @@ async function previousHooksPath(worktree) {
3397
3488
  }
3398
3489
  async function removeWorktree(repo, path) {
3399
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) {
3400
3495
  await cleanupWorktreeProcesses(path);
3401
3496
  await detachWorktree(repo, path);
3402
- 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`;
3403
3500
  }
3404
3501
  async function worktreeDirty(path) {
3405
- 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`;
3406
3503
  if (st.exitCode !== 0) return true;
3407
3504
  const status = st.stdout.trim();
3408
3505
  if (status) return true;
@@ -3992,37 +4089,37 @@ async function materializeAttachments(opts) {
3992
4089
  let index = 0;
3993
4090
  for (const ref of opts.refs) {
3994
4091
  index++;
3995
- const label = ref.caption || ref.url;
4092
+ const label2 = ref.caption || ref.url;
3996
4093
  const cls = classifyAttachment(ref);
3997
4094
  if ("skip" in cls) {
3998
- result.skipped.push(`${label} \u2014 ${cls.skip}`);
4095
+ result.skipped.push(`${label2} \u2014 ${cls.skip}`);
3999
4096
  continue;
4000
4097
  }
4001
4098
  if (result.files.length >= maxFiles) {
4002
- result.skipped.push(`${label} \u2014 \u0441\u0432\u0435\u0440\u0445 \u043B\u0438\u043C\u0438\u0442\u0430 \u0432 ${maxFiles} \u0444\u0430\u0439\u043B\u043E\u0432`);
4099
+ result.skipped.push(`${label2} \u2014 \u0441\u0432\u0435\u0440\u0445 \u043B\u0438\u043C\u0438\u0442\u0430 \u0432 ${maxFiles} \u0444\u0430\u0439\u043B\u043E\u0432`);
4003
4100
  continue;
4004
4101
  }
4005
4102
  if (ref.size != null && ref.size > maxFile) {
4006
- result.skipped.push(`${label} \u2014 ${mb(ref.size)} > \u043B\u0438\u043C\u0438\u0442\u0430 ${mb(maxFile)} \u043D\u0430 \u0444\u0430\u0439\u043B`);
4103
+ result.skipped.push(`${label2} \u2014 ${mb(ref.size)} > \u043B\u0438\u043C\u0438\u0442\u0430 ${mb(maxFile)} \u043D\u0430 \u0444\u0430\u0439\u043B`);
4007
4104
  continue;
4008
4105
  }
4009
4106
  let file = null;
4010
4107
  try {
4011
4108
  file = await opts.fetchFile(ref.url);
4012
4109
  } catch (e) {
4013
- result.skipped.push(`${label} \u2014 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u043E\u0441\u044C (${errText(e)})`);
4110
+ result.skipped.push(`${label2} \u2014 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u043E\u0441\u044C (${errText(e)})`);
4014
4111
  continue;
4015
4112
  }
4016
4113
  if (!file) {
4017
- result.skipped.push(`${label} \u2014 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u043E\u0441\u044C (\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u0438\u043B\u0438 \u0444\u0430\u0439\u043B \u0443\u0434\u0430\u043B\u0451\u043D)`);
4114
+ result.skipped.push(`${label2} \u2014 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u043E\u0441\u044C (\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u0438\u043B\u0438 \u0444\u0430\u0439\u043B \u0443\u0434\u0430\u043B\u0451\u043D)`);
4018
4115
  continue;
4019
4116
  }
4020
4117
  if (file.bytes.byteLength > maxFile) {
4021
- result.skipped.push(`${label} \u2014 ${mb(file.bytes.byteLength)} > \u043B\u0438\u043C\u0438\u0442\u0430 ${mb(maxFile)} \u043D\u0430 \u0444\u0430\u0439\u043B`);
4118
+ result.skipped.push(`${label2} \u2014 ${mb(file.bytes.byteLength)} > \u043B\u0438\u043C\u0438\u0442\u0430 ${mb(maxFile)} \u043D\u0430 \u0444\u0430\u0439\u043B`);
4022
4119
  continue;
4023
4120
  }
4024
4121
  if (total + file.bytes.byteLength > maxTotal) {
4025
- result.skipped.push(`${label} \u2014 \u043D\u0435 \u0432\u043B\u0435\u0437\u043B\u043E \u0432 \u043E\u0431\u0449\u0438\u0439 \u043B\u0438\u043C\u0438\u0442 ${mb(maxTotal)}`);
4122
+ result.skipped.push(`${label2} \u2014 \u043D\u0435 \u0432\u043B\u0435\u0437\u043B\u043E \u0432 \u043E\u0431\u0449\u0438\u0439 \u043B\u0438\u043C\u0438\u0442 ${mb(maxTotal)}`);
4026
4123
  continue;
4027
4124
  }
4028
4125
  const name = attachmentFileName(index, ref.caption, attachmentExt(ref, file.contentType));
@@ -4033,7 +4130,7 @@ async function materializeAttachments(opts) {
4033
4130
  }
4034
4131
  await io.writeFile(join20(dir, name), file.bytes);
4035
4132
  } catch (e) {
4036
- result.skipped.push(`${label} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText(e)})`);
4133
+ result.skipped.push(`${label2} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText(e)})`);
4037
4134
  continue;
4038
4135
  }
4039
4136
  total += file.bytes.byteLength;
@@ -4681,7 +4778,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
4681
4778
  return await safeCall("update_task", { id, assigneeId: userId }) != null;
4682
4779
  },
4683
4780
  uploadShot: (file) => uploadShot(safeCall, file, fetchImpl),
4684
- addComment: async (idOrSlug, body, label, attachments) => {
4781
+ addComment: async (idOrSlug, body, label2, attachments) => {
4685
4782
  const id = await resolveId(idOrSlug);
4686
4783
  if (!id) {
4687
4784
  console.error(`[taskless] add_comment \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: \u0437\u0430\u0434\u0430\u0447\u0430 ${idOrSlug} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 (resolveId \u2192 null)`);
@@ -4700,7 +4797,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
4700
4797
  body: parts[i],
4701
4798
  // Метки роя — на КАЖДОЙ части: без них хвост отчёта выглядит в ленте задачи
4702
4799
  // чужой репликой без автора (swarmKind/swarmDroneName рисуются чипами).
4703
- ...label ?? {},
4800
+ ...label2 ?? {},
4704
4801
  // TASWM-294: скрины — к ПЕРВОЙ части, к шапке отчёта. Разносить их по
4705
4802
  // частям треда нельзя: человек открывает задачу и должен увидеть, как
4706
4803
  // это выглядит, в том же комменте, что и «что сделано».
@@ -10418,6 +10515,70 @@ function parkReason(attempts, reason) {
10418
10515
  return `${PRE_START_MARK} ${attempts} \u0440\u0430\u0437\u0430 \u043F\u043E\u0434\u0440\u044F\u0434 \u2014 ${SPAWN_PARK_MARK}, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u043F\u043E \u043A\u0440\u0443\u0433\u0443. \u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ${reason}`;
10419
10516
  }
10420
10517
 
10518
+ // src/worktreeReap.ts
10519
+ init_src();
10520
+ var DIRTY_WORKTREE_MARK = "worktree \u043D\u0435 \u0443\u0431\u0440\u0430\u043D: \u0432 \u043D\u0451\u043C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430";
10521
+ var HOLDS_WORKTREE = /* @__PURE__ */ new Set(["running", "blocked", "review", "paused", "storm_dead", "stalled"]);
10522
+ function isHelperRun(run) {
10523
+ return run.reviews_run_id != null || run.verifies_run_id != null || run.resolves_run_id != null;
10524
+ }
10525
+ function prIsDead(st) {
10526
+ return st.merged || st.state.toUpperCase() === "MERGED" || st.state.toUpperCase() === "CLOSED";
10527
+ }
10528
+ function heldByLive(deps, all, run, path) {
10529
+ if (deps.liveWorktrees?.().has(path)) return true;
10530
+ return all.some((r) => r.id !== run.id && r.worktree_path === path && HOLDS_WORKTREE.has(r.status));
10531
+ }
10532
+ async function reapFinishedWorktrees(deps) {
10533
+ const report = { removed: 0, keptDirty: 0 };
10534
+ const prunedRepos = /* @__PURE__ */ new Set();
10535
+ const all = deps.db.listRuns();
10536
+ for (const run of all) {
10537
+ const path = run.worktree_path;
10538
+ if (!path || !isTerminalStatus(run.status)) continue;
10539
+ if (heldByLive(deps, all, run, path)) continue;
10540
+ if (!deps.worktreeExists(path)) {
10541
+ if (run.repo && deps.pruneWorktrees && !prunedRepos.has(run.repo)) {
10542
+ prunedRepos.add(run.repo);
10543
+ await deps.pruneWorktrees(run.repo).catch(() => {
10544
+ });
10545
+ }
10546
+ continue;
10547
+ }
10548
+ if (!isHelperRun(run)) {
10549
+ if (!run.pr_url) continue;
10550
+ const st = await deps.prStatus(run.pr_url).catch(() => null);
10551
+ if (!st || !prIsDead(st)) continue;
10552
+ }
10553
+ if (await keepDirty(deps, run, path)) {
10554
+ report.keptDirty += 1;
10555
+ continue;
10556
+ }
10557
+ await deps.releaseWorktree(run.repo, path).catch(() => {
10558
+ });
10559
+ deps.db.addEvent(run.id, "step", `worktree \u0443\u0431\u0440\u0430\u043D: ${path} (\u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D, ${reapReason(run)}) \u2014 \u0441\u043B\u0430\u0433 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430`);
10560
+ report.removed += 1;
10561
+ }
10562
+ return report;
10563
+ }
10564
+ function reapReason(run) {
10565
+ return isHelperRun(run) ? "\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u044B\u043D\u0435\u0441\u0435\u043D, \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0441\u043A\u0430\u044F \u043A\u043E\u043F\u0438\u044F \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u0430" : "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0438\u043B\u0438 \u0437\u0430\u043A\u0440\u044B\u0442";
10566
+ }
10567
+ async function keepDirty(deps, run, path) {
10568
+ const dirty = deps.isDirty ? await deps.isDirty(path).catch(() => true) : true;
10569
+ if (!dirty) return false;
10570
+ const already = deps.db.events(run.id).some((e) => (e.text ?? "").includes(DIRTY_WORKTREE_MARK));
10571
+ if (!already) {
10572
+ deps.db.addEvent(
10573
+ run.id,
10574
+ "anomaly",
10575
+ `${DIRTY_WORKTREE_MARK}. \u041F\u0443\u0442\u044C: ${path}. \u0412\u043D\u0443\u0442\u0440\u0438 \u0435\u0441\u0442\u044C \u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u044B\u0435 \u0438\u043B\u0438 \u043D\u0435\u043E\u0442\u0441\u043B\u0435\u0436\u0438\u0432\u0430\u0435\u043C\u044B\u0435 \u0444\u0430\u0439\u043B\u044B \u043B\u0438\u0431\u043E \u043A\u043E\u043C\u043C\u0438\u0442\u044B, \u043D\u0435 \u0434\u043E\u0448\u0435\u0434\u0448\u0438\u0435 \u0434\u043E origin, \u2014 \u0441\u043D\u043E\u0441\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F, \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0441\u0430\u043C \u0438 \u0443\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438 (\`git -C ${path} status --untracked-files=all\`).`
10576
+ );
10577
+ deps.narrate?.(`${run.task_slug}: worktree \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 \u2014 \u0432 \u043D\u0451\u043C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 (${path}).`);
10578
+ }
10579
+ return true;
10580
+ }
10581
+
10421
10582
  // src/overmind.ts
10422
10583
  init_git();
10423
10584
 
@@ -10472,6 +10633,22 @@ async function prHeadBranch(prUrl, run = realRunner) {
10472
10633
  return null;
10473
10634
  }
10474
10635
  }
10636
+ async function prBaseSync(prUrl, run = realRunner) {
10637
+ const m = prUrl.trim().match(/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/\d+/i);
10638
+ if (!isGithubPrUrl(prUrl) || !m) return null;
10639
+ const [, owner, repo] = m;
10640
+ try {
10641
+ const j = JSON.parse(await run(["pr", "view", prUrl, "--json", "baseRefName,headRefOid"]));
10642
+ const base3 = j.baseRefName;
10643
+ const head = j.headRefOid;
10644
+ if (!base3 || !head) return null;
10645
+ const out = await run(["api", `repos/${owner}/${repo}/compare/${base3}...${head}?per_page=1`, "--jq", ".behind_by"]);
10646
+ const behindBy = Number(out.trim());
10647
+ return Number.isFinite(behindBy) && behindBy >= 0 ? { behindBy, base: base3 } : null;
10648
+ } catch {
10649
+ return null;
10650
+ }
10651
+ }
10475
10652
  async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
10476
10653
  if (!branch || !repoDir) return [];
10477
10654
  try {
@@ -10704,17 +10881,17 @@ function normalizeZoneLabel(zone) {
10704
10881
  return (zone ?? "").trim().replace(/^area:/i, "").trim().toLowerCase();
10705
10882
  }
10706
10883
  function renderZoneMemoryBlock(zone, notes) {
10707
- const label = normalizeZoneLabel(zone);
10708
- if (!label) return "";
10884
+ const label2 = normalizeZoneLabel(zone);
10885
+ if (!label2) return "";
10709
10886
  const head = notes.length ? [
10710
- `\u{1F9E0} \u041F\u0410\u041C\u042F\u0422\u042C \u0417\u041E\u041D\u042B \`${label}\` \u2014 \u0447\u0442\u043E \u0440\u043E\u0439 \u0443\u0436\u0435 \u0432\u044B\u044F\u0441\u043D\u0438\u043B \u043F\u0440\u043E \u044D\u0442\u043E\u0442 \u043A\u0443\u0441\u043E\u043A \u043A\u043E\u0434\u0430 (\u043F\u0440\u043E\u0432\u0435\u0440\u0435\u043D\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u043E\u043C, \u043D\u0435 \u043F\u0435\u0440\u0435\u0441\u043A\u0430\u0437 \u043A\u043E\u0434\u0430):`,
10887
+ `\u{1F9E0} \u041F\u0410\u041C\u042F\u0422\u042C \u0417\u041E\u041D\u042B \`${label2}\` \u2014 \u0447\u0442\u043E \u0440\u043E\u0439 \u0443\u0436\u0435 \u0432\u044B\u044F\u0441\u043D\u0438\u043B \u043F\u0440\u043E \u044D\u0442\u043E\u0442 \u043A\u0443\u0441\u043E\u043A \u043A\u043E\u0434\u0430 (\u043F\u0440\u043E\u0432\u0435\u0440\u0435\u043D\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u043E\u043C, \u043D\u0435 \u043F\u0435\u0440\u0435\u0441\u043A\u0430\u0437 \u043A\u043E\u0434\u0430):`,
10711
10888
  ...notes.map((n) => `\u2022 [${n.id}] ${n.claim}`),
10712
10889
  "\u042D\u0442\u043E \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0438, \u0430 \u043D\u0435 \u0438\u0441\u0442\u0438\u043D\u0430 \u0432 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0439 \u0438\u043D\u0441\u0442\u0430\u043D\u0446\u0438\u0438: \u0440\u0430\u0437\u043E\u0448\u043B\u043E\u0441\u044C \u0441 \u043A\u043E\u0434\u043E\u043C \u2014 \u0432\u0435\u0440\u044C \u043A\u043E\u0434\u0443 \u0438 \u0441\u043A\u0430\u0436\u0438 \u043E\u0431 \u044D\u0442\u043E\u043C \u0432 \u0441\u0434\u0430\u0447\u0435.",
10713
10890
  `\u041F\u0440\u0438\u0433\u043E\u0434\u0438\u043B\u0430\u0441\u044C \u0437\u0430\u043C\u0435\u0442\u043A\u0430 \u2014 \u043D\u0430\u0437\u043E\u0432\u0438 \u0435\u0451 \u0432 done: \`zone_notes_used: ["${notes[0].id}"]\` (id \u0438\u0437 \u0441\u043F\u0438\u0441\u043A\u0430 \u0432\u044B\u0448\u0435 \u0438\u043B\u0438 \u0441\u0430\u043C \u0442\u0435\u043A\u0441\u0442 \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F). \u042D\u0442\u043E \u0435\u0434\u0438\u043D\u0441\u0442\u0432\u0435\u043D\u043D\u043E\u0435, \u0447\u0435\u043C \u0437\u0430\u043C\u0435\u0442\u043A\u0430 \u0436\u0438\u0432\u0451\u0442: \u043F\u0430\u043C\u044F\u0442\u044C \u0437\u043E\u043D\u044B \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0430, \u0438 \u0432\u044B\u0442\u0435\u0441\u043D\u044F\u0435\u0442\u0441\u044F \u0442\u0430, \u0432 \u043A\u043E\u0442\u043E\u0440\u043E\u0439 \u0440\u043E\u0439 \u0434\u043E\u043B\u044C\u0448\u0435 \u0432\u0441\u0435\u0445 \u043D\u0435 \u0443\u0431\u0435\u0436\u0434\u0430\u043B\u0441\u044F. \u041D\u0435 \u043F\u0440\u0438\u0433\u043E\u0434\u0438\u043B\u043E\u0441\u044C \u043D\u0438\u0447\u0435\u0433\u043E \u2014 \u043D\u0435 \u043D\u0430\u0437\u044B\u0432\u0430\u0439 \u043D\u0438\u0447\u0435\u0433\u043E, \u044D\u0442\u043E \u043D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0438\u0441\u0445\u043E\u0434.`
10714
- ] : [`\u{1F9E0} \u041F\u0410\u041C\u042F\u0422\u042C \u0417\u041E\u041D\u042B \`${label}\` \u043F\u0443\u0441\u0442\u0430 \u2014 \u0440\u043E\u0439 \u043F\u0440\u043E \u044D\u0442\u043E\u0442 \u043A\u0443\u0441\u043E\u043A \u043A\u043E\u0434\u0430 \u043F\u043E\u043A\u0430 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B.`];
10891
+ ] : [`\u{1F9E0} \u041F\u0410\u041C\u042F\u0422\u042C \u0417\u041E\u041D\u042B \`${label2}\` \u043F\u0443\u0441\u0442\u0430 \u2014 \u0440\u043E\u0439 \u043F\u0440\u043E \u044D\u0442\u043E\u0442 \u043A\u0443\u0441\u043E\u043A \u043A\u043E\u0434\u0430 \u043F\u043E\u043A\u0430 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B.`];
10715
10892
  return [
10716
10893
  ...head,
10717
- `\u{1F9E0} \u0417\u0410\u041C\u0415\u0422\u041A\u0410 \u0412 \u041F\u0410\u041C\u042F\u0422\u042C \u0417\u041E\u041D\u042B \u2014 \u0432 \u043A\u043E\u043D\u0446\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438 \u0434\u043E ${ZONE_NOTES_PER_RUN_MAX} \u0448\u0442\u0443\u043A: \`zone_notes: [{ zone: "${label}", claim: "\u2026", why: "\u2026" }]\` (zone \u043C\u043E\u0436\u043D\u043E \u043D\u0435 \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u2014 \u0432\u043E\u0437\u044C\u043C\u0451\u0442\u0441\u044F \u0442\u0432\u043E\u044F). \u0427\u0422\u041E \u0433\u043E\u0434\u0438\u0442\u0441\u044F: \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0441 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439 (\xAB\u043E\u0447\u0435\u0440\u0435\u0434\u044C \u0447\u0438\u0442\u0430\u044E\u0442 \u0438\u0437 \u043E\u0434\u043D\u043E\u0439 \u0442\u043E\u0447\u043A\u0438, \u043F\u043E\u0442\u043E\u043C\u0443 \u0447\u0442\u043E \u2026\xBB), \u0433\u0440\u0430\u0431\u043B\u0438 \u0441 \u0446\u0435\u043D\u043E\u0439 (\xAB\u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u043D\u0430\u043A\u0430\u0442\u044B\u0432\u0430\u044E\u0442\u0441\u044F \u043D\u0435 \u0442\u0435\u043C, \u0447\u0435\u043C \u043A\u0430\u0436\u0435\u0442\u0441\u044F, \u2014 \u0441\u0442\u043E\u0438\u043B\u043E \u0447\u0430\u0441\u0430\xBB), \u043E\u0442\u0431\u0440\u043E\u0448\u0435\u043D\u043D\u044B\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 \u0441 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439. \u0427\u0422\u041E \u041D\u0415 \u0433\u043E\u0434\u0438\u0442\u0441\u044F: \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u043E \u043A\u043E\u0434\u0430 (\xAB\u0445\u0435\u043D\u0434\u043B\u0435\u0440 \u043B\u0435\u0436\u0438\u0442 \u0432 handlers.ts\xBB, \xAB\u044D\u0442\u0430 \u0444\u0443\u043D\u043A\u0446\u0438\u044F \u0434\u0435\u043B\u0430\u0435\u0442 \u0442\u043E-\u0442\u043E\xBB) \u2014 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0434\u0440\u043E\u043D \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0435\u0442 \u044D\u0442\u043E \u0441\u0430\u043C \u0438 \u0431\u044B\u0441\u0442\u0440\u0435\u0435, \u0447\u0435\u043C \u0442\u0432\u043E\u044E \u0437\u0430\u043C\u0435\u0442\u043A\u0443. \u0420\u0435\u0448\u0430\u0435\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440; \u043D\u0438\u0447\u0435\u0433\u043E \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435 \u0431\u044B\u043B\u043E \u2014 \u043D\u0435 \u0432\u044B\u0434\u0443\u043C\u044B\u0432\u0430\u0439, \u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u043F\u0438\u0441\u043E\u043A \u044D\u0442\u043E \u043D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0438\u0441\u0445\u043E\u0434 \u043F\u0440\u043E\u0433\u043E\u043D\u0430.`
10894
+ `\u{1F9E0} \u0417\u0410\u041C\u0415\u0422\u041A\u0410 \u0412 \u041F\u0410\u041C\u042F\u0422\u042C \u0417\u041E\u041D\u042B \u2014 \u0432 \u043A\u043E\u043D\u0446\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438 \u0434\u043E ${ZONE_NOTES_PER_RUN_MAX} \u0448\u0442\u0443\u043A: \`zone_notes: [{ zone: "${label2}", claim: "\u2026", why: "\u2026" }]\` (zone \u043C\u043E\u0436\u043D\u043E \u043D\u0435 \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u2014 \u0432\u043E\u0437\u044C\u043C\u0451\u0442\u0441\u044F \u0442\u0432\u043E\u044F). \u0427\u0422\u041E \u0433\u043E\u0434\u0438\u0442\u0441\u044F: \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0441 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439 (\xAB\u043E\u0447\u0435\u0440\u0435\u0434\u044C \u0447\u0438\u0442\u0430\u044E\u0442 \u0438\u0437 \u043E\u0434\u043D\u043E\u0439 \u0442\u043E\u0447\u043A\u0438, \u043F\u043E\u0442\u043E\u043C\u0443 \u0447\u0442\u043E \u2026\xBB), \u0433\u0440\u0430\u0431\u043B\u0438 \u0441 \u0446\u0435\u043D\u043E\u0439 (\xAB\u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u043D\u0430\u043A\u0430\u0442\u044B\u0432\u0430\u044E\u0442\u0441\u044F \u043D\u0435 \u0442\u0435\u043C, \u0447\u0435\u043C \u043A\u0430\u0436\u0435\u0442\u0441\u044F, \u2014 \u0441\u0442\u043E\u0438\u043B\u043E \u0447\u0430\u0441\u0430\xBB), \u043E\u0442\u0431\u0440\u043E\u0448\u0435\u043D\u043D\u044B\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 \u0441 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439. \u0427\u0422\u041E \u041D\u0415 \u0433\u043E\u0434\u0438\u0442\u0441\u044F: \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u043E \u043A\u043E\u0434\u0430 (\xAB\u0445\u0435\u043D\u0434\u043B\u0435\u0440 \u043B\u0435\u0436\u0438\u0442 \u0432 handlers.ts\xBB, \xAB\u044D\u0442\u0430 \u0444\u0443\u043D\u043A\u0446\u0438\u044F \u0434\u0435\u043B\u0430\u0435\u0442 \u0442\u043E-\u0442\u043E\xBB) \u2014 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0434\u0440\u043E\u043D \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0435\u0442 \u044D\u0442\u043E \u0441\u0430\u043C \u0438 \u0431\u044B\u0441\u0442\u0440\u0435\u0435, \u0447\u0435\u043C \u0442\u0432\u043E\u044E \u0437\u0430\u043C\u0435\u0442\u043A\u0443. \u0420\u0435\u0448\u0430\u0435\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440; \u043D\u0438\u0447\u0435\u0433\u043E \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435 \u0431\u044B\u043B\u043E \u2014 \u043D\u0435 \u0432\u044B\u0434\u0443\u043C\u044B\u0432\u0430\u0439, \u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u043F\u0438\u0441\u043E\u043A \u044D\u0442\u043E \u043D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0438\u0441\u0445\u043E\u0434 \u043F\u0440\u043E\u0433\u043E\u043D\u0430.`
10718
10895
  ].join("\n");
10719
10896
  }
10720
10897
  function collectZoneNoteConfirmations(report) {
@@ -11635,6 +11812,52 @@ import { createHash as createHash3 } from "crypto";
11635
11812
 
11636
11813
  // src/anomalies.ts
11637
11814
  init_src();
11815
+
11816
+ // src/reviewRescue.ts
11817
+ init_src();
11818
+ var REVIEW_RESCUE_AFTER_MS = Math.max(6e4, Number(process.env.SWARM_REVIEW_RESCUE_MS) || 10 * 6e4);
11819
+ var REVIEW_RESCUE_MARK = "\u0430\u0432\u0442\u043E\u043F\u043E\u0434\u044A\u0451\u043C \u0440\u0435\u0432\u044C\u044E";
11820
+ var AUTHOR_GONE_STATUSES = /* @__PURE__ */ new Set(["paused", "failed", "stalled"]);
11821
+ function isExecutorRun(run) {
11822
+ if (run.reviews_run_id != null || run.verifies_run_id != null || run.resolves_run_id != null) return false;
11823
+ const kind = run.kind ?? "drone";
11824
+ return kind === "drone" || kind === "hero";
11825
+ }
11826
+ function prOpenedAtMs(events) {
11827
+ const opened = events.filter((e) => e.kind === "pr_opened");
11828
+ if (opened.length === 0) return null;
11829
+ const ms = sqliteMs(opened[opened.length - 1].at);
11830
+ return Number.isFinite(ms) ? ms : null;
11831
+ }
11832
+ function reviewlessPrCandidates(db, now, afterMs = REVIEW_RESCUE_AFTER_MS) {
11833
+ const out = [];
11834
+ for (const run of db.listRuns()) {
11835
+ if (!run.pr_url) continue;
11836
+ if (run.status !== "review" && !AUTHOR_GONE_STATUSES.has(run.status)) continue;
11837
+ if (!isExecutorRun(run)) continue;
11838
+ if (run.review_verdict != null) continue;
11839
+ if (run.status === "review") {
11840
+ if (db.reviewerCountFor(run.id) > 0) continue;
11841
+ if (db.activeReviewersFor(run.id).length > 0) continue;
11842
+ }
11843
+ const events = db.events(run.id);
11844
+ const openedAt = prOpenedAtMs(events) ?? sqliteMs(run.updated_at);
11845
+ if (!Number.isFinite(openedAt) || now - openedAt < afterMs) continue;
11846
+ out.push(run);
11847
+ }
11848
+ return out;
11849
+ }
11850
+ function lastReviewBlockReason(db, runId) {
11851
+ const hit = db.events(runId).findLast((e) => (e.text ?? "").includes(`${REVIEW_RESCUE_MARK}:`));
11852
+ if (!hit?.text) return null;
11853
+ const idx = hit.text.indexOf(`${REVIEW_RESCUE_MARK}:`);
11854
+ return hit.text.slice(idx + REVIEW_RESCUE_MARK.length + 1).trim() || null;
11855
+ }
11856
+ function shouldNoteReason(db, runId, reason) {
11857
+ return lastReviewBlockReason(db, runId) !== reason;
11858
+ }
11859
+
11860
+ // src/anomalies.ts
11638
11861
  var LEVEL_RANK = { note: 0, feed: 1, decision: 2 };
11639
11862
  function plural2(n, forms) {
11640
11863
  const mod100 = n % 100;
@@ -12002,15 +12225,18 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
12002
12225
  key: `pr_no_reviewer:${run.id}`,
12003
12226
  minDurationMs: thresholds.prNoReviewerMs,
12004
12227
  group: PR_GROUP,
12005
- build: () => ({
12006
- runId: run.id,
12007
- // В ленту: ревьюер не поднялся из-за гейта (cap/maxDrones/cutoff) — как только
12008
- // гейт откроется, рой поднимет его сам, решения человека тут не требуется.
12009
- level: "feed",
12010
- title: runTitle(run, "PR \u0431\u0435\u0437 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430"),
12011
- 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}.`,
12012
- ...hrefOf(run.task_url)
12013
- })
12228
+ build: () => {
12229
+ const why = lastReviewBlockReason(db, run.id);
12230
+ return {
12231
+ runId: run.id,
12232
+ // В ленту: рой действует сам (автоподъём), решения человека тут не требуется —
12233
+ // если и подъём не вышел, причина в теле и есть то, с чем человек идёт разбираться.
12234
+ level: "feed",
12235
+ title: runTitle(run, "PR \u0431\u0435\u0437 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430"),
12236
+ body: `${droneOf(run)}: PR \u043E\u0442\u043A\u0440\u044B\u0442 \u2265${Math.round(thresholds.prNoReviewerMs / 6e4)} \u043C\u0438\u043D, \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0442\u0430\u043A \u0438 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F \u2014 ${why ?? "\u0440\u043E\u0439 \u0435\u0449\u0451 \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043B \u043F\u0440\u0438\u0447\u0438\u043D\u0443 (\u0430\u0432\u0442\u043E\u043F\u043E\u0434\u044A\u0451\u043C \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0434\u043E\u0448\u0451\u043B \u0434\u043E \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430)"} \u2014 ${run.pr_url}.`,
12237
+ ...hrefOf(run.task_url)
12238
+ };
12239
+ }
12014
12240
  });
12015
12241
  } else if (run.review_verdict === "changes" && db.activeReviewersFor(run.id).length === 0) {
12016
12242
  candidates.push({
@@ -12508,6 +12734,11 @@ function makeDroneCapReader(opts) {
12508
12734
 
12509
12735
  // src/mergeGate.ts
12510
12736
  init_src();
12737
+
12738
+ // src/mergeQueue.ts
12739
+ init_src();
12740
+
12741
+ // src/prNumber.ts
12511
12742
  function prNumberOf(ref) {
12512
12743
  const s = (ref || "").trim();
12513
12744
  if (!s) return null;
@@ -12516,6 +12747,79 @@ function prNumberOf(ref) {
12516
12747
  const bare = s.match(/^#?(\d+)$/);
12517
12748
  return bare ? Number(bare[1]) : null;
12518
12749
  }
12750
+
12751
+ // src/mergeQueue.ts
12752
+ var TERMINAL2 = /* @__PURE__ */ new Set(["done", "failed", "stalled", "stopped", "superseded", "returned"]);
12753
+ function isMergeCandidate(r) {
12754
+ return !!r.pr_url && r.reviews_run_id == null && r.verifies_run_id == null && r.resolves_run_id == null && !TERMINAL2.has(r.status) && prNumberOf(r.pr_url) != null;
12755
+ }
12756
+ function mergeQueueCandidates(runs, repo) {
12757
+ const byPr = /* @__PURE__ */ new Map();
12758
+ for (const r of runs) {
12759
+ if (r.repo !== repo || !isMergeCandidate(r)) continue;
12760
+ const num = prNumberOf(r.pr_url);
12761
+ const seen = byPr.get(num);
12762
+ if (!seen || r.id > seen.id) byPr.set(num, r);
12763
+ }
12764
+ return [...byPr.values()];
12765
+ }
12766
+ var BASE_TTL_MS = 2e4;
12767
+ function makeMergeQueueResolver(deps) {
12768
+ const memo = /* @__PURE__ */ new Map();
12769
+ const evaluate = async (e, prUrl, ready) => {
12770
+ const now = Date.now();
12771
+ let cached = memo.get(prUrl);
12772
+ const ttl = deps.ttlMs ?? BASE_TTL_MS;
12773
+ if (!cached || now - cached.at >= ttl) {
12774
+ const base3 = await deps.baseSync(prUrl).catch(() => null);
12775
+ let checks = "none";
12776
+ if (base3?.behindBy === 0 && deps.checks) {
12777
+ const ci = await deps.checks(prUrl).catch(() => null);
12778
+ checks = ci ? ci.bucket : null;
12779
+ }
12780
+ cached = { at: now, base: base3, checks };
12781
+ memo.set(prUrl, cached);
12782
+ }
12783
+ return computeMergeQueue({ entry: e, ready, behindBy: cached.base ? cached.base.behindBy : null, checks: cached.checks });
12784
+ };
12785
+ return async (run) => {
12786
+ if (!deps.baseSync || !run.pr_url) return null;
12787
+ const num = prNumberOf(run.pr_url);
12788
+ if (num == null) return null;
12789
+ const candidates = mergeQueueCandidates(deps.listRuns(), run.repo);
12790
+ const mine = candidates.find((c) => prNumberOf(c.pr_url) === num);
12791
+ if (!mine) return null;
12792
+ const ready = [];
12793
+ const urlOf = /* @__PURE__ */ new Map();
12794
+ for (const c of candidates) {
12795
+ if (!await deps.gateAllows(c).catch(() => false)) continue;
12796
+ ready.push(entryOf(c));
12797
+ urlOf.set(c.id, c.pr_url);
12798
+ }
12799
+ const entry = entryOf(mine);
12800
+ if (!ready.some((e) => e.runId === entry.runId)) return null;
12801
+ let queue = orderMergeQueue(ready);
12802
+ while (queue.length > 1 && queue[0].runId !== entry.runId) {
12803
+ const head = queue[0];
12804
+ const headUrl = urlOf.get(head.runId);
12805
+ const decision = await evaluate(head, headUrl, queue);
12806
+ if (decision.code !== "checks_red") break;
12807
+ queue = queue.slice(1);
12808
+ }
12809
+ if (queue[0].runId !== entry.runId) {
12810
+ return computeMergeQueue({ entry, ready: queue, behindBy: null, checks: "none" });
12811
+ }
12812
+ return evaluate(entry, mine.pr_url, queue);
12813
+ };
12814
+ }
12815
+ function entryOf(r) {
12816
+ return { runId: r.id, prNumber: prNumberOf(r.pr_url), slug: r.task_slug };
12817
+ }
12818
+ function needsBaseRefresh(d) {
12819
+ return d?.code === "base_stale";
12820
+ }
12821
+
12822
+ // src/mergeGate.ts
12519
12823
  function findRunByPr(runs, ref) {
12520
12824
  const num = prNumberOf(ref);
12521
12825
  if (num == null) return null;
@@ -12527,17 +12831,34 @@ function findRunByPr(runs, ref) {
12527
12831
  return best;
12528
12832
  }
12529
12833
  function makeMergeGateResolver(deps) {
12530
- return async (ref) => {
12531
- const run = findRunByPr(deps.listRuns(), ref);
12532
- if (!run) return { ...computeMergeGate({ hasRun: false, autoReview: false, reviewVerdict: null }), run: null };
12834
+ return makeMergeGateParts(deps).gate;
12835
+ }
12836
+ function makeMergeGateParts(deps) {
12837
+ const gateFor = async (run) => {
12533
12838
  const autoReview = deps.autoReview ? await deps.autoReview(run.repo).catch(() => deps.fallbackAutoReview) : deps.fallbackAutoReview;
12534
- const decision = computeMergeGate({
12839
+ return computeMergeGate({
12535
12840
  hasRun: true,
12536
12841
  autoReview,
12537
12842
  reviewVerdict: run.review_verdict,
12538
12843
  autoVerify: deps.autoVerify === true,
12539
12844
  verifyVerdict: run.verify_verdict ?? null
12540
12845
  });
12846
+ };
12847
+ const queueFor = makeMergeQueueResolver({
12848
+ listRuns: deps.listRuns,
12849
+ gateAllows: async (r) => (await gateFor(r)).allowed,
12850
+ ...deps.baseSync ? { baseSync: deps.baseSync } : {},
12851
+ ...deps.checks ? { checks: deps.checks } : {},
12852
+ ...deps.ttlMs != null ? { ttlMs: deps.ttlMs } : {}
12853
+ });
12854
+ const gate = async (ref) => {
12855
+ const run = findRunByPr(deps.listRuns(), ref);
12856
+ if (!run) {
12857
+ return { ...computeMergeDecision(computeMergeGate({ hasRun: false, autoReview: false, reviewVerdict: null }), null), run: null };
12858
+ }
12859
+ const accept = await gateFor(run);
12860
+ const queue = accept.allowed && accept.code !== "unknown_pr" ? await queueFor(run) : null;
12861
+ const decision = computeMergeDecision(accept, queue);
12541
12862
  return {
12542
12863
  ...decision,
12543
12864
  run: {
@@ -12550,6 +12871,7 @@ function makeMergeGateResolver(deps) {
12550
12871
  }
12551
12872
  };
12552
12873
  };
12874
+ return { gate, queue: queueFor };
12553
12875
  }
12554
12876
 
12555
12877
  // src/envGate.ts
@@ -12990,7 +13312,7 @@ async function changedPaths(worktree, opts = {}) {
12990
13312
  // package.json
12991
13313
  var package_default = {
12992
13314
  name: "@taskless-app/swarm",
12993
- version: "0.6.18",
13315
+ version: "0.6.20",
12994
13316
  type: "module",
12995
13317
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
12996
13318
  keywords: [
@@ -13040,7 +13362,7 @@ var package_default = {
13040
13362
  execa: "^10.0.0",
13041
13363
  zod: "3.25.76"
13042
13364
  },
13043
- gitHead: "9531f684d8a7c6c4c3595f0c57e4aebce0038ac1"
13365
+ gitHead: "2d05a2e2b7c197ab916f7ff756e3a829b138df16"
13044
13366
  };
13045
13367
 
13046
13368
  // src/version.ts
@@ -14743,8 +15065,8 @@ async function executeHostCommands(opts) {
14743
15065
  }
14744
15066
  async function executeOne(cmd, opts) {
14745
15067
  const { effects, ack, statePath } = opts;
14746
- const label = hostCommandLabel(cmd);
14747
- effects.narrate(`\u{1F4DF} \u041A\u043E\u043C\u0430\u043D\u0434\u0430 \u0438\u0437 Taskless: ${label}`);
15068
+ const label2 = hostCommandLabel(cmd);
15069
+ effects.narrate(`\u{1F4DF} \u041A\u043E\u043C\u0430\u043D\u0434\u0430 \u0438\u0437 Taskless: ${label2}`);
14748
15070
  try {
14749
15071
  switch (cmd.kind) {
14750
15072
  case "pause":
@@ -15627,6 +15949,14 @@ function lastConflictAttemptMs(db, runId) {
15627
15949
  if (!evs.length) return null;
15628
15950
  return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
15629
15951
  }
15952
+ async function needsBaseRefreshNow(deps, run, stormActive, now) {
15953
+ if (!deps.mergeQueue || !deps.resolveConflict || stormActive) return false;
15954
+ if (deps.db.activeResolverFor(run.id)) return false;
15955
+ if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) return false;
15956
+ const last = lastConflictAttemptMs(deps.db, run.id);
15957
+ if (last != null && now - last < CONFLICT_RESOLVE_COOLDOWN_MS) return false;
15958
+ return needsBaseRefresh(await deps.mergeQueue(run).catch(() => null));
15959
+ }
15630
15960
  function lastCiCheckMs(db, runId) {
15631
15961
  const evs = db.events(runId).filter((e) => e.kind === "ci_wake" || e.kind === "ci_checked");
15632
15962
  if (!evs.length) return null;
@@ -15882,6 +16212,92 @@ async function closeMergedTaskRuns(deps) {
15882
16212
  }
15883
16213
  }
15884
16214
  }
16215
+ function relinkUnboundReviewers(deps) {
16216
+ const all = deps.db.listRuns();
16217
+ const unbound = all.filter((r) => r.kind === "reviewer" && r.reviews_run_id == null && r.pr_url && REVIEWER_ALIVE_STATUSES.has(r.status));
16218
+ if (unbound.length === 0) return;
16219
+ for (const rv of unbound) {
16220
+ const candidates = all.filter(
16221
+ (r) => r.id !== rv.id && r.pr_url === rv.pr_url && (r.kind ?? "drone") !== "reviewer" && (r.kind ?? "drone") !== "verifier" && r.reviews_run_id == null && r.verifies_run_id == null && r.resolves_run_id == null
16222
+ );
16223
+ if (candidates.length !== 1) {
16224
+ const why = candidates.length === 0 ? `\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435 ${rv.pr_url} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u043D\u0435\u043A\u0443\u0434\u0430` : `\u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435 ${rv.pr_url} \u043D\u0430\u0439\u0434\u0435\u043D\u043E ${candidates.length} \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u2014 \u043A\u0430\u043A\u043E\u0439 \u0438\u0437 \u043D\u0438\u0445 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439, \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F`;
16225
+ if (!deps.db.events(rv.id).some((e) => (e.text ?? "").includes(why))) deps.db.addEvent(rv.id, "step", why);
16226
+ continue;
16227
+ }
16228
+ const parent = candidates[0];
16229
+ deps.db.updateRun(rv.id, { reviews_run_id: parent.id, review_cycle: parent.review_cycle });
16230
+ deps.db.addEvent(rv.id, "step", `\u0441\u0432\u044F\u0437\u044C \u0441 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u043C \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435 PR: #${parent.id} (${parent.task_slug}) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0435\u0441\u0442\u044C \u043A\u0443\u0434\u0430 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C (TASWM-375)`);
16231
+ deps.db.addEvent(parent.id, "step", `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${rv.name} (#${rv.id}) \u043F\u043E\u0434\u043D\u044F\u0442 \u0431\u0435\u0437 \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438 \u2014 \u0441\u0432\u044F\u0437\u0430\u043B \u0435\u0433\u043E \u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u043C \u043F\u043E PR-\u0441\u0441\u044B\u043B\u043A\u0435, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043B\u044F\u0436\u0435\u0442 \u0441\u044E\u0434\u0430`);
16232
+ narrate(deps, `${droneLabel(rv)} \u0440\u0430\u0431\u043E\u0442\u0430\u043B \u0431\u0435\u0437 \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438 \u043A \u043F\u0440\u043E\u0433\u043E\u043D\u0443 \u2014 \u0441\u0432\u044F\u0437\u0430\u043B \u043F\u043E PR-\u0441\u0441\u044B\u043B\u043A\u0435, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0442\u0435\u043F\u0435\u0440\u044C \u043B\u044F\u0436\u0435\u0442 \u0432 ${parent.task_slug}, \u0430 \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442 GitHub.`);
16233
+ }
16234
+ }
16235
+ function deliberateReviewStop(db, run) {
16236
+ const texts = db.events(run.id).map((e) => e.text ?? "");
16237
+ const has = (mark) => texts.some((t) => t.includes(mark));
16238
+ if (has(SELF_REFERENCE_MARK)) {
16239
+ return `PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430 (${DAEMON_CODE_PATHS.join(", ")}) \u2014 \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u043D\u043E (TASWM-270): \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0449\u0438\u0439 \u0434\u0435\u043C\u043E\u043D \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F \u043D\u0430 \u0441\u0442\u0430\u0440\u043E\u043C \u043A\u043E\u0434\u0435 \u0438 \u0441\u0432\u043E\u044E \u0436\u0435 \u043F\u0440\u0430\u0432\u043A\u0443 \u043D\u0435 \u0430\u0442\u0442\u0435\u0441\u0442\u0443\u0435\u0442. \u041D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
16240
+ }
16241
+ if (has(REVIEW_ESCALATION_MARK)) return `\u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u044B (${REVIEW_SPAWN_LIMIT}) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
16242
+ if (has(REVIEW_CYCLE_ESCALATION_MARK)) return `\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0446\u0438\u043A\u043B\u043E\u0432 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E (${REVIEW_CYCLE_LIMIT}) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
16243
+ if (has(reviewBudgetMark(run.review_cycle ?? 0))) return "\u0440\u0435\u0432\u044C\u044E \u044D\u0442\u043E\u0433\u043E \u043A\u0440\u0443\u0433\u0430 \u0441\u0433\u043E\u0440\u0435\u043B\u043E \u043D\u0430 \u043F\u043E\u0442\u043E\u043B\u043A\u0435 \u0442\u0440\u0430\u0442 \u2014 \u043F\u043E\u0432\u0442\u043E\u0440 \u0443\u043F\u0440\u0451\u0442\u0441\u044F \u0432 \u0442\u043E\u0442 \u0436\u0435, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A";
16244
+ if (has(reviewUnrecoverableMark(run.branch || run.task_slug))) return "\u0432\u0435\u0442\u043A\u0438 PR \u043D\u0435\u0442 \u043D\u0430 origin \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A";
16245
+ return null;
16246
+ }
16247
+ function noteReviewBlocked(deps, run, reason) {
16248
+ if (!shouldNoteReason(deps.db, run.id, reason)) return;
16249
+ deps.db.addEvent(run.id, "step", `${REVIEW_RESCUE_MARK}: ${reason}`);
16250
+ }
16251
+ async function rescueReviewlessPrs(deps, stormActive, now) {
16252
+ for (const run of reviewlessPrCandidates(deps.db, now)) {
16253
+ try {
16254
+ const stop = deliberateReviewStop(deps.db, run);
16255
+ if (stop) {
16256
+ noteReviewBlocked(deps, run, stop);
16257
+ continue;
16258
+ }
16259
+ const authorGone = AUTHOR_GONE_STATUSES.has(run.status) && (run.status !== "paused" || NO_AUTO_WAKE_REASONS.some((r) => run.stop_reason?.startsWith(r)));
16260
+ if (authorGone) {
16261
+ const st = await deps.prStatus(run.pr_url).catch(() => null);
16262
+ const liveness = prLiveness(st);
16263
+ if (liveness !== "open") {
16264
+ noteReviewBlocked(deps, run, liveness === "unknown" ? "gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043F\u0440\u043E PR \u2014 \u043D\u0435 \u0437\u043D\u0430\u044E, \u0436\u0438\u0432 \u043B\u0438 \u043E\u043D; \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C" : `PR ${liveness === "merged" ? "\u0441\u043C\u0435\u0440\u0436\u0435\u043D" : "\u0437\u0430\u043A\u0440\u044B\u0442"} \u2014 \u0440\u0435\u0432\u044C\u044E\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E`);
16265
+ continue;
16266
+ }
16267
+ if (st.isDraft) {
16268
+ noteReviewBlocked(deps, run, "PR \u2014 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A: \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438 \u043D\u0435 \u0441\u043C\u043E\u0442\u0440\u0438\u0442 (\u0441\u043D\u0438\u043C\u0438 draft)");
16269
+ continue;
16270
+ }
16271
+ const wasLabel = STATUS_META[run.status]?.label ?? run.status;
16272
+ deps.db.updateRun(run.id, { status: "review" });
16273
+ deps.db.addEvent(
16274
+ run.id,
16275
+ "step",
16276
+ `${REVIEW_RESCUE_MARK}: \u0430\u0432\u0442\u043E\u0440 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 (${wasLabel}), \u0430 PR \u043E\u0442\u043A\u0440\u044B\u0442 \u0438 \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044E\u0449\u0438\u0439 \u043D\u0443\u0436\u0435\u043D \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u0441\u0443\u0434\u044C\u0431\u044B \u0430\u0432\u0442\u043E\u0440\u0430`
16277
+ );
16278
+ narrate(deps, `PR ${droneLabel(run)} \u0436\u0438\u0432, \u0430 \u0435\u0433\u043E \u0430\u0432\u0442\u043E\u0440 \u2014 \u043D\u0435\u0442 (${wasLabel}): \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0441\u0451 \u0440\u0430\u0432\u043D\u043E \u043D\u0443\u0436\u0435\u043D.`);
16279
+ continue;
16280
+ }
16281
+ if (run.status !== "review") continue;
16282
+ noteReviewBlocked(deps, run, reviewGateReason(deps, run, stormActive));
16283
+ } catch (e) {
16284
+ console.error(`[overmind] \u0430\u0432\u0442\u043E\u043F\u043E\u0434\u044A\u0451\u043C \u0440\u0435\u0432\u044C\u044E \u0434\u043B\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} (${run.task_slug}) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
16285
+ }
16286
+ }
16287
+ }
16288
+ function reviewGateReason(deps, run, stormActive) {
16289
+ if (!deps.spawnReviewer) return "\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 (SWARM_AUTO_REVIEW=0)";
16290
+ if (stormActive) return "\u0448\u0442\u043E\u0440\u043C Anthropic \u2014 \u043D\u043E\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u0440\u043E\u0439 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442, \u0440\u0435\u0432\u044C\u044E \u0436\u0434\u0451\u0442 \u043E\u0442\u0431\u043E\u044F";
16291
+ if (deps.reviewCutoffAt && run.updated_at < deps.reviewCutoffAt) {
16292
+ return `\u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0442\u0430\u0440\u0448\u0435 \u043C\u043E\u043C\u0435\u043D\u0442\u0430 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E (cutoff ${deps.reviewCutoffAt}) \u2014 \u0440\u043E\u0439 \u0435\u0433\u043E \u043D\u0435 \u0431\u0435\u0440\u0451\u0442, \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0440\u0443\u043A\u0430\u043C\u0438`;
16293
+ }
16294
+ if (deps.resourcesOk && !deps.resourcesOk()) return "\u043C\u0430\u0448\u0438\u043D\u0430 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0436\u0434\u0451\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0445 \u0440\u0435\u0441\u0443\u0440\u0441\u043E\u0432";
16295
+ if (deps.reviewCap !== void 0) {
16296
+ const busy = countActiveReviewers(deps.db);
16297
+ if (busy >= deps.reviewCap) return `\u0437\u0430\u043D\u044F\u0442 \u0441\u043B\u043E\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 ${busy} \u043F\u0440\u0438 SWARM_REVIEW_CAP=${deps.reviewCap} \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u043B\u043E\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u0441\u044F`;
16298
+ }
16299
+ return "\u0432\u0441\u0435 \u0433\u0435\u0439\u0442\u044B \u043E\u0442\u043A\u0440\u044B\u0442\u044B, \u0430 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F \u2014 \u0441\u043C\u043E\u0442\u0440\u0438 \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0430 \u043E\u0448\u0438\u0431\u043A\u0443 \u0441\u043F\u0430\u0432\u043D\u0430";
16300
+ }
15885
16301
  async function closeOrphanedReviewers(deps) {
15886
16302
  for (const rv of deps.db.listRuns()) {
15887
16303
  const checkedId = rv.kind === "reviewer" ? rv.reviews_run_id : rv.kind === "verifier" ? rv.verifies_run_id : null;
@@ -16957,9 +17373,42 @@ async function trySpawnQueuedRun(deps, run) {
16957
17373
  return "skipped";
16958
17374
  }
16959
17375
  if (e instanceof AreaBusyError) return deferSpawn(deps, run, e.message);
17376
+ if (e instanceof WorktreeBusyError && await healBusyWorktree(deps, run, e)) {
17377
+ try {
17378
+ await deps.spawn(run);
17379
+ if (deps.spawnDefer) clearDefer(deps.spawnDefer, `run:${run.id}`);
17380
+ narrate(deps, `\u0411\u0435\u0440\u0443 ${run.task_slug} \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0441\u043F\u0430\u0432\u043D\u044E ${droneLabel(run)} (\u043F\u0440\u0438\u0431\u0440\u0430\u043B \u0437\u0430 \u043C\u0451\u0440\u0442\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439).`);
17381
+ return "spawned";
17382
+ } catch (again) {
17383
+ return await failBeforeStart(deps, run, String(again));
17384
+ }
17385
+ }
16960
17386
  return await failBeforeStart(deps, run, String(e));
16961
17387
  }
16962
17388
  }
17389
+ async function healBusyWorktree(deps, run, e) {
17390
+ const path = e.worktreePath;
17391
+ if (!deps.releaseWorktree) return false;
17392
+ if (deps.liveWorktrees?.().has(path)) return false;
17393
+ const holder = deps.db.listRuns().find((r) => r.id !== run.id && r.worktree_path === path && !isTerminalStatus(r.status));
17394
+ if (holder && (holder.pid == null || deps.isAlive(holder.pid))) {
17395
+ return false;
17396
+ }
17397
+ if (deps.isDirty && await deps.isDirty(path).catch(() => true)) {
17398
+ deps.db.addEvent(
17399
+ run.id,
17400
+ "anomaly",
17401
+ `${DIRTY_WORKTREE_MARK}. \u041F\u0443\u0442\u044C: ${path}. \u0416\u0438\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0437\u0430 \u043D\u0438\u043C \u043D\u0435\u0442, \u043D\u043E \u0432\u043D\u0443\u0442\u0440\u0438 \u0435\u0441\u0442\u044C \u043D\u0435\u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u2014 \u043F\u0440\u0438\u0431\u0438\u0440\u0430\u0442\u044C \u043D\u0435 \u0441\u0442\u0430\u043B, \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0441\u0430\u043C: \`git -C ${path} status --untracked-files=all\`.`
17402
+ );
17403
+ narrate(deps, `${run.task_slug}: worktree \u0437\u0430\u043D\u044F\u0442 \u043C\u0451\u0440\u0442\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439, \u043D\u043E \u0432 \u043D\u0451\u043C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u2014 \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`);
17404
+ return false;
17405
+ }
17406
+ await deps.releaseWorktree(run.repo, path).catch(() => {
17407
+ });
17408
+ deps.db.addEvent(run.id, "step", `worktree ${path} \u0434\u0435\u0440\u0436\u0430\u043B\u0430 \u043C\u0451\u0440\u0442\u0432\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u2014 \u043F\u0440\u0438\u0431\u0440\u0430\u043B \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E \u0441\u043F\u0430\u0432\u043D (TASWM-476)`);
17409
+ if (holder) deps.db.addEvent(holder.id, "step", `worktree ${path} \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0451\u043D: \u0436\u0438\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0437\u0430 \u043D\u0438\u043C \u043D\u0435\u0442, \u0441\u043B\u0430\u0433 \u043F\u043E\u043D\u0430\u0434\u043E\u0431\u0438\u043B\u0441\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0443 #${run.id}`);
17410
+ return true;
17411
+ }
16963
17412
  async function failBeforeStart(deps, run, reason) {
16964
17413
  deps.db.updateRun(run.id, { status: "failed", stop_reason: `${PRE_START_MARK}: ${reason}` });
16965
17414
  deps.db.addEvent(run.id, "failed", `${PRE_START_MARK} \u2014 ${reason}`);
@@ -17524,6 +17973,18 @@ async function tickOnce(deps) {
17524
17973
  }
17525
17974
  }
17526
17975
  }
17976
+ if (deps.reapWorktrees) {
17977
+ const rw = deps.reapWorktrees;
17978
+ const sinceLast = rw.lastAt == null ? Infinity : now - rw.lastAt;
17979
+ if (sinceLast >= rw.intervalMs) {
17980
+ rw.lastAt = now;
17981
+ try {
17982
+ await rw.run();
17983
+ } catch (e) {
17984
+ console.error("[overmind] \u0443\u0431\u043E\u0440\u043A\u0430 worktree \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e);
17985
+ }
17986
+ }
17987
+ }
17527
17988
  if (deps.logRotate) {
17528
17989
  const lr = deps.logRotate;
17529
17990
  const sinceLast = lr.lastAt == null ? Infinity : now - lr.lastAt;
@@ -17653,7 +18114,8 @@ async function tickOnce(deps) {
17653
18114
  continue;
17654
18115
  }
17655
18116
  await retireOutdatedReviewers(deps, run);
17656
- if (st.conflicting && deps.resolveConflict && !stormActive) {
18117
+ const baseStale = !st.conflicting && await needsBaseRefreshNow(deps, run, stormActive, now);
18118
+ if ((st.conflicting || baseStale) && deps.resolveConflict && !stormActive) {
17657
18119
  if (deps.resourcesOk && !deps.resourcesOk()) continue;
17658
18120
  if (countActiveDrones(deps.db) >= droneCap) continue;
17659
18121
  if (!await projectQuotaGateAllows(deps, run)) continue;
@@ -17666,7 +18128,11 @@ async function tickOnce(deps) {
17666
18128
  }
17667
18129
  const last = lastConflictAttemptMs(deps.db, run.id);
17668
18130
  if (last != null && now - last < CONFLICT_RESOLVE_COOLDOWN_MS) continue;
17669
- deps.db.addEvent(run.id, "conflict", "PR \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0441 main \u2014 \u0440\u043E\u0439 \u0432\u043B\u0438\u0432\u0430\u0435\u0442 main (merge-main-in)");
18131
+ deps.db.addEvent(
18132
+ run.id,
18133
+ "conflict",
18134
+ st.conflicting ? "PR \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0441 main \u2014 \u0440\u043E\u0439 \u0432\u043B\u0438\u0432\u0430\u0435\u0442 main (merge-main-in)" : "\u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043C\u0435\u0440\u0436\u0430: \u0431\u0430\u0437\u0430 \u043F\u043E\u0434 PR \u0443\u0435\u0445\u0430\u043B\u0430 \u0432\u043F\u0435\u0440\u0451\u0434 \u2014 \u0440\u043E\u0439 \u0432\u043B\u0438\u0432\u0430\u0435\u0442 main \u0438 \u043F\u0435\u0440\u0435\u0433\u043E\u043D\u044F\u0435\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043D\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D\u043D\u043E\u0439 \u0431\u0430\u0437\u0435 (TASWM-472)"
18135
+ );
17670
18136
  try {
17671
18137
  await deps.resolveConflict(run);
17672
18138
  } catch (e) {
@@ -17755,6 +18221,8 @@ async function tickOnce(deps) {
17755
18221
  }
17756
18222
  }
17757
18223
  await closeMergedTaskRuns(deps);
18224
+ await rescueReviewlessPrs(deps, stormActive, now);
18225
+ relinkUnboundReviewers(deps);
17758
18226
  await closeOrphanedReviewers(deps);
17759
18227
  await spawnVerifiersForOpenPrs(deps, stormActive);
17760
18228
  refreshReviewPhases(deps);
@@ -18989,8 +19457,21 @@ async function runDaemon(opts) {
18989
19457
  };
18990
19458
  control.set = setPaused;
18991
19459
  const restart = initialRestartState();
19460
+ const mergeParts = makeMergeGateParts({
19461
+ listRuns: () => db.listRuns(),
19462
+ ...autoReviewProject ? { autoReview: autoReviewProject } : {},
19463
+ fallbackAutoReview: SWARM_AUTO_REVIEW,
19464
+ autoVerify: SWARM_AUTO_VERIFY,
19465
+ // TASWM-344: вторая ось приёмки — вердикт верификатора
19466
+ // TASWM-472: третья ось — ОЧЕРЕДЬ. Вливаем по одному и только на свежей базе; ридер
19467
+ // свежести ходит в gh (`compare/base...head` → behind_by). Ключа нет при
19468
+ // SWARM_MERGE_QUEUE=0 — тогда гейт отвечает ровно как до TASWM-472.
19469
+ ...SWARM_MERGE_QUEUE ? { baseSync: (url) => prBaseSync(url), checks: (url) => prCiStatus(url) } : {}
19470
+ });
18992
19471
  const deps = {
18993
19472
  db,
19473
+ // TASWM-472: реконсайлу от очереди нужен ровно исход `base_stale` (см. needsBaseRefreshNow).
19474
+ ...SWARM_MERGE_QUEUE ? { mergeQueue: mergeParts.queue } : {},
18994
19475
  narrate: narrator.narrate,
18995
19476
  control,
18996
19477
  spawn: async (run) => {
@@ -19072,7 +19553,7 @@ async function runDaemon(opts) {
19072
19553
  // TasklessClient.addComment + gh/git хелперы. Без tl (нет токена) ключи не задаём —
19073
19554
  // postAdoptionReport/postScrapedVerdictReport становятся no-op, как раньше.
19074
19555
  ...tl ? {
19075
- reportComment: (slug, body, label, ws) => tl.forWorkspace(ws).addComment(slug, body, label)
19556
+ reportComment: (slug, body, label2, ws) => tl.forWorkspace(ws).addComment(slug, body, label2)
19076
19557
  } : {},
19077
19558
  prCommitTitles: (prUrl) => prCommitTitles(prUrl),
19078
19559
  prHeadBranch: (prUrl) => prHeadBranch(prUrl),
@@ -19135,6 +19616,32 @@ async function runDaemon(opts) {
19135
19616
  intervalMs: SWARM_SWEEP_INTERVAL_MS,
19136
19617
  lastAt: null
19137
19618
  },
19619
+ // Жнец worktree (TASWM-476): убирает копии терминальных прогонов, чьи PR смержены/закрыты.
19620
+ // Живость каталога определяем ПО ПРОЦЕССАМ (listDroneProcesses — тот же снимок, по которому
19621
+ // идёт реконсайл рантайма), а не только по статусу в зеркале: статус мог не успеть
19622
+ // обновиться, а сносить каталог из-под работающего дрона нельзя ни при каком статусе.
19623
+ reapWorktrees: {
19624
+ run: async () => {
19625
+ const report = await reapFinishedWorktrees({
19626
+ db,
19627
+ prStatus: (url) => prStatus(url),
19628
+ isDirty: (p) => worktreeDirty(p),
19629
+ releaseWorktree: (repo, p) => releaseWorktree(repo || opts.repo, p),
19630
+ pruneWorktrees: (repo) => pruneWorktrees(repo || opts.repo),
19631
+ worktreeExists: existsSync17,
19632
+ liveWorktrees: () => new Set(listDroneProcesses().map((p) => p.worktree)),
19633
+ narrate: narrator.narrate
19634
+ });
19635
+ if (report.removed || report.keptDirty) {
19636
+ console.log(`[overmind] \u0443\u0431\u043E\u0440\u043A\u0430 worktree: \u0441\u043D\u0435\u0441\u0435\u043D\u043E ${report.removed}, \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043E \u0441 \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u043E\u0439 ${report.keptDirty}`);
19637
+ }
19638
+ },
19639
+ intervalMs: SWARM_WORKTREE_REAP_INTERVAL_MS,
19640
+ lastAt: null
19641
+ },
19642
+ // Самолечение залипшего слага (TASWM-476): те же два кирпича, что у жнеца выше.
19643
+ releaseWorktree: (repo, p) => releaseWorktree(repo || opts.repo, p),
19644
+ liveWorktrees: () => new Set(listDroneProcesses().map((p) => p.worktree)),
19138
19645
  isDirty: (p) => worktreeDirty(p),
19139
19646
  // git-активность как liveness (TASWM-67): последний коммит worktree — реальный git-вызов.
19140
19647
  lastCommitMs: (p) => worktreeLastCommitMs(p),
@@ -19548,13 +20055,7 @@ async function runDaemon(opts) {
19548
20055
  // TASWM-396: тот же ридер решает и подъём ревьюера, не только мерж — иначе проектная
19549
20056
  // настройка «ревью не нужно» ничего не выключала, а машина продолжала платить.
19550
20057
  ...autoReviewProject ? { autoReviewForRepo: autoReviewProject } : {},
19551
- mergeGate: makeMergeGateResolver({
19552
- listRuns: () => db.listRuns(),
19553
- ...autoReviewProject ? { autoReview: autoReviewProject } : {},
19554
- fallbackAutoReview: SWARM_AUTO_REVIEW,
19555
- autoVerify: SWARM_AUTO_VERIFY
19556
- // TASWM-344: вторая ось приёмки — вердикт верификатора
19557
- }),
20058
+ mergeGate: mergeParts.gate,
19558
20059
  // Оператор-пауза + «Оживить» (TASWM-214 Фаза 2): тот же shared-флаг, что читает тик;
19559
20060
  // narrate — операторские действия видны в ленте роя; revive — сброс залипшего шторма.
19560
20061
  control,