@taskless-app/swarm 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +372 -37
  2. package/dist/overmind.js +1030 -155
  3. package/package.json +7 -5
package/dist/overmind.js CHANGED
@@ -92,6 +92,17 @@ function buildSegments(created, events, laneEnd) {
92
92
  push(e.at);
93
93
  phase = "running";
94
94
  break;
95
+ // TASWM-166: раньше эти два kind'а не двигали фазу — хвост дорожки
96
+ // дорисовывался ТЕКУЩЕЙ (до остановки) фазой, напр. "running" сплошным
97
+ // зелёным до самого конца — визуально неотличимо от реально идущего прогона.
98
+ case "stopped":
99
+ push(e.at);
100
+ phase = "stopped";
101
+ break;
102
+ case "superseded":
103
+ push(e.at);
104
+ phase = "superseded";
105
+ break;
95
106
  }
96
107
  }
97
108
  push(laneEnd);
@@ -112,7 +123,7 @@ function computeBlocker(status, stopReason, lastStep, pr, ci, deps, openQ) {
112
123
  case "blocked":
113
124
  return { icon: "\u2753", text: openQ ? `\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430: ${openQ}` : "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430", tone: "blocked" };
114
125
  case "paused":
115
- return { icon: "\u{1F4A4}", text: "\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430", tone: "paused" };
126
+ return { icon: "\u{1F4A4}", text: stopReason || "\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430", tone: "paused" };
116
127
  case "storm_dead":
117
128
  return { icon: "\u{1F4A4}", text: "\u0432 \u043A\u043E\u043A\u043E\u043D\u0435 \u2014 \u0448\u0442\u043E\u0440\u043C Anthropic, \u0436\u0434\u0451\u0442 \u043E\u0442\u0431\u043E\u044F", tone: "paused" };
118
129
  case "review": {
@@ -126,7 +137,9 @@ function computeBlocker(status, stopReason, lastStep, pr, ci, deps, openQ) {
126
137
  case "failed":
127
138
  return { icon: "\u274C", text: stopReason || "\u0443\u043F\u0430\u043B", tone: "fail" };
128
139
  case "stopped":
129
- return { icon: "\u23F9", text: stopReason || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", tone: "queued" };
140
+ return { icon: "\u23F9", text: stopReason || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", tone: "stopped" };
141
+ case "superseded":
142
+ return { icon: "\u21BA", text: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442", tone: "superseded" };
130
143
  case "done":
131
144
  return { icon: "\u2705", text: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", tone: "done" };
132
145
  default:
@@ -377,10 +390,15 @@ __export(config_exports, {
377
390
  SWARM_OUTBOX: () => SWARM_OUTBOX,
378
391
  SWARM_PROD_HEARTBEAT_MS: () => SWARM_PROD_HEARTBEAT_MS,
379
392
  SWARM_REHYDRATE_MS: () => SWARM_REHYDRATE_MS,
393
+ SWARM_RELIEF_LOAD_FACTOR: () => SWARM_RELIEF_LOAD_FACTOR,
394
+ SWARM_RELIEF_MAX_FD_LOAD_FACTOR: () => SWARM_RELIEF_MAX_FD_LOAD_FACTOR,
395
+ SWARM_RELIEF_MIN_FREE_GB: () => SWARM_RELIEF_MIN_FREE_GB,
380
396
  SWARM_REPO: () => SWARM_REPO,
397
+ SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
381
398
  SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
382
399
  SWARM_SHOT_TIMEOUT_MS: () => SWARM_SHOT_TIMEOUT_MS,
383
400
  SWARM_STORM_URL: () => SWARM_STORM_URL,
401
+ SWARM_SWEEP_INTERVAL_MS: () => SWARM_SWEEP_INTERVAL_MS,
384
402
  SWARM_VERIFY: () => SWARM_VERIFY,
385
403
  TASKLESS_API: () => TASKLESS_API,
386
404
  TASKLESS_MCP_PATH: () => TASKLESS_MCP_PATH,
@@ -444,7 +462,7 @@ function reviewCutoffAt() {
444
462
  }
445
463
  return at;
446
464
  }
447
- var SWARM_HOME, DB_PATH, WORKTREES_DIR, RUN_LOGS_DIR, 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_REHYDRATE_MS, SWARM_PROD_HEARTBEAT_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS;
465
+ var SWARM_HOME, DB_PATH, WORKTREES_DIR, RUN_LOGS_DIR, 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_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_REHYDRATE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_SWEEP_INTERVAL_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_REPO_PATHS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS;
448
466
  var init_config = __esm({
449
467
  "src/config.ts"() {
450
468
  "use strict";
@@ -462,11 +480,16 @@ var init_config = __esm({
462
480
  SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
463
481
  SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
464
482
  SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
483
+ SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
484
+ SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
485
+ SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
465
486
  SWARM_REHYDRATE_MS = envNum("SWARM_REHYDRATE_MS", 15e3);
466
487
  SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
488
+ SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
467
489
  HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join(SWARM_HOME, "overmind.heartbeat");
468
490
  HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
469
491
  SWARM_REPO = process.env.SWARM_REPO || "";
492
+ SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
470
493
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
471
494
  SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
472
495
  TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
@@ -607,7 +630,7 @@ var db_exports = {};
607
630
  __export(db_exports, {
608
631
  SwarmDb: () => SwarmDb
609
632
  });
610
- import Database from "better-sqlite3";
633
+ import { createRequire } from "module";
611
634
  import { existsSync } from "fs";
612
635
  import { resolve as resolve2 } from "path";
613
636
  function rowToQuestion(row) {
@@ -658,6 +681,16 @@ function rowToRun(row) {
658
681
  updated_at: row.updated_at
659
682
  };
660
683
  }
684
+ function loadBetterSqlite3() {
685
+ const require2 = createRequire(import.meta.url);
686
+ try {
687
+ return require2("better-sqlite3");
688
+ } catch (err) {
689
+ throw new Error(
690
+ `better-sqlite3 \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D. Sqlite-\u0440\u0435\u0436\u0438\u043C (SwarmDb) \u2014 \u0434\u0435\u0444\u043E\u043B\u0442 \u043C\u043E\u043D\u043E\u0440\u0435\u043F\u044B/\u0442\u0435\u0441\u0442\u043E\u0432; \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432 client-\u0440\u0435\u0436\u0438\u043C\u0435 \u0438 sqlite \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442. \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 optional dependency (npm i, \u0431\u0435\u0437 --omit=optional) \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 SWARM_BACKEND=client. \u0418\u0441\u0445\u043E\u0434\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430: ${err.message}`
691
+ );
692
+ }
693
+ }
661
694
  var TERMINAL_SQL, RESOLVER_TERMINAL_SQL, SCHEMA, SwarmDb;
662
695
  var init_db = __esm({
663
696
  "src/db.ts"() {
@@ -733,9 +766,12 @@ CREATE TABLE IF NOT EXISTS imports (
733
766
  SwarmDb = class {
734
767
  db;
735
768
  defaultRepo;
769
+ databaseCtor;
770
+ // для importDb (второй Database, source)
736
771
  constructor(path = "swarm.db", defaultRepo = "") {
737
772
  this.defaultRepo = normalizeRepo(defaultRepo);
738
- this.db = new Database(path);
773
+ this.databaseCtor = loadBetterSqlite3();
774
+ this.db = new this.databaseCtor(path);
739
775
  this.db.exec("PRAGMA journal_mode = WAL;");
740
776
  this.db.exec(SCHEMA);
741
777
  for (const alter of [
@@ -871,6 +907,33 @@ CREATE TABLE IF NOT EXISTS imports (
871
907
  const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ? AND status NOT IN ('failed','stalled')").get(reviewRunId);
872
908
  return row.n;
873
909
  }
910
+ /**
911
+ * Жёсткий кап ревью-спавнов (TASWM-204, карусель мёртвых ревьюеров): сколько ревьюеров
912
+ * ВСЕГО (любой статус, включая failed/stalled — специально в отличие от reviewerCountFor
913
+ * выше) когда-либо поднималось на review-прогон #reviewRunId. Зеркалит resolverCountFor —
914
+ * плоский потолок попыток. Именно это ловит карусель: reviewerCountFor исключает
915
+ * failed/stalled (чтобы ОДИН транзиентный сбой не блокировал ревью навсегда), но тем
916
+ * самым допускает бесконечный ретрай, если ревьюер стабильно умирает без done().
917
+ */
918
+ reviewerAttemptCountFor(reviewRunId) {
919
+ const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ?").get(reviewRunId);
920
+ return row.n;
921
+ }
922
+ /** Последняя (любой статус) попытка ревьюера на review-прогон #reviewRunId (TASWM-196) —
923
+ * см. store.ts:lastReviewerAttemptFor. */
924
+ lastReviewerAttemptFor(reviewRunId) {
925
+ const row = this.db.prepare("SELECT * FROM runs WHERE reviews_run_id = ? ORDER BY id DESC LIMIT 1").get(reviewRunId);
926
+ return row ? rowToRun(row) : null;
927
+ }
928
+ /**
929
+ * Живые/ждущие ревьюер-прогоны review-прогона #reviewRunId (TASWM-204): для
930
+ * closeReviewersOnMerge — PR смержен раньше вердикта, зависшего ревьюера пора закрыть,
931
+ * не дожидаясь done(). Точечный WHERE, не полный скан listRuns() (code-review TASWM-204).
932
+ */
933
+ activeReviewersFor(reviewRunId) {
934
+ const rows = this.db.prepare(`SELECT * FROM runs WHERE reviews_run_id = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC`).all(reviewRunId);
935
+ return rows.map(rowToRun);
936
+ }
874
937
  /** Последний живой (не терминальный) прогон по slug — для swarm cancel. */
875
938
  activeRunForSlug(slug) {
876
939
  const row = this.db.prepare(
@@ -931,7 +994,7 @@ CREATE TABLE IF NOT EXISTS imports (
931
994
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
932
995
  if (already) return { imported: false, runs: already.run_count };
933
996
  if (!existsSync(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
934
- const src = new Database(abs);
997
+ const src = new this.databaseCtor(abs);
935
998
  try {
936
999
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
937
1000
  const events = src.prepare("SELECT * FROM events ORDER BY id").all();
@@ -1368,9 +1431,14 @@ var init_workspace = __esm({
1368
1431
  // src/taskless.ts
1369
1432
  var taskless_exports = {};
1370
1433
  __export(taskless_exports, {
1434
+ TASK_CONTEXT_MAX_COMMENTS: () => TASK_CONTEXT_MAX_COMMENTS,
1435
+ TASK_CONTEXT_MAX_COMMENT_CHARS: () => TASK_CONTEXT_MAX_COMMENT_CHARS,
1436
+ TASK_CONTEXT_MAX_DOCS: () => TASK_CONTEXT_MAX_DOCS,
1437
+ TASK_CONTEXT_MAX_DOC_CHARS: () => TASK_CONTEXT_MAX_DOC_CHARS,
1371
1438
  connectSwarmMcp: () => connectSwarmMcp,
1372
1439
  connectTaskless: () => connectTaskless,
1373
1440
  extractText: () => extractText,
1441
+ fetchTaskContext: () => fetchTaskContext,
1374
1442
  makeTasklessClient: () => makeTasklessClient
1375
1443
  });
1376
1444
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -1399,12 +1467,24 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
1399
1467
  url: taskUrlBase ? `${taskUrlBase}/tasks/${slug}` : null,
1400
1468
  parentId: task.parentId ?? null,
1401
1469
  projectId: task.projectId ?? null,
1402
- statusCategory: task.status?.category ?? null
1470
+ statusCategory: task.status?.category ?? null,
1471
+ comments: (task.comments ?? []).map((c) => ({
1472
+ author: c.author?.displayName ?? "\u2014",
1473
+ createdAt: (c.createdAt ?? "").slice(0, 16).replace("T", " "),
1474
+ text: extractText(c.body)
1475
+ })),
1476
+ documents: (task.documents ?? []).filter((d) => !!d.id).map((d) => ({ id: d.id, title: d.title ?? d.id, url: d.url ?? null }))
1403
1477
  };
1404
1478
  idCache.set(slug, body.id);
1405
1479
  idCache.set(body.id, body.id);
1406
1480
  return body;
1407
1481
  }
1482
+ async function getDocument(id) {
1483
+ const res = await safeCall("get_document", { id });
1484
+ const doc = res?.structuredContent?.document ?? null;
1485
+ if (!doc) return null;
1486
+ return { title: doc.title ?? "", body: extractText(doc.body) };
1487
+ }
1408
1488
  async function resolveId(idOrSlug) {
1409
1489
  const cached = idCache.get(idOrSlug);
1410
1490
  if (cached) return cached;
@@ -1434,15 +1514,18 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
1434
1514
  if (id) await safeCall("add_comment", { taskId: id, body });
1435
1515
  },
1436
1516
  backlogStatusId,
1517
+ getDocument,
1437
1518
  createTask: async (input) => {
1519
+ if (!input.projectId) {
1520
+ throw new Error("createTask: projectId \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u2014 \u0430\u0432\u0442\u043E-\u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0440\u043E\u0435\u043C \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u043E (TASWM-204)");
1521
+ }
1438
1522
  const res = await safeCall("create_task", {
1439
1523
  ...input.parentId ? { parentId: input.parentId } : {},
1440
1524
  title: input.title,
1441
1525
  description: input.description,
1442
1526
  ...input.estimateHours != null ? { estimateHours: input.estimateHours } : {},
1443
1527
  ...input.statusId ? { statusId: input.statusId } : {},
1444
- // TASWM-138: без projectId подзадача — сирота, swarm_claim_work её не возьмёт.
1445
- ...input.projectId ? { projectId: input.projectId } : {},
1528
+ projectId: input.projectId,
1446
1529
  force: true
1447
1530
  });
1448
1531
  const task = res?.structuredContent?.task ?? null;
@@ -1454,6 +1537,44 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
1454
1537
  }
1455
1538
  };
1456
1539
  }
1540
+ async function fetchTaskContext(client, task) {
1541
+ const parts = [];
1542
+ const comments = task.comments ?? [];
1543
+ if (comments.length > 0) {
1544
+ const tail = comments.slice(-TASK_CONTEXT_MAX_COMMENTS);
1545
+ const skipped = comments.length - tail.length;
1546
+ const lines = tail.map((c) => {
1547
+ const cut = c.text.length > TASK_CONTEXT_MAX_COMMENT_CHARS;
1548
+ const text = cut ? `${c.text.slice(0, TASK_CONTEXT_MAX_COMMENT_CHARS)}\u2026 (\u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E)` : c.text;
1549
+ return `\u2014 [${c.createdAt} \xB7 ${c.author}] ${text}`;
1550
+ });
1551
+ const header = skipped > 0 ? `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}, \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${tail.length} \u2014 \u0441\u0430\u043C\u044B\u0435 \u0441\u0432\u0435\u0436\u0438\u0435):` : `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}):`;
1552
+ parts.push([header, ...lines].join("\n"));
1553
+ }
1554
+ const documents = task.documents ?? [];
1555
+ if (documents.length > 0) {
1556
+ const head = documents.slice(0, TASK_CONTEXT_MAX_DOCS);
1557
+ const rest = documents.slice(TASK_CONTEXT_MAX_DOCS);
1558
+ const renderedHead = await Promise.all(
1559
+ head.map(async (d) => {
1560
+ const doc = await client.getDocument(d.id);
1561
+ if (!doc) return `### ${d.title}${d.url ? ` (${d.url})` : ""}
1562
+ (\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442)`;
1563
+ const cut = doc.body.length > TASK_CONTEXT_MAX_DOC_CHARS;
1564
+ const text = cut ? `${doc.body.slice(0, TASK_CONTEXT_MAX_DOC_CHARS)}\u2026` : doc.body;
1565
+ return `### ${d.title}${d.url ? ` (${d.url})` : ""}${cut ? " \u2014 \u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E, \u043F\u043E\u043B\u043D\u044B\u0439 \u0442\u0435\u043A\u0441\u0442 \u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435/get_document" : ""}
1566
+ ${text || "(\u043F\u0443\u0441\u0442\u043E)"}`;
1567
+ })
1568
+ );
1569
+ const restLines = rest.map((d) => `- ${d.title}${d.url ? ` (${d.url})` : ""} \u2014 \u0442\u0435\u043B\u043E \u043D\u0435 \u043F\u043E\u0434\u0442\u044F\u043D\u0443\u0442\u043E, \u0447\u0438\u0442\u0430\u0439 \u0447\u0435\u0440\u0435\u0437 get_document/read_task`);
1570
+ parts.push(
1571
+ [`\u041F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0435 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B (${documents.length}):`, ...renderedHead, ...restLines.length ? [restLines.join("\n")] : []].join(
1572
+ "\n\n"
1573
+ )
1574
+ );
1575
+ }
1576
+ return parts.join("\n\n");
1577
+ }
1457
1578
  function parseJsonContent(res) {
1458
1579
  const text = res?.content?.find((c) => c.type === "text")?.text;
1459
1580
  if (!text) return null;
@@ -1486,7 +1607,7 @@ async function connectTaskless(cfg) {
1486
1607
  });
1487
1608
  const client = new Client({ name: "taskless-swarm", version: "0.1.0" }, { capabilities: {} });
1488
1609
  try {
1489
- await client.connect(transport);
1610
+ await client.connect(transport, { timeout: TASKLESS_CONNECT_TIMEOUT_MS });
1490
1611
  } catch (e) {
1491
1612
  console.warn(`Taskless: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 slug-only. \u0412\u043E\u0439\u0434\u0438 \u0437\u0430\u043D\u043E\u0432\u043E.`);
1492
1613
  return null;
@@ -1524,7 +1645,7 @@ async function connectSwarmMcp(cfg) {
1524
1645
  }
1525
1646
  return async (name, args) => await client.callTool({ name, arguments: { ...args, ...ws } });
1526
1647
  }
1527
- var SLUG_RE;
1648
+ var SLUG_RE, TASK_CONTEXT_MAX_DOCS, TASK_CONTEXT_MAX_DOC_CHARS, TASK_CONTEXT_MAX_COMMENTS, TASK_CONTEXT_MAX_COMMENT_CHARS, TASKLESS_CONNECT_TIMEOUT_MS;
1528
1649
  var init_taskless = __esm({
1529
1650
  "src/taskless.ts"() {
1530
1651
  "use strict";
@@ -1533,6 +1654,11 @@ var init_taskless = __esm({
1533
1654
  init_oauth();
1534
1655
  init_workspace();
1535
1656
  SLUG_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
1657
+ TASK_CONTEXT_MAX_DOCS = 5;
1658
+ TASK_CONTEXT_MAX_DOC_CHARS = 4e3;
1659
+ TASK_CONTEXT_MAX_COMMENTS = 30;
1660
+ TASK_CONTEXT_MAX_COMMENT_CHARS = 2e3;
1661
+ TASKLESS_CONNECT_TIMEOUT_MS = 1e4;
1536
1662
  }
1537
1663
  });
1538
1664
 
@@ -1541,7 +1667,7 @@ init_db();
1541
1667
  import { existsSync as existsSync4, readFileSync as readFileSync8 } from "fs";
1542
1668
  import { basename as basename3, dirname as dirname2, join as join7 } from "path";
1543
1669
  import { fileURLToPath, pathToFileURL } from "url";
1544
- import { execFileSync as execFileSync2 } from "child_process";
1670
+ import { execFileSync as execFileSync3 } from "child_process";
1545
1671
 
1546
1672
  // src/swarmClient.ts
1547
1673
  init_config();
@@ -1618,13 +1744,26 @@ function fileDescriptorLoadRatio() {
1618
1744
  return null;
1619
1745
  }
1620
1746
  var RESOURCES_CACHE_MS = 250;
1621
- var resourcesCache = null;
1622
- function hostResourcesOk() {
1747
+ var readingCache = null;
1748
+ function hostReading() {
1623
1749
  const now = Date.now();
1624
- if (resourcesCache && now - resourcesCache.at < RESOURCES_CACHE_MS) return resourcesCache.ok;
1625
- const ok = resourcesOk(os.loadavg()[0] ?? 0, os.cpus().length, availableMemBytes() / 1e9, void 0, void 0, fileDescriptorLoadRatio());
1626
- resourcesCache = { at: now, ok };
1627
- return ok;
1750
+ if (readingCache && now - readingCache.at < RESOURCES_CACHE_MS) return readingCache.reading;
1751
+ const reading = {
1752
+ load1: os.loadavg()[0] ?? 0,
1753
+ cpus: os.cpus().length,
1754
+ freeGb: availableMemBytes() / 1e9,
1755
+ fdLoadRatio: fileDescriptorLoadRatio()
1756
+ };
1757
+ readingCache = { at: now, reading };
1758
+ return reading;
1759
+ }
1760
+ function hostResourcesOk() {
1761
+ const r = hostReading();
1762
+ return resourcesOk(r.load1, r.cpus, r.freeGb, void 0, void 0, r.fdLoadRatio);
1763
+ }
1764
+ function hostReliefNeeded() {
1765
+ const r = hostReading();
1766
+ return !resourcesOk(r.load1, r.cpus, r.freeGb, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, r.fdLoadRatio, SWARM_RELIEF_MAX_FD_LOAD_FACTOR);
1628
1767
  }
1629
1768
 
1630
1769
  // src/swarmClient.ts
@@ -1654,6 +1793,10 @@ var SwarmClient = class _SwarmClient {
1654
1793
  defaultRepo;
1655
1794
  autoFlush;
1656
1795
  listLimit;
1796
+ // TASWM-169: репо-реестр демона — repoFullName[] на claim/heartbeat + обратная
1797
+ // карта repoFullName→локальный путь для трансляции run.repo из прода (см. resolveRepo).
1798
+ repoFullNames;
1799
+ repoPathByFullName;
1657
1800
  // Зеркало.
1658
1801
  runs = /* @__PURE__ */ new Map();
1659
1802
  runtime = /* @__PURE__ */ new Map();
@@ -1686,6 +1829,9 @@ var SwarmClient = class _SwarmClient {
1686
1829
  this.defaultRepo = normalizeRepo(opts.defaultRepo ?? "");
1687
1830
  this.autoFlush = opts.autoFlush ?? true;
1688
1831
  this.listLimit = opts.listLimit ?? 200;
1832
+ const registry = opts.repoRegistry ?? [];
1833
+ this.repoFullNames = registry.map((e) => e.repoFullName);
1834
+ this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
1689
1835
  this.outboxPath = opts.outboxPath;
1690
1836
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
1691
1837
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
@@ -1932,6 +2078,19 @@ var SwarmClient = class _SwarmClient {
1932
2078
  (r) => r.reviews_run_id === reviewRunId && r.status !== "failed" && r.status !== "stalled"
1933
2079
  ).length;
1934
2080
  }
2081
+ // Жёсткий кап ревью-спавнов (TASWM-204) — см. db.ts:reviewerAttemptCountFor.
2082
+ reviewerAttemptCountFor(reviewRunId) {
2083
+ return [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId).length;
2084
+ }
2085
+ // Последняя попытка ревьюера (TASWM-196) — см. db.ts:lastReviewerAttemptFor.
2086
+ lastReviewerAttemptFor(reviewRunId) {
2087
+ const cands = [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId).sort((a, b) => b.id - a.id);
2088
+ return cands.length ? this.mat(cands[0]) : null;
2089
+ }
2090
+ // Живые/ждущие ревьюер-прогоны (TASWM-204) — см. db.ts:activeReviewersFor.
2091
+ activeReviewersFor(reviewRunId) {
2092
+ return [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId && !TERMINAL_STATUSES.includes(r.status)).sort((a, b) => b.id - a.id).map((d) => this.mat(d));
2093
+ }
1935
2094
  activeRunForSlug(slug) {
1936
2095
  const cands = [...this.runs.values()].filter((r) => r.task_slug === slug && !SLUG_TERMINAL.has(r.status)).sort((a, b) => b.id - a.id);
1937
2096
  return cands.length ? this.mat(cands[0]) : null;
@@ -2252,11 +2411,17 @@ var SwarmClient = class _SwarmClient {
2252
2411
  * cuid известен сразу, никакого create-flush. Возвращает локальный Run (queued; overmind
2253
2412
  * заспавнит его следующим тиком) либо null: нет claimable-задачи / прод недоступен / тул
2254
2413
  * отверг — во всех случаях тик продолжается, следующий ретраит.
2414
+ *
2415
+ * TASWM-169: repoFullNames — ЖИВОЙ реестр демона на момент ЭТОГО вызова, а не
2416
+ * последняя heartbeat-декларация (heartbeat — своя кадансность, ~15с; claim — ~60с;
2417
+ * плюс окно до первого успешного heartbeat при рестарте) — свежий аргумент здесь
2418
+ * убирает то окно рассинхрона целиком. Пусто (демон без SWARM_REPOS) → ключ не
2419
+ * шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
2255
2420
  */
2256
2421
  async claim() {
2257
2422
  let res;
2258
2423
  try {
2259
- res = await this.call("swarm_claim_work", {});
2424
+ res = await this.call("swarm_claim_work", this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {});
2260
2425
  } catch {
2261
2426
  return null;
2262
2427
  }
@@ -2286,16 +2451,41 @@ var SwarmClient = class _SwarmClient {
2286
2451
  const n = obj?.maxDrones;
2287
2452
  return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
2288
2453
  }
2454
+ /**
2455
+ * Live verifyMode проекта из Taskless (TASWM-171): читает swarm_repo_config по
2456
+ * repoFullName (owner/repo из origin — см. git.remoteRepoFullName) — Project.swarmConfig.
2457
+ * verifyMode ("local"/"ci"). Прод недоступен / repo не смэплен на swarm-enabled проект
2458
+ * владельца токена (config=null в ответе) → null: ридер (makeVerifyModeReader) берёт
2459
+ * env-фолбэк SWARM_VERIFY.
2460
+ */
2461
+ async repoVerifyMode(repoFullName) {
2462
+ let res;
2463
+ try {
2464
+ res = await this.call("swarm_repo_config", { repoFullName });
2465
+ } catch {
2466
+ return null;
2467
+ }
2468
+ if (res?.isError) return null;
2469
+ const sc = res.structuredContent ?? {};
2470
+ const v = sc.config?.verifyMode;
2471
+ return v === "local" || v === "ci" ? v : null;
2472
+ }
2289
2473
  /**
2290
2474
  * Пульс overmind в прод (TASWM-129): swarm_heartbeat обновляет User.swarmHeartbeatAt —
2291
2475
  * дашборд /swarm сравнивает свежесть со своим порогом устаревания («нет связи с
2292
2476
  * овермайндом» вместо устаревшего «живого» роя). Best-effort, как ownerMaxDrones/claim:
2293
2477
  * прод недоступен/тул отверг → просто не обновили в этом тике, следующий ретраит —
2294
2478
  * не роняем тик демона из-за упавшего пульса.
2479
+ *
2480
+ * TASWM-169: заодно декларирует repoFullNames (репо-реестр демона) — та же
2481
+ * кадансность, что и пульс, естественная точка «сказать серверу, что обслуживаю».
2482
+ * Пустой реестр (демон не сконфигурирован на multi-repo/SWARM_REPOS) → ключ не
2483
+ * шлём вовсе (undefined) — прод НЕ трогает прошлую декларацию (см. swarm.ts), не
2484
+ * стирает её ложным «репо больше нет» из-за демона без SWARM_REPOS в env.
2295
2485
  */
2296
2486
  async heartbeat() {
2297
2487
  try {
2298
- await this.call("swarm_heartbeat", {});
2488
+ await this.call("swarm_heartbeat", this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {});
2299
2489
  } catch {
2300
2490
  }
2301
2491
  }
@@ -2329,6 +2519,19 @@ var SwarmClient = class _SwarmClient {
2329
2519
  });
2330
2520
  if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
2331
2521
  }
2522
+ /**
2523
+ * TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
2524
+ * через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
2525
+ * сервер не знает локальных fs-путей. Транслируем в путь по СВОЕМУ репо-реестру
2526
+ * (см. repoRegistry.ts), т.к. git-операции (addWorktree/droneWorktreePath) ждут
2527
+ * именно путь. Не нашли в реестре (репо не наш / ручной `--repo <path>` спавн, где
2528
+ * repo — ВСЕГДА уже путь, старый формат) → возвращаем как есть без изменений
2529
+ * (обратная совместимость: раньше pr.repo ВСЕГДА был локальным путём).
2530
+ */
2531
+ resolveRepo(raw) {
2532
+ if (!raw) return "";
2533
+ return this.repoPathByFullName.get(raw) ?? raw;
2534
+ }
2332
2535
  hydrateRun(pr) {
2333
2536
  const prodId = String(pr.id ?? "");
2334
2537
  if (!prodId) return null;
@@ -2384,7 +2587,7 @@ var SwarmClient = class _SwarmClient {
2384
2587
  const revProd = pr.reviewsRunId ?? null;
2385
2588
  if (revProd) local.reviews_run_id = this.localIdForProdId(revProd) ?? local.reviews_run_id;
2386
2589
  local.review_verdict = pr.reviewVerdict ?? local.review_verdict;
2387
- local.repo = pr.repo ?? "";
2590
+ local.repo = this.resolveRepo(pr.repo ?? null);
2388
2591
  local.branch = pr.branch ?? null;
2389
2592
  local.status = pr.status ?? local.status;
2390
2593
  local.area = pr.area ?? null;
@@ -2450,11 +2653,66 @@ var SwarmClient = class _SwarmClient {
2450
2653
  }, this.backoffMs);
2451
2654
  if (typeof this.flushTimer.unref === "function") this.flushTimer.unref();
2452
2655
  }
2453
- resolveArgs(e) {
2656
+ // TASWM-202: create-флаш без прод-id слал taskId=e.taskRef НАПРЯМУЮ — а taskRef это
2657
+ // человеческий слаг (task_slug, напр. «TASWM-171»), не cuid. Резолверы конфликта
2658
+ // рождаются ЛОКАЛЬНО (createRun с uuid-runKey) и попадают в outbox БЕЗ прод-id —
2659
+ // обычные прогоны рождаются в проде (claim) и гидрируются сразу с cuid, эта ветка
2660
+ // их не касается. Прод create-схема ждёт cuid → отбивает слаг → строго
2661
+ // упорядоченный flush стоит на этой голове НАВЕЧНО (ядовитая голова, инцидент
2662
+ // 14.07/18.07, TASWM-201 — общий карантин; здесь — конкретный источник). Резолвим
2663
+ // слаг→cuid через get_task (кэш — taskIdCache) ПЕРЕД тем как слать create.
2664
+ taskIdCache = /* @__PURE__ */ new Map();
2665
+ // task_slug → cuid прода
2666
+ // Create для этого runKey резолвился окончательно неудачно (задача не найдена/
2667
+ // недоступна) — событие/вопросы того же прогона тоже никогда не получат prod-id,
2668
+ // без карантина они бы бросали «нет prod-id» на каждом флаше и СНОВА вставали
2669
+ // ядовитой головой (просто на одну запись ниже по очереди).
2670
+ poisonedRunKeys = /* @__PURE__ */ new Set();
2671
+ // "transient" — прод/сеть недоступны, ответа нет или ошибка НЕ про отсутствие задачи
2672
+ // (offline-стаб CLI, auth-протух, 5xx): НЕ терминально — запись остаётся в outbox и
2673
+ // повторится на следующем флаше (иначе сетевой сбой = потеря прогона). null —
2674
+ // терминально: прод ЯВНО ответил «задачи нет» (task_not_found / not_found в тексте
2675
+ // tool-ошибки) либо успешный ответ без id — только это карантиним.
2676
+ async resolveTaskCuid(taskRef) {
2677
+ const cached = this.taskIdCache.get(taskRef);
2678
+ if (cached) return cached;
2679
+ let res;
2680
+ try {
2681
+ res = await this.call("get_task", { id: taskRef });
2682
+ } catch {
2683
+ return "transient";
2684
+ }
2685
+ if (!res) return "transient";
2686
+ if (res.isError) {
2687
+ const text = (res.content ?? []).map((c) => c.text ?? "").join(" ");
2688
+ return /not_found/i.test(text) ? null : "transient";
2689
+ }
2690
+ const id = res.structuredContent?.task?.id;
2691
+ if (!id) return null;
2692
+ this.taskIdCache.set(taskRef, id);
2693
+ return id;
2694
+ }
2695
+ async resolveArgs(e) {
2454
2696
  if (e.t === "run") {
2455
2697
  const fields = this.materializeRefs(e.fields);
2456
2698
  const prodId2 = this.prodIdByKey.get(e.runKey);
2457
- return prodId2 ? { tool: "swarm_run_upsert", args: { runId: prodId2, ...fields } } : { tool: "swarm_run_upsert", args: { taskId: e.taskRef, ...fields } };
2699
+ if (prodId2) return { tool: "swarm_run_upsert", args: { runId: prodId2, ...fields } };
2700
+ const taskId = await this.resolveTaskCuid(e.taskRef);
2701
+ if (taskId === "transient") {
2702
+ throw new Error(`slug\u2192cuid \u0434\u043B\u044F \xAB${e.taskRef}\xBB \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (\u043F\u0440\u043E\u0434 \u043C\u043E\u0440\u0433\u043D\u0443\u043B) \u2014 retry`);
2703
+ }
2704
+ if (!taskId) {
2705
+ console.error(
2706
+ `[swarm-client] create-\u0444\u043B\u0430\u0448 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: \u043F\u0440\u043E\u0434 \u043D\u0435 \u0437\u043D\u0430\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0443 \xAB${e.taskRef}\xBB (runKey=${e.runKey}) \u2014 \u0437\u0430\u043F\u0438\u0441\u044C \u0432\u044B\u0431\u0440\u043E\u0448\u0435\u043D\u0430 \u0438\u0437 outbox`
2707
+ );
2708
+ this.poisonedRunKeys.add(e.runKey);
2709
+ return "skip";
2710
+ }
2711
+ return { tool: "swarm_run_upsert", args: { taskId, ...fields } };
2712
+ }
2713
+ if (this.poisonedRunKeys.has(e.runKey)) {
2714
+ console.error(`[swarm-client] ${e.t}-\u0444\u043B\u0430\u0448 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: create \u043F\u0440\u043E\u0433\u043E\u043D\u0430 runKey=${e.runKey} \u043D\u0435 \u0440\u0435\u0437\u043E\u043B\u0432\u0438\u043B\u0441\u044F \u0440\u0430\u043D\u0435\u0435 \u2014 \u0437\u0430\u043F\u0438\u0441\u044C \u0432\u044B\u0431\u0440\u043E\u0448\u0435\u043D\u0430 \u0438\u0437 outbox`);
2715
+ return "skip";
2458
2716
  }
2459
2717
  const prodId = this.prodIdByKey.get(e.runKey);
2460
2718
  if (!prodId) throw new Error(`\u043D\u0435\u0442 prod-id \u0434\u043B\u044F ${e.runKey} \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0435\u0449\u0451 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D \u0432 \u043F\u0440\u043E\u0434\u0435`);
@@ -2488,10 +2746,14 @@ var SwarmClient = class _SwarmClient {
2488
2746
  const e = this.pending[0];
2489
2747
  let call;
2490
2748
  try {
2491
- call = this.resolveArgs(e);
2749
+ call = await this.resolveArgs(e);
2492
2750
  } catch {
2493
2751
  break;
2494
2752
  }
2753
+ if (call === "skip") {
2754
+ this.pending.shift();
2755
+ continue;
2756
+ }
2495
2757
  let res;
2496
2758
  try {
2497
2759
  res = await this.call(call.tool, call.args);
@@ -2581,6 +2843,65 @@ function overmindAlive(path, now, staleMs) {
2581
2843
  return now - ts < staleMs;
2582
2844
  }
2583
2845
 
2846
+ // src/repoRegistry.ts
2847
+ init_config();
2848
+ import { $ } from "execa";
2849
+ function parseRepoFullName(remoteUrl) {
2850
+ const m = remoteUrl.trim().match(/^(?:https?:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(\.git)?\/?$/i);
2851
+ if (!m) return null;
2852
+ return `${m[1]}/${m[2]}`.toLowerCase();
2853
+ }
2854
+ async function resolveRepoFullName(path) {
2855
+ const res = await $({ reject: false })`git -C ${path} remote get-url origin`;
2856
+ if (res.exitCode !== 0) return null;
2857
+ return parseRepoFullName(res.stdout);
2858
+ }
2859
+ async function buildRepoRegistry(paths) {
2860
+ const resolved = await Promise.all(
2861
+ paths.map(async (raw) => {
2862
+ const path = normalizeRepo(raw);
2863
+ if (!path) return null;
2864
+ const repoFullName = await resolveRepoFullName(path);
2865
+ return repoFullName ? { repoFullName, path } : null;
2866
+ })
2867
+ );
2868
+ return resolved.filter((e) => e !== null);
2869
+ }
2870
+
2871
+ // src/relief.ts
2872
+ var numEnv2 = (name, def) => {
2873
+ const v = Number(process.env[name]);
2874
+ return Number.isFinite(v) && v > 0 ? v : def;
2875
+ };
2876
+ var DEFAULT_RELIEF_CONFIG = {
2877
+ rampWindowMs: numEnv2("SWARM_RELIEF_RAMP_WINDOW_MS", 2e4),
2878
+ pauseWindowMs: numEnv2("SWARM_RELIEF_PAUSE_WINDOW_MS", 3e4),
2879
+ sustainMs: numEnv2("SWARM_RELIEF_SUSTAIN_MS", 15e3)
2880
+ };
2881
+ function resolveReliefConfig(cfg) {
2882
+ return { ...DEFAULT_RELIEF_CONFIG, ...cfg ?? {} };
2883
+ }
2884
+ var RELIEF_PAUSE_REASON = "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430: \u0445\u043E\u0441\u0442 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D (SIGTERM, \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0447\u0435\u0440\u0435\u0437 --continue), \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 \u043F\u043E\u0441\u043B\u0435 \u043D\u043E\u0440\u043C\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438 (TASWM-151)";
2885
+ function rampDue(lastRampAt, now, cfg) {
2886
+ return lastRampAt == null || now - lastRampAt >= cfg.rampWindowMs;
2887
+ }
2888
+ function pauseDue(reliefSince, lastPauseAt, now, cfg) {
2889
+ if (reliefSince == null || now - reliefSince < cfg.sustainMs) return false;
2890
+ return lastPauseAt == null || now - lastPauseAt >= cfg.pauseWindowMs;
2891
+ }
2892
+ function pickPauseCandidate(runs) {
2893
+ const candidates = runs.filter(
2894
+ (r) => r.kind === "drone" && r.status === "running" && r.pid != null && r.resolves_run_id == null && r.reviews_run_id == null
2895
+ );
2896
+ if (!candidates.length) return null;
2897
+ return candidates.reduce((youngest, r) => r.created_at > youngest.created_at ? r : youngest);
2898
+ }
2899
+ function pickResumeCandidate(pausedForRelief) {
2900
+ const candidates = pausedForRelief.filter((r) => r.kind === "drone");
2901
+ if (!candidates.length) return null;
2902
+ return candidates.reduce((oldest, r) => r.updated_at < oldest.updated_at ? r : oldest);
2903
+ }
2904
+
2584
2905
  // src/launcher.ts
2585
2906
  import { spawn } from "child_process";
2586
2907
  import { closeSync, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync, readdirSync, statSync, writeFileSync as writeFileSync4 } from "fs";
@@ -2671,7 +2992,10 @@ function makeLauncher(cfg) {
2671
2992
  overmind: {
2672
2993
  command: isCompiled ? process.execPath : "bun",
2673
2994
  args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["run", cfg.overmindEntry, "--drone", "--run", String(run.id)],
2674
- env: { SWARM_DB: cfg.dbPath }
2995
+ // SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — устойчив к
2996
+ // пересозданию локального стора между спавном и вызовами read_task (droneServer.ts
2997
+ // доверяет ему, а не db.getRun(run.id).task_slug, который может протухнуть/переехать).
2998
+ env: { SWARM_DB: cfg.dbPath, SWARM_TASK_SLUG: run.task_slug }
2675
2999
  }
2676
3000
  }
2677
3001
  };
@@ -2698,6 +3022,168 @@ function makeLauncher(cfg) {
2698
3022
  };
2699
3023
  }
2700
3024
 
3025
+ // src/procCleanup.ts
3026
+ import { execFileSync as execFileSync2 } from "child_process";
3027
+ function sleep(ms) {
3028
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3029
+ }
3030
+ function parseLsofCwd(output) {
3031
+ const rows = [];
3032
+ let pid = null;
3033
+ for (const line of output.split("\n")) {
3034
+ const pMatch = /^p(\d+)$/.exec(line);
3035
+ if (pMatch) {
3036
+ pid = Number(pMatch[1]);
3037
+ } else if (line.startsWith("n") && pid != null) {
3038
+ rows.push({ pid, cwd: line.slice(1) });
3039
+ pid = null;
3040
+ }
3041
+ }
3042
+ return rows;
3043
+ }
3044
+ function cwdUnder(cwd, dir) {
3045
+ return cwd === dir || cwd.startsWith(`${dir}/`);
3046
+ }
3047
+ function pidsWithCwdUnder(dir) {
3048
+ let out;
3049
+ try {
3050
+ out = execFileSync2("lsof", ["-a", "-d", "cwd", "-F", "pn"], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
3051
+ } catch (e) {
3052
+ logTransientSpawnError("lsof", e);
3053
+ return [];
3054
+ }
3055
+ return parseLsofCwd(out).filter((r) => cwdUnder(r.cwd, dir));
3056
+ }
3057
+ function pidAlive(pid) {
3058
+ try {
3059
+ process.kill(pid, 0);
3060
+ return true;
3061
+ } catch (e) {
3062
+ return e.code === "EPERM";
3063
+ }
3064
+ }
3065
+ async function killPidsGraceful(pids, opts = {}) {
3066
+ const graceMs = opts.graceMs ?? 3e3;
3067
+ const pollMs = opts.pollMs ?? 150;
3068
+ const kill = opts.kill ?? ((pid, signal) => process.kill(pid, signal));
3069
+ const isAlive = opts.isAlive ?? pidAlive;
3070
+ const sleepFn = opts.sleepFn ?? sleep;
3071
+ const signalled = [];
3072
+ for (const pid of pids) {
3073
+ try {
3074
+ kill(pid, "SIGTERM");
3075
+ signalled.push(pid);
3076
+ } catch {
3077
+ }
3078
+ }
3079
+ if (!signalled.length) return signalled;
3080
+ const deadline = Date.now() + graceMs;
3081
+ while (Date.now() < deadline && signalled.some((pid) => isAlive(pid))) {
3082
+ await sleepFn(pollMs);
3083
+ }
3084
+ for (const pid of signalled) {
3085
+ try {
3086
+ if (isAlive(pid)) kill(pid, "SIGKILL");
3087
+ } catch {
3088
+ }
3089
+ }
3090
+ return signalled;
3091
+ }
3092
+ function killProcessGroup(pid, signal) {
3093
+ try {
3094
+ process.kill(-pid, signal);
3095
+ } catch {
3096
+ process.kill(pid, signal);
3097
+ }
3098
+ }
3099
+ function composeProjectPrefix(worktreeDirName) {
3100
+ return worktreeDirName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3101
+ }
3102
+ function containerBelongsToPrefix(name, prefix) {
3103
+ return name === prefix || name.startsWith(`${prefix}-`);
3104
+ }
3105
+ function findOrphanContainers(containerNames, runs) {
3106
+ const orphans = [];
3107
+ for (const name of containerNames) {
3108
+ let best = null;
3109
+ for (const r of runs) {
3110
+ if (!containerBelongsToPrefix(name, r.prefix)) continue;
3111
+ if (!best || r.prefix.length > best.prefix.length) best = r;
3112
+ }
3113
+ if (best && !best.active) orphans.push(name);
3114
+ }
3115
+ return orphans;
3116
+ }
3117
+ function dockerContainerNames() {
3118
+ try {
3119
+ return execFileSync2("docker", ["ps", "-a", "--format", "{{.Names}}"], { encoding: "utf8" }).split("\n").map((s) => s.trim()).filter(Boolean);
3120
+ } catch (e) {
3121
+ logTransientSpawnError("docker ps", e);
3122
+ return [];
3123
+ }
3124
+ }
3125
+ function removeContainers(names) {
3126
+ const removed = [];
3127
+ for (const name of names) {
3128
+ try {
3129
+ execFileSync2("docker", ["rm", "-f", name], { encoding: "utf8" });
3130
+ removed.push(name);
3131
+ } catch (e) {
3132
+ console.warn(`[procCleanup] docker rm -f ${name} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
3133
+ }
3134
+ }
3135
+ return removed;
3136
+ }
3137
+ async function cleanupWorktreeProcesses(worktree, opts = {}) {
3138
+ try {
3139
+ const found = pidsWithCwdUnder(worktree);
3140
+ if (found.length) {
3141
+ const killed = await killPidsGraceful(
3142
+ found.map((f) => f.pid),
3143
+ opts
3144
+ );
3145
+ if (killed.length) console.log(`[procCleanup] worktree ${worktree}: \u043F\u043E\u0433\u0430\u0448\u0435\u043D\u043E \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432 ${killed.length} (${killed.join(", ")})`);
3146
+ }
3147
+ } catch (e) {
3148
+ console.error(`[procCleanup] \u0433\u0430\u0448\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432 worktree ${worktree} \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:`, e);
3149
+ }
3150
+ try {
3151
+ const prefix = composeProjectPrefix(worktree.split("/").filter(Boolean).pop() ?? "");
3152
+ if (!prefix) return;
3153
+ const names = dockerContainerNames().filter((n) => containerBelongsToPrefix(n, prefix));
3154
+ if (!names.length) return;
3155
+ const removed = removeContainers(names);
3156
+ if (removed.length) console.log(`[procCleanup] worktree ${worktree}: \u0441\u043D\u0435\u0441\u0435\u043D\u043E docker-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u0432 ${removed.length} (${removed.join(", ")})`);
3157
+ } catch (e) {
3158
+ console.error(`[procCleanup] \u0441\u043D\u043E\u0441 docker-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u0432 worktree ${worktree} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
3159
+ }
3160
+ }
3161
+ async function sweepOrphans(params) {
3162
+ let killedPids = [];
3163
+ try {
3164
+ const found = pidsWithCwdUnder(params.worktreesDir);
3165
+ const orphanPids = found.filter((f) => ![...params.activeWorktreePaths].some((p) => cwdUnder(f.cwd, p))).map((f) => f.pid);
3166
+ if (orphanPids.length) {
3167
+ console.log(`[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep: ${orphanPids.length} \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0445 \u043F\u0440\u043E\u0446\u0435\u0441\u0441(\u043E\u0432) \u043F\u043E\u0434 ${params.worktreesDir} \u2014 \u0433\u0430\u0448\u0443`, orphanPids);
3168
+ killedPids = await killPidsGraceful(orphanPids, params.killOpts);
3169
+ }
3170
+ } catch (e) {
3171
+ console.error("[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
3172
+ }
3173
+ let removedContainers = [];
3174
+ try {
3175
+ const names = dockerContainerNames();
3176
+ const orphanNames = findOrphanContainers(names, params.containerRunPrefixes);
3177
+ if (orphanNames.length) {
3178
+ console.log(`[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep: ${orphanNames.length} docker-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440(\u043E\u0432)-\u0441\u0438\u0440\u043E\u0442 \u2014 \u0433\u0430\u0448\u0443`, orphanNames);
3179
+ removedContainers = removeContainers(orphanNames);
3180
+ }
3181
+ } catch (e) {
3182
+ console.error("[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
3183
+ }
3184
+ return { killedPids, removedContainers };
3185
+ }
3186
+
2701
3187
  // src/cost.ts
2702
3188
  import { readFileSync as readFileSync5, readdirSync as readdirSync2, renameSync as renameSync3 } from "fs";
2703
3189
  import { join as join3 } from "path";
@@ -2789,7 +3275,7 @@ function makeCostAccountant(deps) {
2789
3275
  }
2790
3276
 
2791
3277
  // src/git.ts
2792
- import { $ } from "execa";
3278
+ import { $ as $2 } from "execa";
2793
3279
  var WorktreeBusyError = class extends Error {
2794
3280
  constructor(branch, worktreePath) {
2795
3281
  super(`worktree \u0437\u0430\u043D\u044F\u0442: ${worktreePath} (\u0432\u0435\u0442\u043A\u0430 ${branch}) \u2014 \u043F\u0440\u0438\u0431\u0435\u0440\u0438 worktree \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438 \u043F\u0440\u043E\u0433\u043E\u043D (swarm cancel ${branch})`);
@@ -2801,10 +3287,10 @@ var WorktreeBusyError = class extends Error {
2801
3287
  worktreePath;
2802
3288
  };
2803
3289
  async function revParse(repo, ref) {
2804
- return (await $({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${ref}`).stdout.trim();
3290
+ return (await $2({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${ref}`).stdout.trim();
2805
3291
  }
2806
3292
  async function worktreeForBranch(repo, branch) {
2807
- const out = (await $`git -C ${repo} worktree list --porcelain`).stdout;
3293
+ const out = (await $2`git -C ${repo} worktree list --porcelain`).stdout;
2808
3294
  const target = `refs/heads/${branch}`;
2809
3295
  let current = null;
2810
3296
  for (const line of out.split("\n")) {
@@ -2813,22 +3299,29 @@ async function worktreeForBranch(repo, branch) {
2813
3299
  }
2814
3300
  return null;
2815
3301
  }
2816
- async function addWorktree(repo, path, branch, base2 = "HEAD") {
3302
+ async function addWorktree(repo, path, branch, base2) {
2817
3303
  const localSha = await revParse(repo, `refs/heads/${branch}`);
2818
3304
  const originSha = await revParse(repo, `refs/remotes/origin/${branch}`);
2819
3305
  if (!localSha && !originSha) {
2820
- await $`git -C ${repo} worktree add -b ${branch} ${path} ${base2}`;
3306
+ const freshBase = base2 ?? await freshWorktreeBase(repo);
3307
+ await $2`git -C ${repo} worktree add -b ${branch} ${path} ${freshBase}`;
2821
3308
  return { reused: false };
2822
3309
  }
2823
3310
  const busyAt = await worktreeForBranch(repo, branch);
2824
3311
  if (busyAt) throw new WorktreeBusyError(branch, busyAt);
2825
3312
  if (originSha) {
2826
- await $`git -C ${repo} worktree add -B ${branch} ${path} ${`origin/${branch}`}`;
3313
+ await $2`git -C ${repo} worktree add -B ${branch} ${path} ${`origin/${branch}`}`;
2827
3314
  return { reused: true, from: "origin", sha: originSha };
2828
3315
  }
2829
- await $`git -C ${repo} worktree add ${path} ${branch}`;
3316
+ await $2`git -C ${repo} worktree add ${path} ${branch}`;
2830
3317
  return { reused: true, from: "local" };
2831
3318
  }
3319
+ async function freshWorktreeBase(repo) {
3320
+ const hasOrigin = (await $2({ reject: false })`git -C ${repo} remote get-url origin`).exitCode === 0;
3321
+ if (!hasOrigin) return "HEAD";
3322
+ await $2`git -C ${repo} fetch --quiet origin`;
3323
+ return remoteMainRef(repo);
3324
+ }
2832
3325
  function worktreeSourceMessage(branch, res) {
2833
3326
  if (!res.reused) return null;
2834
3327
  if (res.from === "origin") return `\u0432\u0435\u0442\u043A\u0430 ${branch} \u043F\u0435\u0440\u0435\u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0430 \u0441 origin/${branch} @ ${res.sha.slice(0, 7)}`;
@@ -2836,49 +3329,56 @@ function worktreeSourceMessage(branch, res) {
2836
3329
  }
2837
3330
  async function setWorktreeIdentity(worktree, name) {
2838
3331
  const local = name.toLowerCase().replace(/\s+/g, "-");
2839
- await $`git -C ${worktree} config extensions.worktreeConfig true`;
2840
- await $`git -C ${worktree} config --worktree user.name ${`${name} (swarm drone)`}`;
2841
- await $`git -C ${worktree} config --worktree user.email ${`${local}@drones.swarm.local`}`;
3332
+ await $2`git -C ${worktree} config extensions.worktreeConfig true`;
3333
+ await $2`git -C ${worktree} config --worktree user.name ${`${name} (swarm drone)`}`;
3334
+ await $2`git -C ${worktree} config --worktree user.email ${`${local}@drones.swarm.local`}`;
2842
3335
  }
2843
3336
  async function removeWorktree(repo, path) {
2844
- const branch = (await $({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
2845
- await $({ reject: false })`git -C ${repo} worktree remove --force ${path}`;
2846
- if (branch) await $({ reject: false })`git -C ${repo} branch -D ${branch}`;
3337
+ const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
3338
+ await cleanupWorktreeProcesses(path);
3339
+ await $2({ reject: false })`git -C ${repo} worktree remove --force ${path}`;
3340
+ if (branch) await $2({ reject: false })`git -C ${repo} branch -D ${branch}`;
2847
3341
  }
2848
3342
  async function worktreeDirty(path) {
2849
- const status = (await $({ reject: false })`git -C ${path} status --porcelain`).stdout.trim();
3343
+ const status = (await $2({ reject: false })`git -C ${path} status --porcelain`).stdout.trim();
2850
3344
  if (status) return true;
2851
- const unpushed = (await $({ reject: false })`git -C ${path} rev-list --count HEAD --not --remotes`).stdout.trim();
3345
+ const unpushed = (await $2({ reject: false })`git -C ${path} rev-list --count HEAD --not --remotes`).stdout.trim();
2852
3346
  return unpushed !== "" && unpushed !== "0";
2853
3347
  }
2854
3348
  async function remoteMainRef(repo) {
2855
- const head = (await $({ reject: false })`git -C ${repo} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
3349
+ const head = (await $2({ reject: false })`git -C ${repo} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
2856
3350
  if (head) return head;
2857
3351
  for (const cand of ["origin/main", "origin/master"]) {
2858
- const ok = await $({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${cand}`;
3352
+ const ok = await $2({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${cand}`;
2859
3353
  if (ok.exitCode === 0) return cand;
2860
3354
  }
2861
3355
  throw new Error(`\u043D\u0435 \u043D\u0430\u0448\u0451\u043B \u0434\u0435\u0444\u043E\u043B\u0442-\u0432\u0435\u0442\u043A\u0443 origin \u0432 ${repo} (\u043D\u0435\u0442 origin/HEAD, origin/main, origin/master)`);
2862
3356
  }
3357
+ var GITHUB_REMOTE_RE = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/i;
3358
+ async function remoteRepoFullName(repo) {
3359
+ const url = (await $2({ reject: false })`git -C ${repo} remote get-url origin`).stdout.trim();
3360
+ const m = url.match(GITHUB_REMOTE_RE);
3361
+ return m ? m[1] : null;
3362
+ }
2863
3363
  async function prepareDetachedWorktree(repo, branch, worktree) {
2864
- await $`git -C ${repo} fetch --quiet origin`;
2865
- await $`git -C ${repo} worktree add --detach --force ${worktree} ${`origin/${branch}`}`;
3364
+ await $2`git -C ${repo} fetch --quiet origin`;
3365
+ await $2`git -C ${repo} worktree add --detach --force ${worktree} ${`origin/${branch}`}`;
2866
3366
  }
2867
3367
  async function mergeRef(worktree, ref) {
2868
- const before = (await $`git -C ${worktree} rev-parse HEAD`).stdout.trim();
2869
- const merge = await $({ reject: false })`git -C ${worktree} merge --no-edit ${ref}`;
3368
+ const before = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
3369
+ const merge = await $2({ reject: false })`git -C ${worktree} merge --no-edit ${ref}`;
2870
3370
  if (merge.exitCode === 0) {
2871
- const after = (await $`git -C ${worktree} rev-parse HEAD`).stdout.trim();
3371
+ const after = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
2872
3372
  return { clean: true, conflicts: [], changed: after !== before };
2873
3373
  }
2874
- const u = (await $({ reject: false })`git -C ${worktree} diff --name-only --diff-filter=U`).stdout.trim();
3374
+ const u = (await $2({ reject: false })`git -C ${worktree} diff --name-only --diff-filter=U`).stdout.trim();
2875
3375
  return { clean: false, conflicts: u ? u.split("\n") : [], changed: false };
2876
3376
  }
2877
3377
  async function pushHeadTo(worktree, branch) {
2878
- await $`git -C ${worktree} push origin ${`HEAD:${branch}`}`;
3378
+ await $2`git -C ${worktree} push origin ${`HEAD:${branch}`}`;
2879
3379
  }
2880
3380
  async function worktreeLastCommitMs(worktree) {
2881
- const out = (await $({ reject: false })`git -C ${worktree} log -1 --format=%ct HEAD`).stdout.trim();
3381
+ const out = (await $2({ reject: false })`git -C ${worktree} log -1 --format=%ct HEAD`).stdout.trim();
2882
3382
  const sec = Number(out);
2883
3383
  return Number.isInteger(sec) && sec > 0 ? sec * 1e3 : null;
2884
3384
  }
@@ -2912,9 +3412,10 @@ async function spawnDrone(deps, run) {
2912
3412
  }
2913
3413
 
2914
3414
  // src/gh.ts
2915
- import { $ as $2 } from "execa";
2916
- var realRunner = async (args) => (await $2`gh ${args}`).stdout;
2917
- var realRepoRunner = async (args, cwd) => (await $2({ cwd })`gh ${args}`).stdout;
3415
+ init_src();
3416
+ import { $ as $3 } from "execa";
3417
+ var realRunner = async (args) => (await $3`gh ${args}`).stdout;
3418
+ var realRepoRunner = async (args, cwd) => (await $3({ cwd })`gh ${args}`).stdout;
2918
3419
  var GH_PR_RE = /^(?:https?:\/\/)?(?:www\.)?github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+(?:[/?#].*)?$/i;
2919
3420
  function isGithubPrUrl(url) {
2920
3421
  return GH_PR_RE.test(url.trim());
@@ -2960,10 +3461,53 @@ async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
2960
3461
  return [];
2961
3462
  }
2962
3463
  }
3464
+ async function mergedPrForBranch(branch, repoDir, run = realRepoRunner) {
3465
+ if (!branch || !repoDir) return null;
3466
+ try {
3467
+ const out = await run(["pr", "list", "--head", branch, "--state", "merged", "--json", "url", "--limit", "1"], repoDir);
3468
+ const arr = JSON.parse(out);
3469
+ const url = Array.isArray(arr) ? arr[0]?.url : void 0;
3470
+ return typeof url === "string" ? url : null;
3471
+ } catch {
3472
+ return null;
3473
+ }
3474
+ }
2963
3475
  async function closePR(prUrl, run = realRunner) {
2964
3476
  await run(["pr", "close", prUrl, "--comment", "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 (swarm)"]);
2965
3477
  }
2966
- var realChecksRunner = async (pr) => (await $2({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`).stdout;
3478
+ var realCommentsRunner = async (prUrl) => (await $3`gh pr view ${prUrl} --json comments`).stdout;
3479
+ var VERDICT_RE = /^\s*\**\s*(APPROVE|REQUEST\s+CHANGES)\b/i;
3480
+ function verdictLabel(verdict) {
3481
+ return verdict === "approve" ? "\u2705 APPROVE" : "\u274C REQUEST CHANGES";
3482
+ }
3483
+ async function latestVerdictComment(prUrl, sinceIso, run = realCommentsRunner) {
3484
+ if (!isGithubPrUrl(prUrl)) return null;
3485
+ let out;
3486
+ try {
3487
+ out = await run(prUrl);
3488
+ } catch {
3489
+ return null;
3490
+ }
3491
+ let j;
3492
+ try {
3493
+ j = JSON.parse(out);
3494
+ } catch {
3495
+ return null;
3496
+ }
3497
+ const comments = Array.isArray(j.comments) ? j.comments : [];
3498
+ const sinceMs = sqliteMs(sinceIso);
3499
+ for (let i = comments.length - 1; i >= 0; i--) {
3500
+ const c = comments[i];
3501
+ if (!c?.body || !c.createdAt) continue;
3502
+ if (Number.isFinite(sinceMs) && Date.parse(c.createdAt) < sinceMs) continue;
3503
+ const m = c.body.match(VERDICT_RE);
3504
+ if (!m) continue;
3505
+ const verdict = /^APPROVE/i.test(m[1]) ? "approve" : "changes";
3506
+ return { verdict, createdAt: c.createdAt };
3507
+ }
3508
+ return null;
3509
+ }
3510
+ var realChecksRunner = async (pr) => (await $3({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`).stdout;
2967
3511
  async function prCheckRows(pr, run) {
2968
3512
  try {
2969
3513
  const out = await run(pr);
@@ -3016,7 +3560,7 @@ async function prCiStatus(pr, run = realChecksRunner) {
3016
3560
  if (rows.some((r) => r.bucket === "pending")) return { bucket: "pending", failing: [], failingRunIds: [] };
3017
3561
  return { bucket: "pass", failing: [], failingRunIds: [] };
3018
3562
  }
3019
- var realRunLogRunner = async (runId) => (await $2({ reject: false, timeout: 3e4, maxBuffer: 10 * 1024 * 1024 })`gh run view ${runId} --log-failed`).stdout;
3563
+ var realRunLogRunner = async (runId) => (await $3({ reject: false, timeout: 3e4, maxBuffer: 10 * 1024 * 1024 })`gh run view ${runId} --log-failed`).stdout;
3020
3564
  var CI_SUMMARY_LOG_CHARS = 4e3;
3021
3565
  var CI_SUMMARY_MAX_RUNS = 2;
3022
3566
  async function ciFailureSummary(failing, failingRunIds, logRun = realRunLogRunner) {
@@ -3037,9 +3581,11 @@ ${logs.join("\n---\n")}` : header;
3037
3581
  }
3038
3582
 
3039
3583
  // src/droneServer.ts
3584
+ init_taskless();
3040
3585
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3041
3586
  import { z } from "zod";
3042
3587
  init_config();
3588
+ init_src();
3043
3589
  function freezeInstruction(slug) {
3044
3590
  return [
3045
3591
  "\u26D4 OVERMIND \u041D\u0415\u0414\u041E\u0421\u0422\u0423\u041F\u0415\u041D \u2014 \u0435\u0433\u043E \u043F\u0443\u043B\u044C\u0441 (heartbeat) \u0443\u0441\u0442\u0430\u0440\u0435\u043B. \u041D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0430\u0434 \u0442\u043E\u0431\u043E\u0439 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0442: \u043D\u0435\u043A\u043E\u043C\u0443",
@@ -3080,7 +3626,7 @@ function buildDroneServer(db, runId, opts = {}) {
3080
3626
  `overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (\u043F\u0443\u043B\u044C\u0441 \u0443\u0441\u0442\u0430\u0440\u0435\u043B > ${Math.round(heartbeatStaleMs / 1e3)}\u0441) \u2014 \u0434\u0440\u043E\u043D \u0437\u0430\u043C\u0435\u0440, \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 origin, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430`
3081
3627
  );
3082
3628
  }
3083
- const slug = run?.task_slug ?? "<slug>";
3629
+ const slug = opts.expectedTaskSlug ?? run?.task_slug ?? "<slug>";
3084
3630
  return { content: [{ type: "text", text: freezeInstruction(slug) }] };
3085
3631
  };
3086
3632
  server.tool(
@@ -3093,6 +3639,47 @@ function buildDroneServer(db, runId, opts = {}) {
3093
3639
  return { content: [{ type: "text", text: "ok" }] };
3094
3640
  }
3095
3641
  );
3642
+ server.tool(
3643
+ "read_task",
3644
+ "\u0421\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0442\u0432\u043E\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u041F\u0420\u042F\u041C\u041E \u0421\u0415\u0419\u0427\u0410\u0421: \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 + \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (\u0430\u0432\u0442\u043E\u0440+\u0432\u0440\u0435\u043C\u044F+\u0442\u0435\u043A\u0441\u0442) + \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0445 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432 (\u0441\u043F\u0435\u043A\u0438 \u043E\u0431\u044B\u0447\u043D\u043E \u043B\u0435\u0436\u0430\u0442 \u0438\u043C\u0435\u043D\u043D\u043E \u0442\u0430\u043C). \u0411\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432. \u0417\u043E\u0432\u0438 \u043F\u0435\u0440\u0435\u0434 done() \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0433 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0443\u0436\u0435 \u043F\u043E\u0441\u043B\u0435 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430, \u0430 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0443\u0432\u0438\u0434\u0438\u0442 (\u043E\u043D \u0441\u043E\u0431\u0440\u0430\u043D \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435).",
3645
+ {},
3646
+ async () => {
3647
+ if (!opts.taskless) {
3648
+ return {
3649
+ content: [{ type: "text", text: "Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0441\u043D\u0430\u043F\u0448\u043E\u0442\u0430 \u043D\u0435\u0442, \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u0443\u0436\u0435 \u0432 \u043F\u0440\u043E\u043C\u043F\u0442\u0435." }],
3650
+ isError: true
3651
+ };
3652
+ }
3653
+ const run = db.getRun(runId);
3654
+ const slug = opts.expectedTaskSlug ?? run?.task_slug;
3655
+ let mismatchNote;
3656
+ if (opts.expectedTaskSlug && !run) {
3657
+ mismatchNote = `\u26A0\uFE0F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0441\u0442\u043E\u0440\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D slug \u0438\u0437 \u0441\u043F\u0430\u0432\u043D\u0430.`;
3658
+ } else if (opts.expectedTaskSlug && run && run.task_slug !== opts.expectedTaskSlug) {
3659
+ mismatchNote = `\u26A0\uFE0F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0441\u0435\u0439\u0447\u0430\u0441 \u0447\u0438\u0441\u043B\u0438\u0442\u0441\u044F \u043A\u0430\u043A \xAB${run.task_slug}\xBB \u2014 \u044D\u0442\u043E \u041D\u0415 \u0442\u0432\u043E\u044F \u0437\u0430\u0434\u0430\u0447\u0430 (\u0441\u0442\u043E\u0440 \u0434\u0435\u043C\u043E\u043D\u0430 \u0440\u0430\u0441\u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D, TASWM-211). \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D \u0434\u043E\u0432\u0435\u0440\u0435\u043D\u043D\u044B\u0439 slug \u0438\u0437 \u0441\u043F\u0430\u0432\u043D\u0430.`;
3660
+ }
3661
+ if (!slug) return { content: [{ type: "text", text: "\u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D" }], isError: true };
3662
+ const task = await opts.taskless.getTask(slug);
3663
+ if (!task) {
3664
+ const notFoundText = `\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 ${slug} \u0438\u0437 Taskless (\u0441\u043C. \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430) \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u0443\u0436\u0435 \u0432 \u043F\u0440\u043E\u043C\u043F\u0442\u0435.`;
3665
+ return {
3666
+ content: [{ type: "text", text: mismatchNote ? `${mismatchNote}
3667
+
3668
+ ${notFoundText}` : notFoundText }],
3669
+ isError: true
3670
+ };
3671
+ }
3672
+ const context = await fetchTaskContext(opts.taskless, task);
3673
+ const lines = [
3674
+ `**${task.title}** \xB7 ${task.slug}${task.url ? ` (${task.url})` : ""}`,
3675
+ "",
3676
+ task.description || "(\u043F\u0443\u0441\u0442\u043E)"
3677
+ ];
3678
+ if (context) lines.push("", context);
3679
+ if (mismatchNote) lines.unshift(mismatchNote, "");
3680
+ return { content: [{ type: "text", text: lines.join("\n") }] };
3681
+ }
3682
+ );
3096
3683
  server.tool(
3097
3684
  "ask",
3098
3685
  "\u0417\u0430\u0434\u0430\u0442\u044C \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u0438 \u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F \u043E\u0442\u0432\u0435\u0442\u0430. \u0415\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F \u2014 \u0434\u0430\u0439 options (2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430, \u0441\u0442\u0430\u043D\u0443\u0442 \u043A\u043D\u043E\u043F\u043A\u0430\u043C\u0438); \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0432\u044B\u0431\u0435\u0440\u0435\u0442 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 \u0438\u043B\u0438 \u043D\u0430\u043F\u0438\u0448\u0435\u0442 \u0441\u0432\u043E\u0451. \u041E\u043F\u0443\u0441\u0442\u0438 options \u0434\u043B\u044F \u043E\u0442\u043A\u0440\u044B\u0442\u043E\u0433\u043E \u0432\u043E\u043F\u0440\u043E\u0441\u0430. \u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043E\u0442\u0432\u0435\u0442 \u0441\u0442\u0440\u043E\u043A\u043E\u0439. \u0417\u043E\u0432\u0438, \u043A\u043E\u0433\u0434\u0430 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u2014 \u043D\u0435 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439.",
@@ -3136,7 +3723,7 @@ function buildDroneServer(db, runId, opts = {}) {
3136
3723
  {
3137
3724
  pr_url: z.string().describe("\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0439 GitHub-PR; \u0434\u043B\u044F \u0437\u0430\u0434\u0430\u0447 \u0431\u0435\u0437 PR \u2014 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0440\u043E\u0441\u0442\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044F)"),
3138
3725
  verdict: z.enum(["approve", "changes"]).optional().describe("\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0434\u0440\u043E\u043D\u0430-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u0438\u0442\u043E\u0433 \u043F\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u043E\u043C\u0443 PR \u2014 approve (APPROVE) \u0438\u043B\u0438 changes (REQUEST CHANGES). \u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u0434\u043B\u044F \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430."),
3139
- summary: z.string().optional().describe("\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0434\u0440\u043E\u043D\u0430-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B/\u043D\u0430\u0448\u0451\u043B \u2014 \u0443\u0439\u0434\u0451\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 REV-\u0437\u0430\u0434\u0430\u0447\u0443 \u0438 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A.")
3726
+ summary: z.string().optional().describe("\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0434\u0440\u043E\u043D\u0430-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B/\u043D\u0430\u0448\u0451\u043B \u2014 \u0443\u0439\u0434\u0451\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A.")
3140
3727
  },
3141
3728
  async ({ pr_url, verdict, summary }) => {
3142
3729
  const run = db.getRun(runId);
@@ -3147,6 +3734,9 @@ function buildDroneServer(db, runId, opts = {}) {
3147
3734
  };
3148
3735
  }
3149
3736
  if (run?.reviews_run_id != null) {
3737
+ if (isTerminalStatus(run.status)) {
3738
+ return { content: [{ type: "text", text: "\u043F\u0440\u043E\u0433\u043E\u043D \u0443\u0436\u0435 \u0437\u0430\u043A\u0440\u044B\u0442 (PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D" }] };
3739
+ }
3150
3740
  if (verdict !== "approve" && verdict !== "changes") {
3151
3741
  return {
3152
3742
  content: [{ type: "text", text: '\u0422\u044B \u0434\u0440\u043E\u043D-\u0440\u0435\u0432\u044C\u044E\u0435\u0440 (\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E, TASWM-148) \u2014 done() \u0442\u0440\u0435\u0431\u0443\u0435\u0442 verdict: "approve" | "changes" (\u0438\u0442\u043E\u0433 \u043F\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u043E\u043C\u0443 PR).' }],
@@ -3154,21 +3744,15 @@ function buildDroneServer(db, runId, opts = {}) {
3154
3744
  };
3155
3745
  }
3156
3746
  const reviewed = db.getRun(run.reviews_run_id);
3157
- const verdictText = verdict === "approve" ? "\u2705 APPROVE" : "\u274C REQUEST CHANGES";
3747
+ const verdictText = verdictLabel(verdict);
3158
3748
  db.updateRun(run.reviews_run_id, { review_verdict: verdict });
3159
3749
  db.addEvent(run.reviews_run_id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E \u043E\u0442 ${run.name}: ${verdictText}${summary ? ` \u2014 ${summary}` : ""}`);
3160
- if (opts.taskless) {
3750
+ if (opts.taskless && reviewed) {
3161
3751
  const note = `${verdictText} \u043E\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 ${run.name} (\u043F\u0440\u043E\u0433\u043E\u043D #${runId})${summary ? `
3162
3752
 
3163
3753
  ${summary}` : ""}`;
3164
- await Promise.all([
3165
- opts.taskless.addComment(run.task_slug, note).catch(() => {
3166
- }),
3167
- // REV-задача
3168
- reviewed ? opts.taskless.addComment(reviewed.task_slug, note).catch(() => {
3169
- }) : Promise.resolve()
3170
- // задача-источник
3171
- ]);
3754
+ await opts.taskless.addComment(reviewed.task_slug, note).catch(() => {
3755
+ });
3172
3756
  }
3173
3757
  const trustedPrUrl = reviewed?.pr_url ?? pr_url;
3174
3758
  db.updateRun(runId, { status: "review", pr_url: trustedPrUrl });
@@ -3207,6 +3791,7 @@ ${summary}` : ""}`;
3207
3791
  if (!taskless) return errText("Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u0442\u044C.");
3208
3792
  const ep = await epic();
3209
3793
  if (!ep) return errText(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u044D\u043F\u0438\u043A ${heroRun.task_slug} \u0438\u0437 Taskless \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430.`);
3794
+ if (!ep.projectId) return errText(`\u044D\u043F\u0438\u043A ${heroRun.task_slug} \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F (\u043D\u0443\u0436\u0435\u043D projectId).`);
3210
3795
  const statusId = await taskless.backlogStatusId();
3211
3796
  if (!statusId) return errText("\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0441\u0442\u0430\u0442\u0443\u0441 \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u0438 backlog \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430 \u2014 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u044E (\u0440\u0438\u0441\u043A \u043F\u043E\u043F\u0430\u0441\u0442\u044C \u0441\u0440\u0430\u0437\u0443 \u0432 \xAB\u0421\u0434\u0435\u043B\u0430\u0442\u044C\xBB).");
3212
3797
  const footer = `
@@ -3309,9 +3894,9 @@ async function runDroneServer(runId) {
3309
3894
  const { DB_PATH: DB_PATH2 } = await Promise.resolve().then(() => (init_config(), config_exports));
3310
3895
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
3311
3896
  const db = new SwarmDb2(DB_PATH2);
3312
- const run = db.getRun(runId);
3313
- const taskless = run?.kind === "hero" || run?.reviews_run_id != null ? await (await Promise.resolve().then(() => (init_taskless(), taskless_exports))).connectTaskless() : null;
3314
- const server = buildDroneServer(db, runId, { taskless });
3897
+ const taskless = await (await Promise.resolve().then(() => (init_taskless(), taskless_exports))).connectTaskless();
3898
+ const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
3899
+ const server = buildDroneServer(db, runId, { taskless, ...expectedTaskSlug ? { expectedTaskSlug } : {} });
3315
3900
  await server.connect(new StdioServerTransport());
3316
3901
  }
3317
3902
 
@@ -3337,6 +3922,24 @@ function makeMaxDronesReader(opts) {
3337
3922
  };
3338
3923
  }
3339
3924
 
3925
+ // src/verifyMode.ts
3926
+ function makeVerifyModeReader(opts) {
3927
+ const ttl = opts.ttlMs ?? 6e4;
3928
+ const now = opts.now ?? Date.now;
3929
+ const cache = /* @__PURE__ */ new Map();
3930
+ return async (repo) => {
3931
+ const t = now();
3932
+ const hit = cache.get(repo);
3933
+ if (hit && t - hit.at < ttl) return hit.mode;
3934
+ const live = await opts.fetch(repo).catch(() => null);
3935
+ if (live === "local" || live === "ci") {
3936
+ cache.set(repo, { mode: live, at: t });
3937
+ return live;
3938
+ }
3939
+ return opts.fallback;
3940
+ };
3941
+ }
3942
+
3340
3943
  // src/web.ts
3341
3944
  init_src();
3342
3945
  import { randomBytes as randomBytes2 } from "crypto";
@@ -3346,7 +3949,7 @@ import { serve } from "@hono/node-server";
3346
3949
  // src/diff.ts
3347
3950
  import { statSync as statSync2 } from "fs";
3348
3951
  import { join as join6 } from "path";
3349
- import { $ as $3 } from "execa";
3952
+ import { $ as $4 } from "execa";
3350
3953
  var MAX_FILES = 200;
3351
3954
  var MAX_LINES_PER_FILE = 1500;
3352
3955
  var MAX_UNTRACKED = 60;
@@ -3466,12 +4069,12 @@ function truncateFile(f) {
3466
4069
  }
3467
4070
  async function resolveBase(wt) {
3468
4071
  const tryRef = async (ref) => {
3469
- const sha = (await $3({ reject: false })`git -C ${wt} rev-parse --verify --quiet ${ref + "^{commit}"}`).stdout.trim();
4072
+ const sha = (await $4({ reject: false })`git -C ${wt} rev-parse --verify --quiet ${ref + "^{commit}"}`).stdout.trim();
3470
4073
  if (!sha) return null;
3471
- const mb = (await $3({ reject: false })`git -C ${wt} merge-base HEAD ${ref}`).stdout.trim();
4074
+ const mb = (await $4({ reject: false })`git -C ${wt} merge-base HEAD ${ref}`).stdout.trim();
3472
4075
  return mb || null;
3473
4076
  };
3474
- const originHead = (await $3({ reject: false })`git -C ${wt} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
4077
+ const originHead = (await $4({ reject: false })`git -C ${wt} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
3475
4078
  const candidates = [originHead, "origin/main", "origin/master", "main", "master"].filter(Boolean);
3476
4079
  for (const c of candidates) {
3477
4080
  const mb = await tryRef(c);
@@ -3490,16 +4093,16 @@ async function collectRunDiff(worktree, opts = {}) {
3490
4093
  const stub = (reason) => ({ ok: false, reason, pr: opts.pr ?? null, files: [], added: 0, removed: 0 });
3491
4094
  const gone = opts.status === "queued" ? "\u0434\u0440\u043E\u043D \u0435\u0449\u0451 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u043A\u043E\u0434\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442" : "worktree \u0443\u0431\u0440\u0430\u043D \u2014 \u043A\u043E\u0434 \u0441\u043C\u043E\u0442\u0440\u0438 \u0432 PR";
3492
4095
  if (!worktree) return stub(gone);
3493
- const isWt = (await $3({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
4096
+ const isWt = (await $4({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
3494
4097
  if (isWt !== "true") return stub(gone);
3495
4098
  const base2 = await resolveBase(worktree);
3496
4099
  const from = base2 ?? "HEAD";
3497
- const trackedTxt = (await $3({ reject: false })`git -C ${worktree} diff --no-color --find-renames ${from} --`).stdout;
4100
+ const trackedTxt = (await $4({ reject: false })`git -C ${worktree} diff --no-color --find-renames ${from} --`).stdout;
3498
4101
  let files = parseUnifiedDiff(trackedTxt);
3499
- const untrackedRaw = (await $3({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
4102
+ const untrackedRaw = (await $4({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
3500
4103
  const untracked = untrackedRaw.split("\0").filter(Boolean).slice(0, MAX_UNTRACKED);
3501
4104
  for (const rel of untracked) {
3502
- const t = (await $3({ reject: false })`git -C ${worktree} diff --no-color --no-index -- /dev/null ${rel}`).stdout;
4105
+ const t = (await $4({ reject: false })`git -C ${worktree} diff --no-color --no-index -- /dev/null ${rel}`).stdout;
3503
4106
  for (const pf of parseUnifiedDiff(t)) {
3504
4107
  pf.status = "added";
3505
4108
  if (!pf.path || pf.path === "/dev/null") pf.path = rel;
@@ -3692,7 +4295,8 @@ function timelineState(db, now = Date.now(), overmindLog = []) {
3692
4295
  cost: r.cost_usd || 0,
3693
4296
  role: r.kind,
3694
4297
  heroRunId: r.hero_run_id,
3695
- reviewVerdict: r.review_verdict
4298
+ reviewVerdict: r.review_verdict,
4299
+ model: r.model
3696
4300
  });
3697
4301
  }
3698
4302
  const liveLanes = lanes.filter((l) => l.live);
@@ -4190,7 +4794,7 @@ function cardStep(d){
4190
4794
  if(d.status==='stalled') return d.reason || '\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441/\u043F\u043E\u0433\u0438\u0431';
4191
4795
  if(d.status==='stopped') return d.stopReason || d.reason || '\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D';
4192
4796
  if(d.status==='storm_dead') return '\u0434\u0440\u043E\u043D \u0432 \u043A\u043E\u043A\u043E\u043D\u0435 \u2014 \u043F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 (Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442), worktree \u0446\u0435\u043B, \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043E\u0442\u0431\u043E\u044F';
4193
- if(d.status==='paused') return '\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430';
4797
+ if(d.status==='paused') return d.stopReason || '\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430'; // TASWM-151: \u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u0437\u0430\u0434\u0430\u0451\u0442 \u0441\u0432\u043E\u0439 stopReason
4194
4798
  if(d.status==='review') { var n=prNum(d.pr); return 'PR'+(n?(' #'+n):'')+' \u043D\u0430 \u0440\u0435\u0432\u044C\u044E'; }
4195
4799
  if(d.status==='done') return '\u0441\u043C\u0435\u0440\u0436\u0435\u043D';
4196
4800
  if(d.status==='queued') return (d.waiting&&d.waiting.length) ? '\u0436\u0434\u0451\u0442: '+d.waiting.join(', ') : '\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0436\u0434\u0451\u0442 \u0441\u043B\u043E\u0442\u0430';
@@ -5014,21 +5618,15 @@ var RESPAWN_YOUNG_MS = Number(process.env.SWARM_RESPAWN_YOUNG_MS) || 60 * 1e3;
5014
5618
  var CONFLICT_RESOLVE_COOLDOWN_MS = Number(process.env.SWARM_CONFLICT_COOLDOWN_MS) || 3 * 60 * 1e3;
5015
5619
  var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
5016
5620
  var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
5621
+ var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
5622
+ var REVIEW_ESCALATION_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F";
5017
5623
  var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
5018
5624
  var CI_WAKE_COOLDOWN_MS = Number(process.env.SWARM_CI_WAKE_COOLDOWN_MS) || 3 * 60 * 1e3;
5019
5625
  var CI_CHECK_COOLDOWN_MS = Number(process.env.SWARM_CI_CHECK_COOLDOWN_MS) || 60 * 1e3;
5020
5626
  var CI_WAKE_ESCALATION_MARK = "CI-\u0430\u0432\u0442\u043E\u043F\u043E\u0447\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043F\u0440\u0430\u0432\u0438\u043B\u0430\u0441\u044C";
5021
- function pidAlive(pid) {
5022
- try {
5023
- process.kill(pid, 0);
5024
- return true;
5025
- } catch (e) {
5026
- return e.code === "EPERM";
5027
- }
5028
- }
5029
5627
  function psLines(extraArgs) {
5030
5628
  try {
5031
- return execFileSync2("ps", ["-A", "-ww", ...extraArgs], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }).split(
5629
+ return execFileSync3("ps", ["-A", "-ww", ...extraArgs], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }).split(
5032
5630
  "\n"
5033
5631
  );
5034
5632
  } catch (e) {
@@ -5114,6 +5712,91 @@ function countActiveDrones(db) {
5114
5712
  function countActiveReviewers(db) {
5115
5713
  return db.listRuns().filter((r) => r.kind === "reviewer" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
5116
5714
  }
5715
+ async function closeReviewersOnMerge(deps, reviewRunId) {
5716
+ const stale = deps.db.activeReviewersFor(reviewRunId);
5717
+ for (const rv of stale) {
5718
+ if (rv.pid != null && deps.kill && deps.isAlive(rv.pid)) {
5719
+ try {
5720
+ deps.kill(rv.pid);
5721
+ } catch {
5722
+ }
5723
+ }
5724
+ if (rv.worktree_path) await deps.removeWorktree(rv.repo, rv.worktree_path).catch(() => {
5725
+ });
5726
+ deps.db.updateRun(rv.id, { status: "done" });
5727
+ deps.db.addEvent(rv.id, "done", "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E \u2014 \u0440\u0435\u0432\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E");
5728
+ }
5729
+ }
5730
+ async function finalizeMergedReview(deps, run) {
5731
+ await deps.finishTask(run.task_slug);
5732
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
5733
+ });
5734
+ deps.db.addEvent(run.id, "merged", run.pr_url ?? "");
5735
+ deps.db.updateRun(run.id, { status: "done" });
5736
+ deps.db.addEvent(run.id, "done");
5737
+ await closeReviewersOnMerge(deps, run.id);
5738
+ }
5739
+ function doneMergedPrForSlug(db, slug) {
5740
+ const done = db.listRuns("done").find((r) => r.task_slug === slug && r.pr_url && r.kind !== "hero" && r.resolves_run_id == null && r.reviews_run_id == null);
5741
+ return done?.pr_url ?? null;
5742
+ }
5743
+ async function closeDuplicateTaskRun(deps, run, mergedUrl) {
5744
+ if (run.pid != null && deps.kill && deps.isAlive(run.pid)) {
5745
+ try {
5746
+ deps.kill(run.pid);
5747
+ } catch {
5748
+ }
5749
+ }
5750
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
5751
+ });
5752
+ const reason = `PR \u0437\u0430\u0434\u0430\u0447\u0438 \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D (${mergedUrl}) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D-\u0434\u0443\u0431\u043B\u044C \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u0441\u043F\u0430\u0432\u043D\u0430/\u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0438 (TASWM-206)`;
5753
+ deps.db.updateRun(run.id, { status: "superseded", stop_reason: reason });
5754
+ deps.db.addEvent(run.id, "superseded", reason);
5755
+ }
5756
+ async function closeMergedTaskRuns(deps) {
5757
+ const bySlug = /* @__PURE__ */ new Map();
5758
+ for (const run of deps.db.listRuns()) {
5759
+ if (run.kind === "hero" || run.resolves_run_id != null || run.reviews_run_id != null) continue;
5760
+ let group = bySlug.get(run.task_slug);
5761
+ if (!group) bySlug.set(run.task_slug, group = []);
5762
+ group.push(run);
5763
+ }
5764
+ for (const [slug, group] of bySlug) {
5765
+ const nonTerminal = group.filter((r) => !isTerminalStatus(r.status));
5766
+ if (nonTerminal.length === 0) continue;
5767
+ if (nonTerminal.length === 1 && nonTerminal[0].status === "review") continue;
5768
+ try {
5769
+ const withPr = [...group].sort((a, b) => b.id - a.id).find((r) => r.pr_url);
5770
+ let mergedUrl = null;
5771
+ if (withPr) {
5772
+ if (withPr.status === "done") {
5773
+ mergedUrl = withPr.pr_url;
5774
+ } else {
5775
+ const st = await deps.prStatus(withPr.pr_url);
5776
+ if (st.merged) mergedUrl = withPr.pr_url;
5777
+ }
5778
+ }
5779
+ if (!mergedUrl && deps.mergedPrForBranch) {
5780
+ mergedUrl = await deps.mergedPrForBranch(slug, group[0].repo || "").catch(() => null);
5781
+ }
5782
+ if (!mergedUrl) continue;
5783
+ const canonical = withPr && withPr.pr_url === mergedUrl && !isTerminalStatus(withPr.status) ? withPr : null;
5784
+ for (const run of nonTerminal) {
5785
+ if (canonical && run.id === canonical.id) {
5786
+ await finalizeMergedReview(deps, run);
5787
+ narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430 (\u0434\u0443\u0431\u043B\u044C-\u0440\u0430\u043D\u044B \u0440\u043E\u044F, TASWM-206).`);
5788
+ } else {
5789
+ await closeDuplicateTaskRun(deps, run, mergedUrl);
5790
+ narrate(deps, `${droneLabel(run)} \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 ${slug} \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u0430 \u0434\u0440\u0443\u0433\u0438\u043C \u0440\u0430\u043D\u043E\u043C, \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u0434\u0443\u0431\u043B\u044C.`);
5791
+ }
5792
+ }
5793
+ if (!canonical) await deps.finishTask(slug).catch(() => {
5794
+ });
5795
+ } catch (e) {
5796
+ console.error(`[overmind] \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u0434\u0443\u0431\u043B\u0435\u0439-\u0440\u0430\u043D\u043E\u0432 \u0437\u0430\u0434\u0430\u0447\u0438 ${slug} (TASWM-206) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
5797
+ }
5798
+ }
5799
+ }
5117
5800
  var CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C (\u0441\u0435\u0442\u044C/\u043F\u0440\u043E\u0446\u0435\u0441\u0441) \u2014 \u0442\u044B headless-\u0434\u0440\u043E\u043D, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u0423 \u0416\u0415 \u0437\u0430\u0434\u0430\u0447\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 `git status`/`git log` \u0438 \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0441\u0432\u043E\u0438\u0445 \u0448\u0430\u0433\u043E\u0432: \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E \u2014 \u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438 \u0441 \u043C\u0435\u0441\u0442\u0430 \u043E\u0431\u0440\u044B\u0432\u0430. \u0414\u043E\u0432\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u043E \u043A\u043E\u043D\u0446\u0430, \u041A\u041E\u041C\u041C\u0418\u0422\u042C \u0418 \u041F\u0423\u0428\u042C \u043F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0448\u0430\u0433\u0430, \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url). \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 MCP-\u0442\u0443\u043B ask.";
5118
5801
  function buildCiWakeNudge(summary) {
5119
5802
  return [
@@ -5126,7 +5809,7 @@ function buildCiWakeNudge(summary) {
5126
5809
  }
5127
5810
  var HERO_CONTINUE_NUDGE = "\u0422\u0435\u0431\u044F \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B\u0438, \u0413\u0435\u0440\u043E\u0439: \u0432 \u043E\u0442\u0440\u044F\u0434\u0435 \u0447\u0442\u043E-\u0442\u043E \u043F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u043E (\u0434\u0440\u043E\u043D \u0437\u0430\u0434\u0430\u043B \u0432\u043E\u043F\u0440\u043E\u0441, \u0441\u0434\u0430\u043B PR \u0438\u043B\u0438 \u0443\u043C\u0435\u0440) \u2014 \u043B\u0438\u0431\u043E \u0442\u0435\u0431\u044F \u043F\u043E\u0437\u0432\u0430\u043B \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440. \u0422\u044B headless, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u041E\u0422 \u0416\u0415 \u044D\u043F\u0438\u043A: \u0443\u0441\u0442\u0430\u0432 \u0438 \u0442\u0435\u043B\u043E \u044D\u043F\u0438\u043A\u0430 \u0443\u0436\u0435 \u0432 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0435 \u043F\u0440\u043E\u0448\u043B\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438. \u0413\u043B\u044F\u043D\u044C \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043E\u0442\u0440\u044F\u0434\u0430 (squad_status) \u0438 \u0440\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C, \u0447\u0442\u043E \u0438\u0437\u043C\u0435\u043D\u0438\u043B\u043E\u0441\u044C: \u0442\u0435\u0445\u043D\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0434\u0440\u043E\u043D\u043E\u0432 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0439 answer_drone; \u043F\u0440\u043E\u0434\u0443\u043A\u0442\u043E\u0432\u043E\u0435/\u0434\u0438\u0437\u0430\u0439\u043D/scope \u2014 \u044D\u0441\u043A\u0430\u043B\u0438\u0440\u0443\u0439 \u0447\u0435\u0440\u0435\u0437 ask. \u041A\u043E\u0434 \u043D\u0435 \u043F\u0438\u0448\u0438, PR \u043D\u0435 \u043C\u0435\u0440\u0436\u0438, done \u041D\u0415 \u0437\u043E\u0432\u0438. \u0420\u0430\u0437\u043E\u0431\u0440\u0430\u043B\u0441\u044F \u0441 \u0440\u044B\u0432\u043A\u043E\u043C \u2014 \u0441\u043D\u043E\u0432\u0430 \u0437\u043E\u0432\u0438 rest(summary) \u0438 \u0437\u0430\u043C\u0440\u0438. \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 ask.";
5128
5811
  var HEADLESS_BLOCK = "\u26D4 HEADLESS-\u0420\u0415\u0416\u0418\u041C \u2014 \u0416\u0401\u0421\u0422\u041A\u041E. \u0418\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u0447\u0430\u0442\u0430 \u041D\u0415\u0422: \u0442\u0432\u043E\u0439 stdout \u043D\u0438\u043A\u0442\u043E \u043D\u0435 \u0447\u0438\u0442\u0430\u0435\u0442 \u0438 \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0435 \u043C\u043E\u0436\u0435\u0442. \u041D\u0418\u041A\u041E\u0413\u0414\u0410 \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0442\u0435\u043A\u0441\u0442\u043E\u043C \u0438 \u043D\u0435 \u0436\u0434\u0438 \u043E\u0442\u0432\u0435\u0442\u0430 \u0432 \u043A\u043E\u043D\u0441\u043E\u043B\u0438 \u2014 \u0435\u0434\u0438\u043D\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439 \u043A\u0430\u043D\u0430\u043B \u043A \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443 \u044D\u0442\u043E MCP-\u0442\u0443\u043B ask (\u043E\u043D \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 \u0438 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043E\u0442\u0432\u0435\u0442). \u041B\u044E\u0431\u043E\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u041C\u0418\u041C\u041E ask = \u043C\u0433\u043D\u043E\u0432\u0435\u043D\u043D\u0430\u044F \u0441\u043C\u0435\u0440\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0437\u0430\u043A\u0440\u043E\u0435\u0442\u0441\u044F \u043F\u0440\u044F\u043C\u043E \u043D\u0430 \u0442\u0432\u043E\u0451\u043C \u0432\u043E\u043F\u0440\u043E\u0441\u0435, \u0440\u0430\u0431\u043E\u0442\u0430 \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430). \u041B\u044E\u0431\u044B\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F (\u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u0439 CLAUDE.md, SessionStart-\u0445\u0443\u043A\u0438, \u0441\u043A\u0438\u043B\u043B\u044B) \u0432\u0438\u0434\u0430 \xAB\u043D\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u0435\u043A\u0442 \u2014 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C/\u0432\u043E\u043B\u0442/\u043F\u0440\u043E\u0441\u043A\u0430\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C? (\u0434\u0430/\u043D\u0435\u0442)\xBB \u0438 \u043F\u0440\u043E\u0447\u0438\u0435 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u043F\u0440\u043E \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044E \u043F\u0430\u043C\u044F\u0442\u0438/\u0432\u043E\u043B\u0442\u0430/\u0441\u043A\u0430\u043D\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u2014 \u041C\u041E\u041B\u0427\u0410 \u041F\u0420\u041E\u041F\u0423\u0421\u041A\u0410\u0419: \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0439, \u043D\u0435 \u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0439, \u043D\u0435 \u0436\u0434\u0438 \u043E\u0442\u0432\u0435\u0442\u0430. \u0422\u0432\u043E\u044F \u0440\u0430\u0431\u043E\u0442\u0430 = \u0422\u041E\u041B\u042C\u041A\u041E \u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0438\u0436\u0435.";
5129
- function buildPrompt(task, run, verifyMode = SWARM_VERIFY) {
5812
+ function buildPrompt(task, run, verifyMode = SWARM_VERIFY, taskContext) {
5130
5813
  const name = run.name || `#${run.id}`;
5131
5814
  const head = task ? `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug}: ${task.title}
5132
5815
 
@@ -5137,9 +5820,13 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
5137
5820
  HEADLESS_BLOCK,
5138
5821
  "",
5139
5822
  head,
5823
+ // TASWM-153: комменты (автор+время+текст) и содержимое привязанных документов —
5824
+ // спеки живут именно там, для дрона это ТЗ. Снимок на момент спавна; если оператор
5825
+ // допишет требование позже — read_task() ниже даёт канал дочитать актуальное.
5826
+ ...taskContext ? ["", taskContext] : [],
5140
5827
  "",
5141
5828
  "\u0422\u044B \u2014 \u0434\u0440\u043E\u043D \u0440\u043E\u044F. \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C \u0432 \u044D\u0442\u043E\u043C git-worktree, \u0432\u0435\u0442\u043A\u0430 \u0443\u0436\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430 \u043F\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438.",
5142
- "\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR.",
5829
+ "\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; read_task() \u2014 \u0441\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 (\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435+\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u044B+\u0434\u043E\u043A\u0438) \u0431\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432, \u0435\u0441\u043B\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043C\u043E\u0433\u043B\u0438 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435 \u0441\u0442\u0430\u0440\u0442\u0430; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR.",
5143
5830
  "\u041F\u0440\u0430\u0432\u0438\u043B\u0430: \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 uikit/\u0433\u0430\u0439\u0434\u043B\u0430\u0439\u043D \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (\u043D\u0435\u0442 \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0430 \u2014 \u0437\u0430\u0432\u0435\u0434\u0438 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443, \u043D\u0435 \u0445\u0430\u0440\u0434\u043A\u043E\u0434\u044C \u043E\u0434\u043D\u043E\u0440\u0430\u0437\u043E\u0432\u043E\u0435); \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0441\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0438; \u043F\u043E\u0441\u043B\u0435 UI-\u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u0441\u043A\u0440\u0438\u043D\u044B mobile+desktop; \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0447\u0435\u0440\u0435\u0437 gh \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url).",
5144
5831
  `\u0412 \u041E\u041F\u0418\u0421\u0410\u041D\u0418\u0415 PR \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u0434\u043E\u0431\u0430\u0432\u044C \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443${task?.url ? ` \u2014 ${task.url}` : " \u0432 Taskless"} (\u043F\u0435\u0440\u0432\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439), \u0447\u0442\u043E\u0431\u044B \u043E\u0442 PR \u0431\u044B\u043B \u043E\u0434\u0438\u043D \u043A\u043B\u0438\u043A \u0434\u043E \u0437\u0430\u0434\u0430\u0447\u0438, \u0431\u0435\u0437 \u043F\u043E\u0438\u0441\u043A\u0430.`,
5145
5832
  `\u041F\u043E\u043C\u0435\u0442\u044C \u0440\u0430\u0431\u043E\u0442\u0443 \u0441\u0432\u043E\u0438\u043C \u0438\u043C\u0435\u043D\u0435\u043C ${name}: \u0417\u0410\u0413\u041E\u041B\u041E\u0412\u041E\u041A PR \u043E\u043A\u0430\u043D\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u0445\u0432\u043E\u0441\u0442\u043E\u043C [${name}] (\u043D\u0430\u043F\u0440. \xABfeat(api): reserved slug swarm (${run.task_slug}) [${name}]\xBB); \u0432 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0438 PR \u0441\u0440\u0430\u0437\u0443 \u041F\u041E\u0421\u041B\u0415 \u0441\u0441\u044B\u043B\u043A\u0438 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443 \u2014 \u0441\u0442\u0440\u043E\u043A\u0430 \xAB\u{1F41D} \u0414\u0440\u043E\u043D ${name} \xB7 \u043F\u0440\u043E\u0433\u043E\u043D #${run.id}\xBB.`,
@@ -5151,6 +5838,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
5151
5838
  '\u2022 \u041F\u0435\u0440\u0435\u0434 \u0434\u043E\u043B\u0433\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 (\u0442\u0435\u0441\u0442\u044B, \u0441\u0431\u043E\u0440\u043A\u0430 \u2014 \u043C\u0438\u043D\u0443\u0442\u044B \u0431\u0435\u0437 \u0432\u044B\u0432\u043E\u0434\u0430) \u043A\u0438\u043D\u044C report_step("\u0433\u043E\u043D\u044E \u0442\u0435\u0441\u0442\u044B\u2026") \u2014 \u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E (\u0440\u043E\u0439 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E \u043E\u0442\u0441\u043B\u0435\u0436\u0438\u0432\u0430\u0435\u0442 \u0436\u0438\u0432\u043E\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043A\u043E\u043C\u0430\u043D\u0434\u044B), \u043D\u043E \u043F\u043E\u043B\u0435\u0437\u043D\u0435\u0435 \u0434\u043B\u044F \u0432\u0438\u0434\u0438\u043C\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430 \u0432 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u0435 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430.',
5152
5839
  "",
5153
5840
  "\u041F\u0420\u041E\u0422\u041E\u041A\u041E\u041B \u0421\u0414\u0410\u0427\u0418 \u2014 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0448\u0430\u0433\u0438 \u041F\u0415\u0420\u0415\u0414 done(pr_url), \u044D\u0442\u043E \u0448\u0430\u0433\u0438, \u0430 \u043D\u0435 \u043F\u043E\u0436\u0435\u043B\u0430\u043D\u0438\u044F:",
5841
+ "0. \u041F\u0435\u0440\u0435\u0437\u0432\u043E\u043D\u0438 read_task() \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0433 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0438\u043B\u0438 \u0432 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u0443\u0436\u0435 \u041F\u041E\u0421\u041B\u0415 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430 (\u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0432\u0438\u0434\u0438\u0442, \u043E\u043D \u0441\u043D\u044F\u0442 \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435). \u0421\u0432\u0435\u0440\u044C\u0441\u044F \u0441 \u0442\u0435\u043C, \u0447\u0442\u043E \u0432\u0435\u0440\u043D\u0443\u043B read_task, \u043F\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043C \u0441\u0434\u0430\u0432\u0430\u0442\u044C.",
5154
5842
  verifyMode === "ci" ? "1. CI-\u0440\u0435\u0436\u0438\u043C (SWARM_VERIFY=ci): \u0433\u043E\u043D\u044F\u0439 \u0442\u043E\u043B\u044C\u043A\u043E \u0431\u044B\u0441\u0442\u0440\u044B\u0439 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 `pnpm typecheck` \u043F\u0430\u043A\u0435\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0442\u044B \u0442\u0440\u043E\u0433\u0430\u043B (\u0434\u0451\u0448\u0435\u0432\u043E, \u043B\u043E\u0432\u0438\u0442 \u0442\u0443\u043F\u044B\u0435 \u043E\u0448\u0438\u0431\u043A\u0438 \u0442\u0438\u043F\u043E\u0432 \u0434\u043E \u0442\u0440\u0430\u0442\u044B CI-\u043F\u0440\u043E\u0433\u043E\u043D\u0430) \u2014 \u043A\u0440\u0430\u0441\u043D\u044B\u0439 typecheck \u0447\u0438\u043D\u0438. \u0422\u044F\u0436\u0451\u043B\u0443\u044E \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E (\u043F\u043E\u043B\u043D\u044B\u0439 \u0442\u0435\u0441\u0442-\u0441\u044C\u044E\u0442, e2e, docker-\u0441\u0431\u043E\u0440\u043A\u0438) \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E \u041D\u0415 \u0433\u043E\u043D\u044F\u0439, CI \u043F\u0440\u043E\u0433\u043E\u043D\u0438\u0442 \u0435\u0451 \u0441\u0430\u043C \u043F\u043E\u0441\u043B\u0435 push (\u0441\u043C. skill test-taskless \xABmirror CI locally\xBB \u2014 \u044D\u0442\u043E \u0442\u043E, \u0447\u0442\u043E \u0442\u0435\u043F\u0435\u0440\u044C \u0434\u0435\u043B\u0430\u0435\u0442 CI, \u0430 \u043D\u0435 \u0442\u044B); \u043F\u043E\u043B\u0430\u0433\u0430\u0439\u0441\u044F \u043D\u0430 \u043D\u0435\u0451, \u0430 \u043D\u0435 \u0434\u043E\u0434\u0443\u043C\u044B\u0432\u0430\u0439 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442." : "1. \u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u043F\u0430\u043A\u0435\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0442\u044B \u0442\u0440\u043E\u0433\u0430\u043B: typecheck + \u0442\u0435\u0441\u0442\u044B (\u0440\u0435\u0446\u0435\u043F\u0442 \u0438 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0439 \u0437\u0435\u043B\u0451\u043D\u043E\u0441\u0442\u0438 \u2014 \u0441\u043A\u0438\u043B\u043B test-taskless). \u041A\u0440\u0430\u0441\u043D\u043E\u0435 \u2014 \u041D\u0415 \u0441\u0434\u0430\u0432\u0430\u0439, \u0447\u0438\u043D\u0438.",
5155
5843
  "2. `/code-review --fix` \u2014 \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0440\u0435\u0432\u044C\u044E \u0441\u0432\u043E\u0435\u0433\u043E \u0434\u0438\u0444\u0444\u0430; \u043F\u0440\u0438\u043C\u0435\u043D\u0451\u043D\u043D\u044B\u0435 \u043F\u0440\u0430\u0432\u043A\u0438 \u043F\u0435\u0440\u0435\u0447\u0438\u0442\u0430\u0439 \u0438 \u043F\u0435\u0440\u0435\u0443\u0431\u0435\u0434\u0438\u0441\u044C, \u0447\u0442\u043E \u043D\u0435 \u0441\u043B\u043E\u043C\u0430\u043B \u0444\u0438\u0447\u0443.",
5156
5844
  "3. `/verify` \u2014 \u0422\u041E\u041B\u042C\u041A\u041E \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044F\u043B \u0432\u0438\u0434\u0438\u043C\u044B\u0439 UI/\u0432\u0438\u0437\u0443\u0430\u043B: \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0444\u0438\u0447\u0443 \u043D\u0430 \u0436\u0438\u0432\u043E\u043C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 (\u0441\u043A\u0438\u043B\u043B run-taskless) \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435, \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u043E \u043A\u043E\u043C\u043F\u0438\u043B\u0438\u0440\u0443\u0435\u0442\u0441\u044F.",
@@ -5169,7 +5857,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
5169
5857
  return parts.join("\n");
5170
5858
  }
5171
5859
  var HERO_CHARTER_PATH = join7(dirname2(THIS_FILE), "..", "docs", "HERO.md");
5172
- function buildHeroPrompt(task, run) {
5860
+ function buildHeroPrompt(task, run, taskContext) {
5173
5861
  const name = run.name || `#${run.id}`;
5174
5862
  const charter = readFileSync8(HERO_CHARTER_PATH, "utf8");
5175
5863
  const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
@@ -5184,6 +5872,8 @@ ${task.description}` : `\u042D\u041F\u0418\u041A ${run.task_slug} (\u0442\u0435\
5184
5872
  HEADLESS_BLOCK,
5185
5873
  "",
5186
5874
  head + epicUrl,
5875
+ // TASWM-153: комменты+доки эпика — то же обогащение, что у рабочего дрона.
5876
+ ...taskContext ? ["", taskContext] : [],
5187
5877
  "",
5188
5878
  "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u0422\u0412\u041E\u0419 \u0423\u0421\u0422\u0410\u0412 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
5189
5879
  charter
@@ -5248,7 +5938,7 @@ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch) {
5248
5938
  "\u041F\u043E\u0440\u044F\u0434\u043E\u043A:",
5249
5939
  "1. \u041F\u0440\u043E\u0433\u043E\u043D\u0438 \u0447\u0435\u043A-\u043B\u0438\u0441\u0442 \u0432\u044B\u0448\u0435: \u0442\u0435\u0441\u0442\u044B/typecheck \u0421\u0410\u041C (\u043D\u0435 \u0432\u0435\u0440\u044C \u043E\u0442\u0447\u0451\u0442\u0443 \u0430\u0432\u0442\u043E\u0440\u0430/CI), \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438/\u0433\u043E\u043D\u043A\u0438/UI/per-page meta \u043F\u043E \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\u0441\u0442\u0438.",
5250
5940
  `2. \u041E\u0441\u0442\u0430\u0432\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043D\u0430 \u0441\u0430\u043C PR: \`gh pr comment ${prUrl} --body "..."\` (\u041D\u0415 \`gh pr review\` \u2014 GitHub \u0435\u0433\u043E \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442 \u043D\u0430 \u0441\u0432\u043E\u0451\u043C PR). \u041D\u0430\u0447\u043D\u0438 \u0441 **APPROVE** \u0438\u043B\u0438 **REQUEST CHANGES** \u0437\u0430\u0433\u043B\u0430\u0432\u043D\u044B\u043C\u0438, \u0434\u0430\u043B\u044C\u0448\u0435 \u2014 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u0438\u043A\u0430: \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B, \u0447\u0442\u043E \u043D\u0430\u0448\u0451\u043B; \u0434\u043B\u044F REQUEST CHANGES \u2014 \u0442\u043E\u0447\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B/\u0441\u0442\u0440\u043E\u043A\u0438 \u0438 \u0447\u0442\u043E \u0447\u0438\u043D\u0438\u0442\u044C.`,
5251
- `3. \u0412\u044B\u0437\u043E\u0432\u0438 done(${prUrl}, verdict: "approve"|"changes", summary: "\u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u0447\u0442\u043E \u0440\u0435\u0448\u0438\u043B") \u2014 \u0440\u043E\u0439 \u0441\u0430\u043C \u0440\u0430\u0437\u043D\u0435\u0441\u0451\u0442 summary \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u043C\u0438 \u0432 REV-\u0437\u0430\u0434\u0430\u0447\u0443 \u0438 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A, \u0430 verdict \u0437\u0430\u043F\u0438\u0448\u0435\u0442 \u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D (\u0431\u0435\u0439\u0434\u0436 \u0432 \u0432\u0435\u0431\u0435). \u0422\u044B \u0441\u0430\u043C \u0440\u0435\u0432\u044C\u044E \u041D\u0415 \u043F\u043E\u0434\u043B\u0435\u0436\u0438\u0448\u044C \u2014 \u0440\u0435\u043A\u0443\u0440\u0441\u0438\u0438 \u043D\u0435\u0442.`
5941
+ `3. \u0412\u044B\u0437\u043E\u0432\u0438 done(${prUrl}, verdict: "approve"|"changes", summary: "\u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u0447\u0442\u043E \u0440\u0435\u0448\u0438\u043B") \u2014 \u0440\u043E\u0439 \u0441\u0430\u043C \u0440\u0430\u0437\u043D\u0435\u0441\u0451\u0442 summary \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A, \u0430 verdict \u0437\u0430\u043F\u0438\u0448\u0435\u0442 \u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D (\u0431\u0435\u0439\u0434\u0436 \u0432 \u0432\u0435\u0431\u0435). \u0422\u044B \u0441\u0430\u043C \u0440\u0435\u0432\u044C\u044E \u041D\u0415 \u043F\u043E\u0434\u043B\u0435\u0436\u0438\u0448\u044C \u2014 \u0440\u0435\u043A\u0443\u0440\u0441\u0438\u0438 \u043D\u0435\u0442.`
5252
5942
  ].join("\n");
5253
5943
  }
5254
5944
  function droneLabel(run) {
@@ -5346,6 +6036,100 @@ async function handleCiRed(deps, run, now, worktreeExists, hasTranscript, droneC
5346
6036
  narrate(deps, `\u043F\u043E\u0431\u0443\u0434\u043A\u0430 ${droneLabel(run)} \u043D\u0430 \u043A\u0440\u0430\u0441\u043D\u044B\u0439 CI \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C.`);
5347
6037
  }
5348
6038
  }
6039
+ async function trySpawnQueuedRun(deps, run) {
6040
+ const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
6041
+ if (mergedUrl) {
6042
+ await closeDuplicateTaskRun(deps, run, mergedUrl);
6043
+ await deps.finishTask(run.task_slug).catch(() => {
6044
+ });
6045
+ narrate(deps, `${droneLabel(run)} \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u0430 \u0434\u0440\u0443\u0433\u0438\u043C \u0440\u0430\u043D\u043E\u043C, \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E.`);
6046
+ return "closed";
6047
+ }
6048
+ if (deps.db.unmetDeps(run.depends_on).length > 0) return "skipped";
6049
+ try {
6050
+ await deps.spawn(run);
6051
+ 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)}.`);
6052
+ return "spawned";
6053
+ } catch (e) {
6054
+ if (e instanceof AreaBusyError) return "skipped";
6055
+ deps.db.updateRun(run.id, { status: "failed" });
6056
+ deps.db.addEvent(run.id, "failed", String(e));
6057
+ return "failed";
6058
+ }
6059
+ }
6060
+ async function batchSpawnQueue(deps, droneCap) {
6061
+ for (const run of deps.db.listRuns("queued")) {
6062
+ if (countActiveDrones(deps.db) >= droneCap) break;
6063
+ await trySpawnQueuedRun(deps, run);
6064
+ }
6065
+ }
6066
+ async function manageRelief(deps, now, droneCap, stormActive) {
6067
+ const state = deps.relief ?? (deps.relief = {});
6068
+ const cfg = state.config ? resolveReliefConfig(state.config) : DEFAULT_RELIEF_CONFIG;
6069
+ const needsRelief = deps.reliefNeeded ? deps.reliefNeeded() : false;
6070
+ const resourcesOk2 = deps.resourcesOk();
6071
+ state.reliefSince = needsRelief ? state.reliefSince ?? now : null;
6072
+ if (!resourcesOk2 || needsRelief) state.overloadedAt = now;
6073
+ if (needsRelief && deps.kill && pauseDue(state.reliefSince, state.lastPauseAt ?? null, now, cfg)) {
6074
+ const candidate = pickPauseCandidate(deps.db.listRuns("running"));
6075
+ if (candidate) {
6076
+ try {
6077
+ deps.kill(candidate.pid);
6078
+ } catch {
6079
+ }
6080
+ deps.db.updateRun(candidate.id, { status: "paused", stop_reason: RELIEF_PAUSE_REASON });
6081
+ deps.db.addEvent(candidate.id, "paused", RELIEF_PAUSE_REASON);
6082
+ state.lastPauseAt = now;
6083
+ narrate(deps, `\u0425\u043E\u0441\u0442 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D \u2014 \u0441\u0442\u0430\u0432\u043B\u044E ${droneLabel(candidate)} \u043D\u0430 \u043F\u0430\u0443\u0437\u0443 (\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443, TASWM-151).`);
6084
+ }
6085
+ return;
6086
+ }
6087
+ if (stormActive) return;
6088
+ if (!resourcesOk2) {
6089
+ if (deps.db.listRuns("queued").some((r) => deps.db.unmetDeps(r.depends_on).length === 0))
6090
+ console.error("[overmind] \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D: \u043C\u0430\u0448\u0438\u043D\u0430 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D\u0430 (\u0440\u0435\u0441\u0443\u0440\u0441-\u0433\u0435\u0439\u0442 TASWM-151)");
6091
+ return;
6092
+ }
6093
+ if (countActiveDrones(deps.db) >= droneCap) return;
6094
+ const pausedForRelief = deps.db.listRuns("paused").filter((r) => r.stop_reason === RELIEF_PAUSE_REASON);
6095
+ const recovering = pausedForRelief.length > 0 || state.overloadedAt != null && now - state.overloadedAt < cfg.rampWindowMs;
6096
+ if (!recovering) {
6097
+ await batchSpawnQueue(deps, droneCap);
6098
+ return;
6099
+ }
6100
+ if (!rampDue(state.lastRampAt ?? null, now, cfg)) return;
6101
+ const resumeCandidate = pickResumeCandidate(pausedForRelief);
6102
+ if (resumeCandidate) {
6103
+ if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
6104
+ if (!deps.respawn) return;
6105
+ const wt = resumeCandidate.worktree_path;
6106
+ const worktreeExists = deps.worktreeExists ?? existsSync4;
6107
+ const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
6108
+ const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
6109
+ const mode = canContinue ? "continue" : "fresh";
6110
+ try {
6111
+ await deps.respawn(resumeCandidate, mode);
6112
+ deps.db.updateRun(resumeCandidate.id, { stop_reason: null });
6113
+ deps.db.addEvent(
6114
+ resumeCandidate.id,
6115
+ "resumed",
6116
+ mode === "continue" ? "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043F\u0443\u0441\u0442\u0438\u043B\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 (--continue: \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 origin + \u0441\u0435\u0441\u0441\u0438\u044F \u0446\u0435\u043B\u044B)" : "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043F\u0443\u0441\u0442\u0438\u043B\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443, \u0441\u0442\u0430\u0440\u0442 \u0437\u0430\u043D\u043E\u0432\u043E (\u0441\u0435\u0441\u0441\u0438\u044F/worktree \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B)"
6117
+ );
6118
+ narrate(deps, `\u041F\u043E\u043B\u0435\u0433\u0447\u0430\u043B\u043E \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E ${droneLabel(resumeCandidate)} \u0438\u0437 \u043F\u0430\u0443\u0437\u044B.`);
6119
+ state.lastRampAt = now;
6120
+ } catch (e) {
6121
+ deps.db.updateRun(resumeCandidate.id, { status: "stalled" });
6122
+ deps.db.addEvent(resumeCandidate.id, "stalled", `\u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u0438\u0437 \u0440\u0435\u0441\u0443\u0440\u0441-\u043F\u0430\u0443\u0437\u044B \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
6123
+ }
6124
+ return;
6125
+ }
6126
+ for (const run of deps.db.listRuns("queued")) {
6127
+ const outcome = await trySpawnQueuedRun(deps, run);
6128
+ if (outcome === "skipped" || outcome === "closed") continue;
6129
+ state.lastRampAt = now;
6130
+ return;
6131
+ }
6132
+ }
5349
6133
  async function tickOnce(deps) {
5350
6134
  const now = deps.now ? deps.now() : Date.now();
5351
6135
  const worktreeExists = deps.worktreeExists ?? existsSync4;
@@ -5372,9 +6156,9 @@ async function tickOnce(deps) {
5372
6156
  }
5373
6157
  if (deps.rehydrate && deps.kill) {
5374
6158
  const kill = deps.kill;
5375
- for (const run of deps.db.listRuns("stopped")) {
5376
- if (run.pid == null || !deps.isAlive(run.pid)) continue;
5377
- console.log(`[overmind] reap: \u043F\u0440\u043E\u0433\u043E\u043D ${run.name} (#${run.id}) \u043F\u043E\u043C\u0435\u0447\u0435\u043D stopped \u0438\u0437\u0432\u043D\u0435 \u2014 \u0433\u0430\u0448\u0443 pid ${run.pid}`);
6159
+ for (const run of deps.db.listRuns()) {
6160
+ if (!isTerminalStatus(run.status) || run.pid == null || !deps.isAlive(run.pid)) continue;
6161
+ console.log(`[overmind] reap: \u043F\u0440\u043E\u0433\u043E\u043D ${run.name} (#${run.id}) \u043F\u043E\u043C\u0435\u0447\u0435\u043D ${run.status} \u0438\u0437\u0432\u043D\u0435 \u2014 \u0433\u0430\u0448\u0443 pid ${run.pid}`);
5378
6162
  try {
5379
6163
  kill(run.pid);
5380
6164
  } catch {
@@ -5383,6 +6167,18 @@ async function tickOnce(deps) {
5383
6167
  });
5384
6168
  }
5385
6169
  }
6170
+ if (deps.sweep) {
6171
+ const sw = deps.sweep;
6172
+ const sinceLast = sw.lastAt == null ? Infinity : now - sw.lastAt;
6173
+ if (sinceLast >= sw.intervalMs) {
6174
+ sw.lastAt = now;
6175
+ try {
6176
+ await sw.run();
6177
+ } catch (e) {
6178
+ console.error("[overmind] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
6179
+ }
6180
+ }
6181
+ }
5386
6182
  let stormActive = false;
5387
6183
  if (deps.storm) {
5388
6184
  for (const run of deps.db.listRuns("running")) {
@@ -5393,26 +6189,10 @@ async function tickOnce(deps) {
5393
6189
  stormActive = await manageStorm(deps, now, worktreeExists, hasTranscript);
5394
6190
  }
5395
6191
  const droneCap = deps.maxDrones ? await deps.maxDrones() : Infinity;
5396
- if (!stormActive) {
5397
- const canSpawn = !deps.resourcesOk || deps.resourcesOk();
5398
- const queued = deps.db.listRuns("queued");
5399
- if (!canSpawn) {
5400
- if (queued.some((r) => deps.db.unmetDeps(r.depends_on).length === 0))
5401
- console.error("[overmind] \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D: \u043C\u0430\u0448\u0438\u043D\u0430 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D\u0430 (\u0440\u0435\u0441\u0443\u0440\u0441-\u0433\u0435\u0439\u0442 TASWM-130)");
5402
- } else {
5403
- for (const run of queued) {
5404
- if (countActiveDrones(deps.db) >= droneCap) break;
5405
- if (deps.db.unmetDeps(run.depends_on).length > 0) continue;
5406
- try {
5407
- await deps.spawn(run);
5408
- 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)}.`);
5409
- } catch (e) {
5410
- if (e instanceof AreaBusyError) continue;
5411
- deps.db.updateRun(run.id, { status: "failed" });
5412
- deps.db.addEvent(run.id, "failed", String(e));
5413
- }
5414
- }
5415
- }
6192
+ if (deps.resourcesOk) {
6193
+ await manageRelief(deps, now, droneCap, stormActive);
6194
+ } else if (!stormActive) {
6195
+ await batchSpawnQueue(deps, droneCap);
5416
6196
  }
5417
6197
  for (const run of deps.db.listRuns("review")) {
5418
6198
  try {
@@ -5425,8 +6205,6 @@ async function tickOnce(deps) {
5425
6205
  continue;
5426
6206
  }
5427
6207
  if (run.reviews_run_id != null) {
5428
- await deps.finishTask(run.task_slug).catch(() => {
5429
- });
5430
6208
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
5431
6209
  });
5432
6210
  deps.db.updateRun(run.id, { status: "done" });
@@ -5437,12 +6215,7 @@ async function tickOnce(deps) {
5437
6215
  if (!run.pr_url) continue;
5438
6216
  const st = await deps.prStatus(run.pr_url);
5439
6217
  if (st.merged) {
5440
- await deps.finishTask(run.task_slug);
5441
- if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
5442
- });
5443
- deps.db.addEvent(run.id, "merged", run.pr_url);
5444
- deps.db.updateRun(run.id, { status: "done" });
5445
- deps.db.addEvent(run.id, "done");
6218
+ await finalizeMergedReview(deps, run);
5446
6219
  narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${run.task_slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
5447
6220
  continue;
5448
6221
  }
@@ -5467,15 +6240,47 @@ async function tickOnce(deps) {
5467
6240
  } else if (!st.conflicting && deps.ci && deps.respawn && !stormActive) {
5468
6241
  await handleCiRed(deps, run, now, worktreeExists, hasTranscript, droneCap);
5469
6242
  }
5470
- if (!st.conflicting && !st.isDraft && deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt)) {
5471
- const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
5472
- const leavesSlotForDrones = countActiveDrones(deps.db) < droneCap - 1;
5473
- if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk && leavesSlotForDrones && deps.db.reviewerCountFor(run.id) === 0) {
5474
- try {
5475
- await deps.spawnReviewer(run, st.files ?? []);
5476
- narrate(deps, `PR ${droneLabel(run)} \u0433\u043E\u0442\u043E\u0432 \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430.`);
5477
- } catch (e) {
5478
- deps.db.addEvent(run.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u043E\u0441\u044C: ${String(e)}`);
6243
+ if (!st.conflicting && !st.isDraft && deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt) && deps.db.reviewerCountFor(run.id) === 0) {
6244
+ const attempts = deps.db.reviewerAttemptCountFor(run.id);
6245
+ let verdictFromComment = null;
6246
+ if (attempts > 0 && deps.verdictComment) {
6247
+ const lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
6248
+ verdictFromComment = await deps.verdictComment(run.pr_url, lastAttempt?.created_at ?? run.created_at).catch(() => null);
6249
+ }
6250
+ if (verdictFromComment) {
6251
+ const verdictText = verdictLabel(verdictFromComment.verdict);
6252
+ deps.db.updateRun(run.id, { review_verdict: verdictFromComment.verdict });
6253
+ deps.db.addEvent(run.id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E \u043F\u043E\u0434\u0445\u0432\u0430\u0447\u0435\u043D \u0438\u0437 PR-\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430 (done() \u0431\u044B\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0443): ${verdictText}`);
6254
+ narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0430\u0448\u0451\u043B\u0441\u044F \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0445 PR (${verdictText}), \u0440\u0435\u0441\u043F\u0430\u0432\u043D \u043D\u0435 \u043D\u0443\u0436\u0435\u043D.`);
6255
+ } else if (attempts >= REVIEW_SPAWN_LIMIT) {
6256
+ if (!deps.db.events(run.id).some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
6257
+ deps.db.addEvent(run.id, "step", `${REVIEW_ESCALATION_MARK}: ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u0440\u0435\u0432\u044C\u044E \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
6258
+ narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F \u0437\u0430 ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`);
6259
+ }
6260
+ } else {
6261
+ const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
6262
+ const leavesSlotForDrones = countActiveDrones(deps.db) < droneCap - 1;
6263
+ if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk && leavesSlotForDrones) {
6264
+ const freshSt = await deps.prStatus(run.pr_url);
6265
+ if (freshSt.merged) {
6266
+ await finalizeMergedReview(deps, run);
6267
+ narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E, \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
6268
+ } else if (freshSt.state === "CLOSED") {
6269
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
6270
+ });
6271
+ deps.db.updateRun(run.id, { status: "done" });
6272
+ deps.db.addEvent(run.id, "done", "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E");
6273
+ narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
6274
+ } else if (freshSt.conflicting || freshSt.isDraft) {
6275
+ deps.db.addEvent(run.id, "step", "PR \u0441\u0442\u0430\u043B \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u044E\u0449\u0438\u043C/\u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u043E\u043C, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D \u0434\u043E \u0441\u043B\u0435\u0434. \u0442\u0438\u043A\u0430");
6276
+ } else {
6277
+ try {
6278
+ await deps.spawnReviewer(run, st.files ?? []);
6279
+ narrate(deps, `PR ${droneLabel(run)} \u0433\u043E\u0442\u043E\u0432 \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430.`);
6280
+ } catch (e) {
6281
+ deps.db.addEvent(run.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u043E\u0441\u044C: ${String(e)}`);
6282
+ }
6283
+ }
5479
6284
  }
5480
6285
  }
5481
6286
  }
@@ -5483,6 +6288,7 @@ async function tickOnce(deps) {
5483
6288
  console.error(`[overmind] \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B review-\u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} (${run.task_slug}) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
5484
6289
  }
5485
6290
  }
6291
+ await closeMergedTaskRuns(deps);
5486
6292
  let liveChildPpids;
5487
6293
  try {
5488
6294
  liveChildPpids = deps.livePpids?.() ?? null;
@@ -5591,6 +6397,7 @@ async function tickOnce(deps) {
5591
6397
  for (const run of deps.db.listRuns("paused")) {
5592
6398
  if (run.kind === "hero") continue;
5593
6399
  if (run.reviews_run_id != null) continue;
6400
+ if (run.stop_reason === RELIEF_PAUSE_REASON) continue;
5594
6401
  if (run.pid != null && deps.isAlive(run.pid)) continue;
5595
6402
  const wt = run.worktree_path;
5596
6403
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -5735,12 +6542,8 @@ async function spawnReviewer(deps, reviewRun, files) {
5735
6542
  const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
5736
6543
  const branch = reviewRun.branch || reviewRun.task_slug;
5737
6544
  const checklist = buildReviewChecklist(reviewRun, files);
5738
- const rev = deps.taskless ? await deps.taskless.createTask({
5739
- title: `REV: ${reviewRun.task_slug} \u2014 ${reviewRun.task_title ?? "\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E PR"}`,
5740
- description: checklist
5741
- }).catch(() => null) : null;
5742
6545
  const reviewer = deps.db.createRun({
5743
- task_slug: rev?.slug ?? `${reviewRun.task_slug}-review`,
6546
+ task_slug: reviewRun.task_slug,
5744
6547
  repo,
5745
6548
  kind: "reviewer",
5746
6549
  reviews_run_id: reviewRun.id
@@ -5752,7 +6555,7 @@ async function spawnReviewer(deps, reviewRun, files) {
5752
6555
  const prompt = buildReviewerPrompt(reviewRun, reviewer, checklist, branch);
5753
6556
  const pid = deps.launch({ worktree: wt, run: reviewer, prompt });
5754
6557
  deps.db.updateRun(reviewer.id, { status: "running", worktree_path: wt, branch, pid });
5755
- deps.db.addEvent(reviewRun.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E: \u043F\u043E\u0434\u043D\u044F\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${reviewer.name} (#${reviewer.id})${rev ? ` \u2014 REV-\u0437\u0430\u0434\u0430\u0447\u0430 ${rev.slug}` : ""}`);
6558
+ deps.db.addEvent(reviewRun.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E: \u043F\u043E\u0434\u043D\u044F\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${reviewer.name} (#${reviewer.id})`);
5756
6559
  return deps.db.getRun(reviewer.id);
5757
6560
  } catch (e) {
5758
6561
  await deps.git.removeWorktree(repo, wt).catch(() => {
@@ -5779,8 +6582,12 @@ async function respawnRun(deps, run, mode, nudge) {
5779
6582
  const sourceMsg = worktreeSourceMessage(branch, source);
5780
6583
  if (sourceMsg) deps.db.addEvent(run.id, "step", sourceMsg);
5781
6584
  }
5782
- const task = await deps.getTask(run.task_slug);
5783
- const prompt = run.kind === "hero" ? buildHeroPrompt(task, run) : buildPrompt(task, run);
6585
+ const [task, verifyMode] = await Promise.all([
6586
+ deps.getTask(run.task_slug),
6587
+ deps.verifyMode ? deps.verifyMode(repo) : Promise.resolve(void 0)
6588
+ ]);
6589
+ const taskContext = task && deps.getTaskContext ? await deps.getTaskContext(task) : void 0;
6590
+ const prompt = run.kind === "hero" ? buildHeroPrompt(task, run, taskContext) : buildPrompt(task, run, verifyMode, taskContext);
5784
6591
  const pid = deps.launch({ worktree: wt, run, prompt });
5785
6592
  deps.db.updateRun(run.id, { status: "running", pid });
5786
6593
  }
@@ -5809,8 +6616,16 @@ async function syncCrashedOnStartup(deps) {
5809
6616
  async function makeStore(repo) {
5810
6617
  if (SWARM_BACKEND !== "client") return { store: new SwarmDb(DB_PATH, repo), reconciled: true };
5811
6618
  const failing = async () => ({ isError: true });
5812
- const call = await connectSwarmMcp() ?? failing;
5813
- const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo });
6619
+ const [call, repoRegistry] = await Promise.all([
6620
+ connectSwarmMcp().then((c) => c ?? failing),
6621
+ buildRepoRegistry(SWARM_REPO_PATHS)
6622
+ ]);
6623
+ if (SWARM_REPO_PATHS.length > 0 && repoRegistry.length < SWARM_REPO_PATHS.length) {
6624
+ console.warn(
6625
+ `[overmind] \u0440\u0435\u043F\u043E-\u0440\u0435\u0435\u0441\u0442\u0440: ${repoRegistry.length}/${SWARM_REPO_PATHS.length} \u043F\u0443\u0442\u0435\u0439 \u0440\u0435\u0437\u043E\u043B\u0432\u043D\u0443\u043B\u0438\u0441\u044C \u0432 repoFullName (\u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u0435 \u2014 \u043D\u0435 git-\u0440\u0435\u043F\u043E \u0438\u043B\u0438 \u0431\u0435\u0437 remote origin)`
6626
+ );
6627
+ }
6628
+ const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo, repoRegistry });
5814
6629
  await client.hydrate();
5815
6630
  const rec = client.reconcileRuntime({ worktreesDir: WORKTREES_DIR, listProcesses: listDroneProcesses });
5816
6631
  if (rec.reconciled) {
@@ -5902,7 +6717,20 @@ async function runDaemon(opts) {
5902
6717
  ttlMs: 1e4
5903
6718
  });
5904
6719
  }
6720
+ let verifyModeReader;
6721
+ if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
6722
+ const client = db;
6723
+ verifyModeReader = makeVerifyModeReader({
6724
+ fetch: async (repo) => {
6725
+ const repoFullName = await remoteRepoFullName(repo).catch(() => null);
6726
+ return repoFullName ? client.repoVerifyMode(repoFullName) : null;
6727
+ },
6728
+ fallback: SWARM_VERIFY,
6729
+ ttlMs: 6e4
6730
+ });
6731
+ }
5905
6732
  let spawnReviewerFn;
6733
+ let verdictCommentFn;
5906
6734
  let reviewCutoffAtValue;
5907
6735
  if (SWARM_AUTO_REVIEW) {
5908
6736
  spawnReviewerFn = (reviewRun, files) => spawnReviewer(
@@ -5911,13 +6739,13 @@ async function runDaemon(opts) {
5911
6739
  repoFallback: opts.repo,
5912
6740
  worktreesDir: WORKTREES_DIR,
5913
6741
  launch,
5914
- git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
5915
- taskless: tl
6742
+ git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree }
5916
6743
  },
5917
6744
  reviewRun,
5918
6745
  files
5919
6746
  ).then(() => {
5920
6747
  });
6748
+ verdictCommentFn = (prUrl, sinceIso) => latestVerdictComment(prUrl, sinceIso);
5921
6749
  reviewCutoffAtValue = reviewCutoffAt();
5922
6750
  }
5923
6751
  const narrator = makeOvermindNarrator();
@@ -5925,16 +6753,23 @@ async function runDaemon(opts) {
5925
6753
  db,
5926
6754
  narrate: narrator.narrate,
5927
6755
  spawn: async (run) => {
5928
- const task = tl ? await tl.getTask(run.task_slug) : null;
5929
- const prompt = run.kind === "hero" ? buildHeroPrompt(task, run) : buildPrompt(task, run);
6756
+ const [task, verifyMode] = await Promise.all([
6757
+ tl ? tl.getTask(run.task_slug) : Promise.resolve(null),
6758
+ verifyModeReader ? verifyModeReader(normalizeRepo(run.repo || opts.repo)) : Promise.resolve(void 0)
6759
+ ]);
6760
+ const taskContext = task && tl ? await fetchTaskContext(tl, task) : void 0;
6761
+ const prompt = run.kind === "hero" ? buildHeroPrompt(task, run, taskContext) : buildPrompt(task, run, verifyMode, taskContext);
5930
6762
  await spawnDrone({ ...baseSpawn, buildPrompt: () => prompt }, run);
5931
6763
  if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
5932
6764
  if (tl) await tl.startTask(run.task_slug);
5933
6765
  },
5934
- prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files })),
6766
+ prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state })),
5935
6767
  // Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
5936
6768
  // (фолбэк — дефолт-репо роя). Реальный gh; логика усыновления — в tickOnce (тестируется).
5937
6769
  openPrsForBranch: (branch, repoDir) => openPrsForBranch(branch, repoDir || opts.repo),
6770
+ // Смерженный PR ветки-слага (TASWM-206) — тот же фолбэк на дефолт-репо роя, что и
6771
+ // openPrsForBranch выше; реальный gh, логика закрытия дублей — в tickOnce (тестируется).
6772
+ mergedPrForBranch: (branch, repoDir) => mergedPrForBranch(branch, repoDir || opts.repo),
5938
6773
  finishTask: (slug) => tl ? tl.finishTask(slug) : Promise.resolve(),
5939
6774
  removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p),
5940
6775
  isAlive: pidAlive,
@@ -5944,7 +6779,27 @@ async function runDaemon(opts) {
5944
6779
  livePpids,
5945
6780
  // Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
5946
6781
  // POST /swarm/kill → stopped в проде → overmind гасит живой pid на реконсайле).
5947
- kill: (pid) => process.kill(pid, "SIGTERM"),
6782
+ kill: (pid) => killProcessGroup(pid, "SIGTERM"),
6783
+ // Защитный sweep осиротевших процессов/docker-контейнеров (TASWM-210): активные
6784
+ // worktree-пути и docker-префиксы — из ВСЕХ ранов в БД (не только running/blocked —
6785
+ // paused/stalled/queued/review/storm_dead тоже держат свой worktree/контейнеры, их
6786
+ // сносить рано), терминальные (done/failed/stopped/superseded) — кандидаты на снос.
6787
+ sweep: {
6788
+ run: () => {
6789
+ const runsWithWorktree = db.listRuns().filter((r) => r.worktree_path);
6790
+ return sweepOrphans({
6791
+ worktreesDir: WORKTREES_DIR,
6792
+ activeWorktreePaths: new Set(runsWithWorktree.filter((r) => !isTerminalStatus(r.status)).map((r) => r.worktree_path)),
6793
+ containerRunPrefixes: runsWithWorktree.map((r) => ({
6794
+ prefix: composeProjectPrefix(basename3(r.worktree_path)),
6795
+ active: !isTerminalStatus(r.status)
6796
+ }))
6797
+ }).then(() => {
6798
+ });
6799
+ },
6800
+ intervalMs: SWARM_SWEEP_INTERVAL_MS,
6801
+ lastAt: null
6802
+ },
5948
6803
  isDirty: (p) => worktreeDirty(p),
5949
6804
  // git-активность как liveness (TASWM-67): последний коммит worktree — реальный git-вызов.
5950
6805
  lastCommitMs: (p) => worktreeLastCommitMs(p),
@@ -5953,6 +6808,12 @@ async function runDaemon(opts) {
5953
6808
  transcriptMtimeMs: (p) => transcriptMtimeMs(p),
5954
6809
  // Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
5955
6810
  resourcesOk: hostResourcesOk,
6811
+ // Ресурс-гейт v2 (TASWM-151): жёсткий порог активной разгрузки — те же сырые показания
6812
+ // хоста (общий кэш в resources.ts), но заведомо строже настроенные пороги.
6813
+ reliefNeeded: hostReliefNeeded,
6814
+ // Состояние градуального контроллера (окна ramp/pause, reliefSince) — заводим ОДИН раз
6815
+ // на цикл демона, tickOnce (manageRelief) мутирует эти же поля каждый тик.
6816
+ relief: {},
5956
6817
  // merge-main-in: review-PR конфликтует с main → влить main в ветку PR, не переделывая
5957
6818
  // фичу. Реальные git-примитивы + launch; логика — в resolveConflict (тестируется).
5958
6819
  resolveConflict: (reviewRun) => resolveConflict(
@@ -5970,7 +6831,15 @@ async function runDaemon(opts) {
5970
6831
  // (kind-aware: герой ← HERO_CONTINUE_NUDGE/buildHeroPrompt). Лимит — RESPAWN_LIMIT, считает tickOnce.
5971
6832
  // nudge прокинут для CI-побудки (TASWM-102): tickOnce передаёт buildCiWakeNudge(summary).
5972
6833
  respawn: (run, mode, nudge) => respawnRun(
5973
- { db, launch, repoFallback: opts.repo, worktreesDir: WORKTREES_DIR, getTask: (slug) => tl ? tl.getTask(slug) : Promise.resolve(null) },
6834
+ {
6835
+ db,
6836
+ launch,
6837
+ repoFallback: opts.repo,
6838
+ worktreesDir: WORKTREES_DIR,
6839
+ getTask: (slug) => tl ? tl.getTask(slug) : Promise.resolve(null),
6840
+ ...verifyModeReader ? { verifyMode: verifyModeReader } : {},
6841
+ getTaskContext: (task) => tl ? fetchTaskContext(tl, task) : Promise.resolve(void 0)
6842
+ },
5974
6843
  run,
5975
6844
  mode,
5976
6845
  nudge
@@ -6003,9 +6872,9 @@ async function runDaemon(opts) {
6003
6872
  // Встроенный надзор (client-режим, TASWM-194): ключ есть только когда сконфигурен.
6004
6873
  ...anomalies ? { anomalies } : {},
6005
6874
  // Авто-ревью (TASWM-148): ключи есть только когда SWARM_AUTO_REVIEW включён (дефолт да) —
6006
- // spawnReviewerFn и reviewCutoffAtValue выставляются В ОДНОМ if(SWARM_AUTO_REVIEW) блоке
6007
- // выше, поэтому один и тот же гейт на оба (без второго независимого тернарного условия).
6008
- ...spawnReviewerFn ? { spawnReviewer: spawnReviewerFn, reviewCap: SWARM_REVIEW_CAP, reviewCutoffAt: reviewCutoffAtValue } : {}
6875
+ // spawnReviewerFn/verdictCommentFn/reviewCutoffAtValue выставляются В ОДНОМ if(SWARM_AUTO_REVIEW)
6876
+ // блоке выше, поэтому один и тот же гейт на все три (без отдельных тернарных условий).
6877
+ ...spawnReviewerFn ? { spawnReviewer: spawnReviewerFn, verdictComment: verdictCommentFn, reviewCap: SWARM_REVIEW_CAP, reviewCutoffAt: reviewCutoffAtValue } : {}
6009
6878
  };
6010
6879
  const web = startWeb({
6011
6880
  db,
@@ -6018,7 +6887,7 @@ async function runDaemon(opts) {
6018
6887
  replay: (qid, chosen) => replayDecision(
6019
6888
  {
6020
6889
  db,
6021
- kill: (pid) => process.kill(pid, "SIGTERM"),
6890
+ kill: (pid) => killProcessGroup(pid, "SIGTERM"),
6022
6891
  closePR: (u) => closePR(u),
6023
6892
  removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
6024
6893
  },
@@ -6030,7 +6899,7 @@ async function runDaemon(opts) {
6030
6899
  stop: (slug, reason) => cancelRun(
6031
6900
  {
6032
6901
  db,
6033
- kill: (pid) => process.kill(pid, "SIGTERM"),
6902
+ kill: (pid) => killProcessGroup(pid, "SIGTERM"),
6034
6903
  removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
6035
6904
  },
6036
6905
  slug,
@@ -6044,6 +6913,10 @@ async function runDaemon(opts) {
6044
6913
  console.log(`\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430: http://127.0.0.1:${web.port} \u2190 \u043E\u0442\u043A\u0440\u043E\u0439 \u0438 \u0432\u043E\u0439\u0434\u0438 \u0447\u0435\u0440\u0435\u0437 Taskless`);
6045
6914
  const interval = opts.intervalMs ?? 4e3;
6046
6915
  writeHeartbeat(HEARTBEAT_PATH, Date.now());
6916
+ if (deps.sweep) {
6917
+ deps.sweep.lastAt = Date.now();
6918
+ await deps.sweep.run().catch((e) => console.error("[overmind] \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 sweep \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e));
6919
+ }
6047
6920
  for (; ; ) {
6048
6921
  try {
6049
6922
  writeHeartbeat(HEARTBEAT_PATH, Date.now());
@@ -6085,6 +6958,8 @@ export {
6085
6958
  RESOLVE_LIMIT,
6086
6959
  RESPAWN_LIMIT,
6087
6960
  RESPAWN_YOUNG_MS,
6961
+ REVIEW_ESCALATION_MARK,
6962
+ REVIEW_SPAWN_LIMIT,
6088
6963
  STALL_MS,
6089
6964
  buildCiWakeNudge,
6090
6965
  buildHeroPrompt,