@taskless-app/swarm 0.6.48 → 0.6.50

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 +262 -25
  2. package/dist/overmind.js +485 -53
  3. package/package.json +2 -2
package/dist/overmind.js CHANGED
@@ -666,7 +666,7 @@ var init_modelPolicy = __esm({
666
666
  label: "Anthropic",
667
667
  executorId: "claude-code",
668
668
  selectable: true,
669
- roles: { executor: "claude-opus-5", review: "claude-sonnet-5" }
669
+ roles: { executor: "claude-opus-5-5", review: "claude-sonnet-5" }
670
670
  },
671
671
  // TASWM-565 · шаг 1 из четырёх: провайдер в реестре ЕСТЬ, выбрать его ещё нельзя.
672
672
  // Имён моделей здесь нет СОЗНАТЕЛЬНО: у владельца подписка, состав моделей в ней нам
@@ -1277,6 +1277,9 @@ var init_cost = __esm({
1277
1277
  });
1278
1278
 
1279
1279
  // ../swarm-core/src/modelPricing.ts
1280
+ function cacheReadFactorOf(pricing) {
1281
+ return pricing.cacheReadFactor ?? CACHE_READ_FACTOR;
1282
+ }
1280
1283
  function findModelPricing(model, table = MODEL_PRICING) {
1281
1284
  if (!model) return void 0;
1282
1285
  const exact = table[model];
@@ -1298,6 +1301,10 @@ var init_modelPricing = __esm({
1298
1301
  // и «почему прогон вчера стоил иначе» приходится выяснять по коду, а не по таблице.
1299
1302
  // Меняется цена — меняется строка, одним видимым коммитом.
1300
1303
  "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
1304
+ // Opus 5.5 — 4/20, и чтение из кеша у него 5 % входа ($0.20), а не обычные 10 %.
1305
+ // У дронов чтение из кеша — основная часть токенов: с общим множителем прогон на
1306
+ // Opus 5.5 стоил бы в расчёте почти вдвое больше настоящего.
1307
+ "claude-opus-5-5": { inputPerMTok: 4, outputPerMTok: 20, cacheReadFactor: 0.05 },
1301
1308
  "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
1302
1309
  "claude-fable-5": { inputPerMTok: 10, outputPerMTok: 50 },
1303
1310
  // Прогоны и вызовы на 4.x остаются в истории — строки держим ради их стоимости.
@@ -1716,13 +1723,61 @@ function similarTasks(model, text, limit = 5) {
1716
1723
  }
1717
1724
  return scored.sort((a, b) => b.score - a.score).slice(0, limit);
1718
1725
  }
1726
+ function historyFiles(model) {
1727
+ let idx = historyIndex.get(model);
1728
+ if (!idx) {
1729
+ const files = new Set(model.entries.flatMap((e) => e.files));
1730
+ const byBase = /* @__PURE__ */ new Map();
1731
+ for (const f of files) {
1732
+ const base3 = f.slice(f.lastIndexOf("/") + 1);
1733
+ byBase.set(base3, [...byBase.get(base3) ?? [], f]);
1734
+ }
1735
+ idx = { files, byBase };
1736
+ historyIndex.set(model, idx);
1737
+ }
1738
+ return idx;
1739
+ }
1740
+ function namedPaths(model, text, fileExists) {
1741
+ const { files, byBase } = historyFiles(model);
1742
+ const known = (p) => files.has(p) || (fileExists?.(p) ?? false);
1743
+ const raw = [...new Set((text.match(PATH_CANDIDATE) ?? []).map((c) => c.replace(/^\.?\/+/, "").replace(/\.+$/, "")))].filter((c) => c && !c.split("/").includes(".."));
1744
+ raw.sort((a, b) => b.split("/").length - a.split("/").length);
1745
+ const out = [];
1746
+ const dirs = /* @__PURE__ */ new Set();
1747
+ const take = (p) => {
1748
+ if (out.includes(p)) return;
1749
+ out.push(p);
1750
+ dirs.add(dirOf(p));
1751
+ };
1752
+ for (const c of raw) {
1753
+ if (known(c)) {
1754
+ take(c);
1755
+ continue;
1756
+ }
1757
+ const base3 = c.slice(c.lastIndexOf("/") + 1);
1758
+ let matches = (byBase.get(base3) ?? []).filter((f) => f === c || f.endsWith(`/${c}`));
1759
+ if (matches.length > 1) matches = matches.filter((f) => dirs.has(dirOf(f)));
1760
+ if (matches.length === 1) {
1761
+ take(matches[0]);
1762
+ continue;
1763
+ }
1764
+ if (!c.includes("/") && fileExists) {
1765
+ const sibling = [...dirs].map((d) => d ? `${d}/${c}` : c).filter(fileExists);
1766
+ if (sibling.length === 1) take(sibling[0]);
1767
+ }
1768
+ }
1769
+ return out;
1770
+ }
1719
1771
  function predictFiles(model, opts) {
1720
1772
  if (!hasZoneModel(model)) return [];
1721
1773
  const limit = opts.limit ?? 40;
1722
1774
  const best = /* @__PURE__ */ new Map();
1775
+ const named = opts.text ? namedPaths(model, opts.text, opts.fileExists) : [];
1776
+ for (const path of named) best.set(path, { path, weight: 1, why: "\u043D\u0430\u0437\u0432\u0430\u043D \u0432 \u0437\u0430\u0434\u0430\u0447\u0435", named: true });
1777
+ const factor = named.length ? NAMED_BACKGROUND_FACTOR : 1;
1723
1778
  const put = (path, weight, why) => {
1724
1779
  const cur = best.get(path);
1725
- if (!cur || cur.weight < weight) best.set(path, { path, weight, why });
1780
+ if (!cur || cur.weight < weight * factor) best.set(path, { path, weight: weight * factor, why });
1726
1781
  };
1727
1782
  if (opts.text) {
1728
1783
  const sim = similarTasks(model, opts.text, opts.similar ?? 5);
@@ -1739,12 +1794,18 @@ function predictFiles(model, opts) {
1739
1794
  }
1740
1795
  return [...best.values()].sort((a, b) => b.weight - a.weight || a.path.localeCompare(b.path)).slice(0, limit);
1741
1796
  }
1797
+ function heldByPrediction(predicted) {
1798
+ const named = predicted.filter((p) => p.named);
1799
+ return (named.length ? named : predicted).map((p) => p.path);
1800
+ }
1742
1801
  function overlapRisk(predicted, actual, opts = {}) {
1743
1802
  const o = { ...OVERLAP_DEFAULTS, ...opts };
1744
1803
  const actualSet = new Set(actual);
1745
1804
  const actualDirs = new Set([...actualSet].map(dirOf).filter(Boolean));
1746
1805
  const totalWeight = predicted.reduce((a, p) => a + p.weight, 0);
1747
1806
  if (!totalWeight) return { overlap: false, score: 0, sharedFiles: [], sharedDirs: [] };
1807
+ const hasNamed = predicted.some((p) => p.named);
1808
+ let namedHit = false;
1748
1809
  let hit = 0;
1749
1810
  let strong = false;
1750
1811
  const sharedFiles = [];
@@ -1753,24 +1814,26 @@ function overlapRisk(predicted, actual, opts = {}) {
1753
1814
  if (actualSet.has(p.path)) {
1754
1815
  hit += p.weight;
1755
1816
  sharedFiles.push(p.path);
1756
- if (p.weight >= o.strongFile) strong = true;
1817
+ if (p.named) namedHit = true;
1818
+ if (p.weight >= o.strongFile && (p.named || !hasNamed)) strong = true;
1757
1819
  continue;
1758
1820
  }
1759
1821
  const d = dirOf(p.path);
1760
1822
  if (d && actualDirs.has(d)) {
1761
1823
  hit += p.weight * 0.25;
1762
1824
  sharedDirs.add(d);
1825
+ if (p.named) namedHit = true;
1763
1826
  }
1764
1827
  }
1765
1828
  const score = Math.min(1, hit / totalWeight);
1766
1829
  return {
1767
- overlap: strong || score >= o.threshold,
1830
+ overlap: strong || score >= o.threshold && (!hasNamed || namedHit),
1768
1831
  score,
1769
1832
  sharedFiles: sharedFiles.slice(0, 10),
1770
1833
  sharedDirs: [...sharedDirs].slice(0, 5)
1771
1834
  };
1772
1835
  }
1773
- var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS, OVERLAP_DEFAULTS;
1836
+ var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS, PATH_CANDIDATE, historyIndex, NAMED_BACKGROUND_FACTOR, OVERLAP_DEFAULTS;
1774
1837
  var init_codeZones = __esm({
1775
1838
  "../swarm-core/src/codeZones.ts"() {
1776
1839
  "use strict";
@@ -1849,6 +1912,9 @@ var init_codeZones = __esm({
1849
1912
  "\u043F\u0435\u0440\u0444",
1850
1913
  "wip"
1851
1914
  ]);
1915
+ PATH_CANDIDATE = /[\w@.\-/]+\.[A-Za-z][A-Za-z0-9]{0,5}/g;
1916
+ historyIndex = /* @__PURE__ */ new WeakMap();
1917
+ NAMED_BACKGROUND_FACTOR = 0.1;
1852
1918
  OVERLAP_DEFAULTS = { threshold: 0.2, strongFile: 0.35 };
1853
1919
  }
1854
1920
  });
@@ -2168,7 +2234,7 @@ function costFromUsage(usage, model, table = MODEL_PRICING) {
2168
2234
  const output = usage.output_tokens ?? 0;
2169
2235
  const cacheWrite = usage.cache_creation_input_tokens ?? 0;
2170
2236
  const cacheRead = usage.cache_read_input_tokens ?? 0;
2171
- const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
2237
+ const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * cacheReadFactorOf(pricing)) / 1e6;
2172
2238
  return { costUsd };
2173
2239
  }
2174
2240
  var init_pricing = __esm({
@@ -3480,6 +3546,18 @@ var init_zoneNotes = __esm({
3480
3546
  }
3481
3547
  });
3482
3548
 
3549
+ // src/store.ts
3550
+ function isCleanResolve(run) {
3551
+ return (run.stop_reason ?? "").includes(RESOLVE_CLEAN_MARK);
3552
+ }
3553
+ var RESOLVE_CLEAN_MARK;
3554
+ var init_store = __esm({
3555
+ "src/store.ts"() {
3556
+ "use strict";
3557
+ RESOLVE_CLEAN_MARK = "\u0447\u0438\u0441\u0442\u043E\u0435 \u0432\u043B\u0438\u0432\u0430\u043D\u0438\u0435 \u0431\u0430\u0437\u044B";
3558
+ }
3559
+ });
3560
+
3483
3561
  // src/agentToken.ts
3484
3562
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
3485
3563
  import { join as join8 } from "path";
@@ -6234,6 +6312,10 @@ function rowToRun(row) {
6234
6312
  // вычисляемая фаза ревью (TASWM-230)
6235
6313
  review_cycle: row.review_cycle ?? 0,
6236
6314
  // старые прогоны без колонки → 0
6315
+ // TASWM-577: намерение смержить (человек отметил «пора влить»). В sqlite-бэкенде оно
6316
+ // живёт только здесь — ставить его некому, кроме самого демона, зато читает его
6317
+ // реконсайл, и колонка нужна, чтобы отметка пережила рестарт.
6318
+ merge_requested_at: row.merge_requested_at ?? null,
6237
6319
  task_title: row.task_title ?? null,
6238
6320
  task_desc: row.task_desc ?? null,
6239
6321
  // TASWM-556: когда заведена сама задача — по ней проверка дубля отличает свой
@@ -6271,6 +6353,7 @@ var init_db = __esm({
6271
6353
  init_names2();
6272
6354
  init_zoneNotes();
6273
6355
  init_storm();
6356
+ init_store();
6274
6357
  init_src();
6275
6358
  TERMINAL_SQL = TERMINAL_STATUSES.map((s) => `'${s}'`).join(",");
6276
6359
  RESOLVER_TERMINAL_SQL = [...TERMINAL_STATUSES, "stalled"].map((s) => `'${s}'`).join(",");
@@ -6308,6 +6391,7 @@ CREATE TABLE IF NOT EXISTS runs (
6308
6391
  verifies_run_id INTEGER,
6309
6392
  verify_verdict TEXT,
6310
6393
  review_cycle INTEGER NOT NULL DEFAULT 0,
6394
+ merge_requested_at TEXT,
6311
6395
  task_title TEXT,
6312
6396
  task_desc TEXT,
6313
6397
  task_created_at TEXT,
@@ -6402,6 +6486,8 @@ CREATE TABLE IF NOT EXISTS imports (
6402
6486
  "ALTER TABLE runs ADD COLUMN verify_verdict TEXT",
6403
6487
  // Авто-ревью v2 (TASWM-162): номер цикла ревью — пере-ревью после фикс-итерации.
6404
6488
  "ALTER TABLE runs ADD COLUMN review_cycle INTEGER NOT NULL DEFAULT 0",
6489
+ // TASWM-577: намерение смержить — по нему рой вливает PR сам, по очереди мержа.
6490
+ "ALTER TABLE runs ADD COLUMN merge_requested_at TEXT",
6405
6491
  // Вычисляемая фаза ревью (TASWM-230): человеко-фраза «чего ждёт PR», её считает демон.
6406
6492
  "ALTER TABLE runs ADD COLUMN review_phase TEXT",
6407
6493
  // Приоритетная очередь (TASWM-269): явный приоритет прогона, выше — раньше из queued.
@@ -6624,9 +6710,19 @@ CREATE TABLE IF NOT EXISTS imports (
6624
6710
  ).get(reviewRunId);
6625
6711
  return row ? rowToRun(row) : null;
6626
6712
  }
6627
- /** Сколько всего резолверов уже поднимали на этот review-PR — потолок авто-попыток. */
6713
+ /**
6714
+ * Сколько резолверов С ДРОНОМ уже поднимали на этот review-PR — потолок авто-попыток.
6715
+ *
6716
+ * TASWM-575: чистые вливания базы (RESOLVE_CLEAN_MARK в stop_reason — git свёл сам, дрон
6717
+ * не поднимался, стоимость ноль) в счёт НЕ идут. Потолок защищает от карусели ДРОНОВ на
6718
+ * нетривиальном конфликте (TASWM-127); бесплатное вливание не карусель, а на оживлённом
6719
+ * main оно случается на каждый чужой мерж — три сдвига main выбирали потолок, и PR с
6720
+ * отставшей базой застревал молча (гейт мержа не пускает base_stale, а вливать уже
6721
+ * некому). Всё остальное (failed/stalled резолверы, в т.ч. транзиентные сбои ОС) считается
6722
+ * как раньше — иначе потолок потерял бы смысл на органических смертях.
6723
+ */
6628
6724
  resolverCountFor(reviewRunId) {
6629
- const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE resolves_run_id = ?").get(reviewRunId);
6725
+ const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE resolves_run_id = ? AND (stop_reason IS NULL OR stop_reason NOT LIKE ?)").get(reviewRunId, `%${RESOLVE_CLEAN_MARK}%`);
6630
6726
  return row.n;
6631
6727
  }
6632
6728
  /**
@@ -7177,7 +7273,7 @@ var init_droneStore = __esm({
7177
7273
 
7178
7274
  // src/overmind.ts
7179
7275
  import { homedir as homedir7 } from "os";
7180
- import { existsSync as existsSync20, readFileSync as readFileSync25, readdirSync as readdirSync14, renameSync as renameSync12 } from "fs";
7276
+ import { existsSync as existsSync20, readFileSync as readFileSync25, readdirSync as readdirSync14, renameSync as renameSync12, statSync as statSync13 } from "fs";
7181
7277
  import { basename as basename6, dirname as dirname5, join as join34 } from "path";
7182
7278
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
7183
7279
  import { spawn as spawn4 } from "child_process";
@@ -7312,6 +7408,7 @@ function renderPendingRulesBlock(rules) {
7312
7408
 
7313
7409
  // src/swarmClient.ts
7314
7410
  init_zoneNotes();
7411
+ init_store();
7315
7412
  init_toolFailure();
7316
7413
 
7317
7414
  // src/logTail.ts
@@ -7524,6 +7621,12 @@ function depsFailureRetriable(code) {
7524
7621
  function runRef(runKey) {
7525
7622
  return { __runRef: runKey };
7526
7623
  }
7624
+ function isoOrNull(v) {
7625
+ if (v == null) return null;
7626
+ const raw = v instanceof Date ? v.toISOString() : typeof v === "string" ? v : "";
7627
+ const at = Date.parse(raw);
7628
+ return Number.isNaN(at) ? null : new Date(at).toISOString();
7629
+ }
7527
7630
  function isRunRef(v) {
7528
7631
  return typeof v === "object" && v !== null && "__runRef" in v;
7529
7632
  }
@@ -7991,6 +8094,17 @@ var SwarmClient = class _SwarmClient {
7991
8094
  runKeyOf(id) {
7992
8095
  return this.runs.get(id)?.runKey ?? null;
7993
8096
  }
8097
+ /**
8098
+ * Cuid прогона В ПРОДЕ — им адресуются прод-РУЧКИ, а не MCP-тулы (TASWM-577: демон
8099
+ * вливает PR тем же `POST /swarm/merge`, что и кнопка, и снимает намерение тем же
8100
+ * `DELETE /swarm/merge-request/:runId`). null — прогон до прода ещё не долетел
8101
+ * (создан этим демоном, флаш не прошёл): адресовать его там нечем, и вызывающий
8102
+ * обязан прочитать это как «сейчас нельзя», а не как ошибку.
8103
+ */
8104
+ prodRunId(id) {
8105
+ const key = this.runs.get(id)?.runKey;
8106
+ return key ? this.prodIdFor(key) ?? null : null;
8107
+ }
7994
8108
  runByKey(key) {
7995
8109
  const direct = [...this.runs.values()].find((r) => r.runKey === key);
7996
8110
  if (direct) return this.mat(direct);
@@ -8061,8 +8175,10 @@ var SwarmClient = class _SwarmClient {
8061
8175
  const cands = [...this.runs.values()].filter((r) => r.resolves_run_id === reviewRunId && !RESOLVER_TERMINAL.has(r.status)).sort((a, b) => b.id - a.id);
8062
8176
  return cands.length ? this.mat(cands[0]) : null;
8063
8177
  }
8178
+ // Потолок авто-попыток: чистые вливания базы (дрон не поднимался, стоимость ноль) не
8179
+ // считаются — см. db.ts:resolverCountFor и store.ts:RESOLVE_CLEAN_MARK (TASWM-575).
8064
8180
  resolverCountFor(reviewRunId) {
8065
- return [...this.runs.values()].filter((r) => r.resolves_run_id === reviewRunId).length;
8181
+ return [...this.runs.values()].filter((r) => r.resolves_run_id === reviewRunId && !isCleanResolve(r)).length;
8066
8182
  }
8067
8183
  // failed/stalled ревьюеры (сорвался спавн ДО вердикта) в счёт НЕ идут; stopped/superseded
8068
8184
  // тоже (TASWM-253: оператор закрыл зомби-ревьюера/сбросил попытки ревью — POST /swarm/
@@ -9535,6 +9651,9 @@ var SwarmClient = class _SwarmClient {
9535
9651
  verify_verdict: null,
9536
9652
  review_phase: null,
9537
9653
  // вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
9654
+ // TASWM-577: намерение смержить — прод-поле (его ставит человек ручкой
9655
+ // /swarm/merge-request), заполняется ниже из ответа.
9656
+ merge_requested_at: null,
9538
9657
  // Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
9539
9658
  // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
9540
9659
  // в отличие от respawns, — авто-ревью v2, TASWM-162.
@@ -9570,6 +9689,7 @@ var SwarmClient = class _SwarmClient {
9570
9689
  local.task_title = pr.taskTitle ?? local.task_title;
9571
9690
  local.task_desc = pr.taskDesc ?? local.task_desc;
9572
9691
  local.task_created_at = pr.taskCreatedAt ?? local.task_created_at;
9692
+ if ("mergeRequestedAt" in pr) local.merge_requested_at = isoOrNull(pr.mergeRequestedAt);
9573
9693
  local.name = pr.droneName ?? local.name;
9574
9694
  local.kind = pr.role ?? local.kind;
9575
9695
  const heroProd = pr.heroId ?? null;
@@ -9927,6 +10047,7 @@ function parseDecision(v) {
9927
10047
  }
9928
10048
 
9929
10049
  // src/overmind.ts
10050
+ init_store();
9930
10051
  init_config();
9931
10052
  init_heartbeat();
9932
10053
 
@@ -12359,7 +12480,7 @@ function parkReasonUnretriable(reason) {
12359
12480
  // package.json
12360
12481
  var package_default = {
12361
12482
  name: "@taskless-app/swarm",
12362
- version: "0.6.48",
12483
+ version: "0.6.50",
12363
12484
  type: "module",
12364
12485
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
12365
12486
  keywords: [
@@ -12410,7 +12531,7 @@ var package_default = {
12410
12531
  execa: "^10.0.0",
12411
12532
  zod: "3.25.76"
12412
12533
  },
12413
- gitHead: "62a77b0a48d5a07ae71b45c5f0f6212dc6069829"
12534
+ gitHead: "859ffe2d91bf4c791af067147515b14a41b96739"
12414
12535
  };
12415
12536
 
12416
12537
  // src/version.ts
@@ -12728,18 +12849,48 @@ async function prHeadBranch(prUrl, run = realRunner) {
12728
12849
  return null;
12729
12850
  }
12730
12851
  }
12731
- async function prBaseSync(prUrl, run = realRunner) {
12852
+ function prRepoPath(prUrl) {
12732
12853
  const m = prUrl.trim().match(/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/\d+/i);
12733
- if (!isGithubPrUrl(prUrl) || !m) return null;
12734
- const [, owner, repo] = m;
12854
+ return isGithubPrUrl(prUrl) && m ? `${m[1]}/${m[2]}` : null;
12855
+ }
12856
+ async function prBaseSync(prUrl, run = realRunner) {
12857
+ const repoPath = prRepoPath(prUrl);
12858
+ if (!repoPath) return null;
12735
12859
  try {
12736
12860
  const j = JSON.parse(await run(["pr", "view", prUrl, "--json", "baseRefName,headRefOid"]));
12737
12861
  const base3 = j.baseRefName;
12738
12862
  const head = j.headRefOid;
12739
12863
  if (!base3 || !head) return null;
12740
- const out = await run(["api", `repos/${owner}/${repo}/compare/${base3}...${head}?per_page=1`, "--jq", ".behind_by"]);
12864
+ const out = await run(["api", `repos/${repoPath}/compare/${base3}...${head}?per_page=1`, "--jq", ".behind_by"]);
12741
12865
  const behindBy = Number(out.trim());
12742
- return Number.isFinite(behindBy) && behindBy >= 0 ? { behindBy, base: base3 } : null;
12866
+ return Number.isFinite(behindBy) && behindBy >= 0 ? { behindBy, base: base3, headSha: head } : null;
12867
+ } catch {
12868
+ return null;
12869
+ }
12870
+ }
12871
+ var ACTIONS_RUN_OK = /* @__PURE__ */ new Set(["success", "skipped", "neutral"]);
12872
+ async function prActionsRuns(prUrl, headSha2, run = realRunner) {
12873
+ const repoPath = prRepoPath(prUrl);
12874
+ if (!repoPath || !/^[0-9a-f]{7,40}$/i.test(headSha2)) return null;
12875
+ try {
12876
+ const j = JSON.parse(
12877
+ await run(["api", `repos/${repoPath}/actions/runs?head_sha=${headSha2}&per_page=100&exclude_pull_requests=true`])
12878
+ );
12879
+ const runs = j.workflow_runs;
12880
+ if (!Array.isArray(runs)) return null;
12881
+ if (typeof j.total_count === "number" && j.total_count > runs.length) return null;
12882
+ const latest = /* @__PURE__ */ new Map();
12883
+ for (const r of runs) {
12884
+ const key = `${r.workflow_id ?? r.name ?? "?"}:${r.event ?? "?"}`;
12885
+ const seen = latest.get(key);
12886
+ if (!seen || (r.id ?? 0) > (seen.id ?? 0)) latest.set(key, r);
12887
+ }
12888
+ const label2 = (r, what) => `${r.name || "workflow"} (${what || "?"})`;
12889
+ return {
12890
+ total: runs.length,
12891
+ active: runs.filter((r) => r.status !== "completed").map((r) => label2(r, r.status)),
12892
+ failed: [...latest.values()].filter((r) => r.status === "completed" && !ACTIONS_RUN_OK.has(r.conclusion ?? "")).map((r) => label2(r, r.conclusion))
12893
+ };
12743
12894
  } catch {
12744
12895
  return null;
12745
12896
  }
@@ -15303,22 +15454,10 @@ function mergeQueueCandidates(runs, repo, repoFallback) {
15303
15454
  }
15304
15455
  var BASE_TTL_MS = 2e4;
15305
15456
  function makeMergeQueueResolver(deps) {
15306
- const memo = /* @__PURE__ */ new Map();
15457
+ const read = deps.headState ?? makeHeadStateReader(deps);
15307
15458
  const evaluate = async (e, prUrl, ready) => {
15308
- const now = Date.now();
15309
- let cached = memo.get(prUrl);
15310
- const ttl = deps.ttlMs ?? BASE_TTL_MS;
15311
- if (!cached || now - cached.at >= ttl) {
15312
- const base3 = await deps.baseSync(prUrl).catch(() => null);
15313
- let checks = "none";
15314
- if (base3?.behindBy === 0 && deps.checks) {
15315
- const ci = await deps.checks(prUrl).catch(() => null);
15316
- checks = ci ? ci.bucket : null;
15317
- }
15318
- cached = { at: now, base: base3, checks };
15319
- memo.set(prUrl, cached);
15320
- }
15321
- return computeMergeQueue({ entry: e, ready, behindBy: cached.base ? cached.base.behindBy : null, checks: cached.checks });
15459
+ const st = await read(prUrl);
15460
+ return computeMergeQueue({ entry: e, ready, behindBy: st.behindBy, checks: st.checks });
15322
15461
  };
15323
15462
  return async (run) => {
15324
15463
  if (!deps.baseSync || !run.pr_url) return null;
@@ -15353,6 +15492,124 @@ function makeMergeQueueResolver(deps) {
15353
15492
  function entryOf(r) {
15354
15493
  return { runId: r.id, prNumber: prNumberOf(r.pr_url), slug: r.task_slug };
15355
15494
  }
15495
+ function makeHeadStateReader(deps) {
15496
+ const memo = /* @__PURE__ */ new Map();
15497
+ return async (prUrl) => {
15498
+ const now = Date.now();
15499
+ const ttl = deps.ttlMs ?? BASE_TTL_MS;
15500
+ const cached = memo.get(prUrl);
15501
+ if (cached && now - cached.at < ttl) return { behindBy: cached.behindBy, checks: cached.checks, headSha: cached.headSha };
15502
+ const base3 = deps.baseSync ? await deps.baseSync(prUrl).catch(() => null) : null;
15503
+ let checks = "none";
15504
+ if (base3?.behindBy === 0 && deps.checks) {
15505
+ const ci = await deps.checks(prUrl).catch(() => null);
15506
+ checks = ci ? ci.bucket : null;
15507
+ }
15508
+ const fresh = { behindBy: base3 ? base3.behindBy : null, checks, headSha: base3?.headSha ?? null };
15509
+ memo.set(prUrl, { at: now, ...fresh });
15510
+ return fresh;
15511
+ };
15512
+ }
15513
+ function hasMergeIntent(r) {
15514
+ return !!(r.merge_requested_at ?? "").trim();
15515
+ }
15516
+ var NO_CI_GRACE_MS = 10 * 6e4;
15517
+ var shortSha = (sha) => sha.slice(0, 7);
15518
+ function makeMergeIntentResolver(deps) {
15519
+ const read = deps.headState ?? makeHeadStateReader(deps);
15520
+ const firstSeen = /* @__PURE__ */ new Map();
15521
+ const ciSettled = async (prUrl, st, now) => {
15522
+ const sha = st.headSha;
15523
+ if (!sha) {
15524
+ return { action: "wait", code: "checks_unknown", reason: "gh \u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043B HEAD PR \u2014 \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C, \u0434\u043E\u0431\u0435\u0436\u0430\u043B \u043B\u0438 \u043D\u0430 \u043D\u0451\u043C CI. \u0421\u043F\u0440\u043E\u0448\u0443 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C" };
15525
+ }
15526
+ if (!deps.actionsRuns) {
15527
+ return { action: "wait", code: "checks_unknown", reason: "\u0440\u043E\u044E \u043D\u0435\u0447\u0435\u043C \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D\u044B GitHub Actions \u043D\u0430 HEAD PR \u2014 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 \u0430\u0433\u0440\u0435\u0433\u0430\u0442\u0443 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u0432\u0441\u043B\u0435\u043F\u0443\u044E \u043D\u0435 \u0432\u043B\u0438\u0432\u0430\u044E" };
15528
+ }
15529
+ const runs = await deps.actionsRuns(prUrl, sha).catch(() => null);
15530
+ if (!runs) {
15531
+ return { action: "wait", code: "checks_unknown", reason: `\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D\u044B GitHub Actions \u043D\u0430 ${shortSha(sha)} \u2014 \u0441\u043F\u0440\u043E\u0448\u0443 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C` };
15532
+ }
15533
+ if (runs.failed.length > 0) {
15534
+ return { action: "clear", code: "checks_red", reason: `\u043D\u0430 ${shortSha(sha)} \u0443\u043F\u0430\u043B \u043F\u0440\u043E\u0433\u043E\u043D GitHub Actions: ${runs.failed.join(", ")} \u2014 \u0432\u043B\u0438\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E, \u043D\u0443\u0436\u043D\u0430 \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0430` };
15535
+ }
15536
+ if (runs.active.length > 0) {
15537
+ return {
15538
+ action: "wait",
15539
+ code: "checks_pending",
15540
+ reason: `\u043D\u0430 ${shortSha(sha)} \u0435\u0449\u0451 \u0438\u0434\u0443\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u044B GitHub Actions: ${runs.active.join(", ")} \u2014 \u0432\u0438\u0434\u0438\u043C\u044B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043C\u043E\u0433\u0443\u0442 \u0431\u044B\u0442\u044C \u0437\u0435\u043B\u0451\u043D\u044B\u043C\u0438, \u043F\u043E\u043A\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0430\u044F \u0432\u043E\u043B\u043D\u0430 \u0434\u0436\u043E\u0431 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430`
15541
+ };
15542
+ }
15543
+ if (st.checks === "none") {
15544
+ if (runs.total > 0) {
15545
+ return { action: "wait", code: "checks_pending", reason: `\u043F\u0440\u043E\u0433\u043E\u043D\u044B GitHub Actions \u043D\u0430 ${shortSha(sha)} \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0438\u0441\u044C, \u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u0443 PR \u0435\u0449\u0451 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E \u2014 \u0436\u0434\u0443, \u043F\u043E\u043A\u0430 GitHub \u0438\u0445 \u043F\u043E\u043A\u0430\u0436\u0435\u0442` };
15546
+ }
15547
+ const seen = firstSeen.get(prUrl);
15548
+ const waited = seen && seen.sha === sha ? now - seen.at : 0;
15549
+ const graceMin = NO_CI_GRACE_MS / 6e4;
15550
+ if (waited < NO_CI_GRACE_MS) {
15551
+ return {
15552
+ action: "wait",
15553
+ code: "checks_pending",
15554
+ reason: `\u043D\u0430 ${shortSha(sha)} \u043F\u043E\u043A\u0430 \u043D\u0438 \u043E\u0434\u043D\u043E\u0439 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0438 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 GitHub Actions \u2014 CI \u043C\u043E\u0433 \u0435\u0449\u0451 \u043D\u0435 \u0437\u0430\u0432\u0435\u0441\u0442\u0438 \u0434\u0436\u043E\u0431\u044B \u043F\u043E\u0441\u043B\u0435 \u043F\u0443\u0448\u0430. \u0416\u0434\u0443 \u0434\u043E ${graceMin} \u043C\u0438\u043D (\u043F\u0440\u043E\u0448\u043B\u043E ${Math.floor(waited / 6e4)})`
15555
+ };
15556
+ }
15557
+ return { action: "merge", code: "queue_head", reason: `\u0435\u0433\u043E \u0447\u0435\u0440\u0451\u0434 \u0438 \u0431\u0430\u0437\u0430 \u0441\u0432\u0435\u0436\u0430\u044F, \u0430 ${shortSha(sha)} ${graceMin} \u043C\u0438\u043D \u043F\u0440\u043E\u0441\u0442\u043E\u044F\u043B \u0431\u0435\u0437 \u0435\u0434\u0438\u043D\u043E\u0439 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0438 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 GitHub Actions \u2014 CI \u0443 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u043D\u0435\u0442` };
15558
+ }
15559
+ return { action: "merge", code: "queue_head", reason: `\u0435\u0433\u043E \u0447\u0435\u0440\u0451\u0434, \u0431\u0430\u0437\u0430 \u0441\u0432\u0435\u0436\u0430\u044F, \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043D\u0430 ${shortSha(sha)} \u0437\u0435\u043B\u0451\u043D\u044B\u0435 \u0438 \u0432\u0441\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B GitHub Actions \u043D\u0430 \u043D\u0451\u043C \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u044B` };
15560
+ };
15561
+ const confirmLive = async (prUrl, sha, verdict) => {
15562
+ if (!sha) {
15563
+ return { action: "wait", code: "checks_unknown", reason: "gh \u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043B HEAD PR \u2014 \u043D\u0435 \u043F\u043E\u043D\u044F\u0442\u044C, \u043F\u0440\u043E \u043A\u0430\u043A\u043E\u0439 \u043A\u043E\u043C\u043C\u0438\u0442 \u044D\u0442\u043E\u0442 \u0432\u0435\u0440\u0434\u0438\u043A\u0442; \u0441\u043F\u0440\u043E\u0448\u0443 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C" };
15564
+ }
15565
+ const live = deps.baseSync ? await deps.baseSync(prUrl).catch(() => null) : null;
15566
+ if (!live || !live.headSha) {
15567
+ return { action: "wait", code: "base_unknown", reason: "\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0435\u0440\u0435\u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u0436\u0438\u0432\u043E\u0439 HEAD PR \u2014 \u0441\u043F\u0440\u043E\u0448\u0443 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C" };
15568
+ }
15569
+ if (verdict.action === "merge" && live.behindBy > 0) {
15570
+ return {
15571
+ action: "refresh_base",
15572
+ code: "base_stale",
15573
+ reason: `\u0431\u0430\u0437\u0430 \u0443\u0448\u043B\u0430 \u0432\u043F\u0435\u0440\u0451\u0434 \u043D\u0430 ${live.behindBy} \u043A\u043E\u043C\u043C\u0438\u0442(\u043E\u0432) \u043F\u043E\u0441\u043B\u0435 \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0433\u043E \u043C\u0435\u0440\u0436\u0430 \u2014 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u043B\u0438\u0442\u044C main \u0438 \u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F CI \u043D\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D\u043D\u043E\u0439 \u0431\u0430\u0437\u0435`
15574
+ };
15575
+ }
15576
+ if (live.headSha !== sha) {
15577
+ return { action: "wait", code: "checks_pending", reason: `\u043D\u0430 PR \u043D\u043E\u0432\u044B\u0439 \u043A\u043E\u043C\u043C\u0438\u0442 ${shortSha(live.headSha)} \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044E \u0437\u0430\u043D\u043E\u0432\u043E` };
15578
+ }
15579
+ return verdict;
15580
+ };
15581
+ return async (run) => {
15582
+ if (!deps.baseSync || !run.pr_url || !hasMergeIntent(run)) return null;
15583
+ const num = prNumberOf(run.pr_url);
15584
+ if (num == null) return null;
15585
+ const marked = mergeQueueCandidates(deps.listRuns(), effectiveRepoOf(run.repo, deps.repoFallback), deps.repoFallback).filter(hasMergeIntent);
15586
+ const mine = marked.find((c) => prNumberOf(c.pr_url) === num);
15587
+ if (!mine) return null;
15588
+ const queue = orderMergeQueue(marked.map(entryOf));
15589
+ const entry = entryOf(mine);
15590
+ const position = queue.findIndex((e) => e.runId === entry.runId) + 1;
15591
+ const head = queue[0];
15592
+ if (head.runId !== entry.runId) {
15593
+ return {
15594
+ action: "wait",
15595
+ code: "queue_wait",
15596
+ reason: `\u0440\u043E\u0439 \u0432\u043B\u0438\u0432\u0430\u0435\u0442 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443: \u0441\u0435\u0439\u0447\u0430\u0441 \u043E\u0447\u0435\u0440\u0435\u0434\u044C PR #${head.prNumber}${head.slug ? ` (${head.slug})` : ""}, \u044D\u0442\u043E\u0442 \u2014 ${position}-\u0439 \u0438\u0437 ${queue.length}`,
15597
+ position,
15598
+ size: queue.length
15599
+ };
15600
+ }
15601
+ const st = await read(mine.pr_url);
15602
+ const now = deps.now ? deps.now() : Date.now();
15603
+ if (st.headSha) {
15604
+ const seen = firstSeen.get(mine.pr_url);
15605
+ if (!seen || seen.sha !== st.headSha) firstSeen.set(mine.pr_url, { sha: st.headSha, at: now });
15606
+ }
15607
+ const d = computeMergeQueue({ entry, ready: queue, behindBy: st.behindBy, checks: st.checks });
15608
+ const verdict = d.code === "queue_head" ? await ciSettled(mine.pr_url, st, now) : { action: d.code === "base_stale" ? "refresh_base" : d.code === "checks_red" ? "clear" : "wait", code: d.code, reason: d.reason };
15609
+ const final = verdict.action === "merge" || verdict.action === "clear" ? await confirmLive(mine.pr_url, st.headSha, verdict) : verdict;
15610
+ return { ...final, position, size: queue.length, ...st.headSha ? { headSha: st.headSha } : {} };
15611
+ };
15612
+ }
15356
15613
  function needsBaseRefresh(d) {
15357
15614
  return d?.code === "base_stale";
15358
15615
  }
@@ -15384,9 +15641,15 @@ function makeMergeGateParts(deps) {
15384
15641
  });
15385
15642
  };
15386
15643
  const repoOfRun = deps.repoFullNameOf ? (r) => deps.repoFullNameOf(effectiveRepoOf(r.repo, deps.repoFallback)) : void 0;
15644
+ const headState = makeHeadStateReader({
15645
+ ...deps.baseSync ? { baseSync: deps.baseSync } : {},
15646
+ ...deps.checks ? { checks: deps.checks } : {},
15647
+ ...deps.ttlMs != null ? { ttlMs: deps.ttlMs } : {}
15648
+ });
15387
15649
  const queueFor = makeMergeQueueResolver({
15388
15650
  listRuns: deps.listRuns,
15389
15651
  gateAllows: async (r) => (await gateFor(r)).allowed,
15652
+ headState,
15390
15653
  ...deps.repoFallback ? { repoFallback: deps.repoFallback } : {},
15391
15654
  ...deps.baseSync ? { baseSync: deps.baseSync } : {},
15392
15655
  ...deps.checks ? { checks: deps.checks } : {},
@@ -15412,7 +15675,68 @@ function makeMergeGateParts(deps) {
15412
15675
  }
15413
15676
  };
15414
15677
  };
15415
- return { gate, queue: queueFor };
15678
+ const intentFor = makeMergeIntentResolver({
15679
+ listRuns: deps.listRuns,
15680
+ headState,
15681
+ ...deps.repoFallback ? { repoFallback: deps.repoFallback } : {},
15682
+ ...deps.baseSync ? { baseSync: deps.baseSync } : {},
15683
+ ...deps.checks ? { checks: deps.checks } : {},
15684
+ ...deps.actionsRuns ? { actionsRuns: deps.actionsRuns } : {},
15685
+ ...deps.ttlMs != null ? { ttlMs: deps.ttlMs } : {}
15686
+ });
15687
+ return { gate, queue: queueFor, intent: intentFor };
15688
+ }
15689
+
15690
+ // src/prodMerge.ts
15691
+ init_config();
15692
+ init_taskless();
15693
+ var TIMEOUT_MS = 3e4;
15694
+ async function tokenFor(api, deps) {
15695
+ return deps.token ? deps.token() : resolveApiToken(api);
15696
+ }
15697
+ async function prodMergePr(runId, sha, deps = {}) {
15698
+ const api = deps.api ?? TASKLESS_API;
15699
+ const doFetch = deps.fetchFn ?? fetch;
15700
+ try {
15701
+ const token = await tokenFor(api, deps);
15702
+ if (!token) return { ok: false, error: "\u043D\u0435\u0447\u0435\u043C \u0445\u043E\u0434\u0438\u0442\u044C \u0432 \u043F\u0440\u043E\u0434 (\u043D\u0435\u0442 \u0442\u043E\u043A\u0435\u043D\u0430)" };
15703
+ const res = await doFetch(restEndpoint(api, "/swarm/merge"), {
15704
+ method: "POST",
15705
+ headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
15706
+ body: JSON.stringify(sha ? { runId, sha } : { runId }),
15707
+ signal: AbortSignal.timeout(TIMEOUT_MS)
15708
+ });
15709
+ if (!res.ok) {
15710
+ const text = await res.text().catch(() => "");
15711
+ let code;
15712
+ try {
15713
+ code = JSON.parse(text).error;
15714
+ } catch {
15715
+ code = void 0;
15716
+ }
15717
+ if (code === "head_moved") return { ok: false, error: "HEAD PR \u0441\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438", headMoved: true };
15718
+ return { ok: false, error: `\u043F\u0440\u043E\u0434 \u043E\u0442\u043A\u0430\u0437\u0430\u043B (${res.status}): ${text.slice(0, 300)}` };
15719
+ }
15720
+ return { ok: true };
15721
+ } catch (e) {
15722
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
15723
+ }
15724
+ }
15725
+ async function prodClearMergeIntent(runId, deps = {}) {
15726
+ const api = deps.api ?? TASKLESS_API;
15727
+ const doFetch = deps.fetchFn ?? fetch;
15728
+ try {
15729
+ const token = await tokenFor(api, deps);
15730
+ if (!token) return false;
15731
+ const res = await doFetch(restEndpoint(api, `/swarm/merge-request/${encodeURIComponent(runId)}`), {
15732
+ method: "DELETE",
15733
+ headers: { Authorization: `Bearer ${token}` },
15734
+ signal: AbortSignal.timeout(TIMEOUT_MS)
15735
+ });
15736
+ return res.ok;
15737
+ } catch {
15738
+ return false;
15739
+ }
15416
15740
  }
15417
15741
 
15418
15742
  // src/envGate.ts
@@ -18000,8 +18324,8 @@ async function headSha(repo) {
18000
18324
  function runText(run) {
18001
18325
  return [run.task_title ?? "", run.task_desc ?? ""].join(" ").trim();
18002
18326
  }
18003
- function predictForRun(model, run) {
18004
- return predictFiles(model, { zone: run.area, text: runText(run) });
18327
+ function predictForRun(model, run, fileExists) {
18328
+ return predictFiles(model, { zone: run.area, text: runText(run), ...fileExists ? { fileExists } : {} });
18005
18329
  }
18006
18330
  function short(files, n = 3) {
18007
18331
  return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
@@ -18016,17 +18340,17 @@ var SPINE_PREDICTION_WEIGHT = 0.7;
18016
18340
  function spineOfPrediction(spine, predicted) {
18017
18341
  return predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
18018
18342
  }
18019
- function decideConflict({ model, candidate, active }) {
18343
+ function decideConflict({ model, candidate, active, fileExists }) {
18020
18344
  if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
18021
18345
  if (!hasZoneModel(model)) return { kind: "unknown" };
18022
- const predicted = predictForRun(model, candidate);
18346
+ const predicted = predictForRun(model, candidate, fileExists);
18023
18347
  if (!predicted.length) return { kind: "unknown" };
18024
18348
  if (!active.length) return { kind: "clear" };
18025
18349
  const spine = new Set(model.spine);
18026
18350
  const candidateSpine = new Set(spineOfPrediction(spine, predicted));
18027
18351
  if (candidateSpine.size) {
18028
18352
  for (const w of active) {
18029
- const theirSpine = w.actual ? w.files.filter((f) => spine.has(f)) : spineOfPrediction(spine, predictForRun(model, w.run));
18353
+ const theirSpine = w.actual ? w.files.filter((f) => spine.has(f)) : spineOfPrediction(spine, predictForRun(model, w.run, fileExists));
18030
18354
  const shared = theirSpine.filter((f) => candidateSpine.has(f));
18031
18355
  if (!shared.length) continue;
18032
18356
  return {
@@ -18150,6 +18474,8 @@ var EXITED_CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u043E\u0448\u
18150
18474
  var STORM_DEAD_LIMIT = Number(process.env.SWARM_STORM_DEAD_LIMIT) || 3;
18151
18475
  var CONFLICT_RESOLVE_COOLDOWN_MS = Number(process.env.SWARM_CONFLICT_COOLDOWN_MS) || 3 * 60 * 1e3;
18152
18476
  var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
18477
+ var BASE_MERGED_MARK = "\u0431\u0430\u0437\u0430 \u0432\u043B\u0438\u0442\u0430 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438";
18478
+ var BASE_NOOP_MARK = "\u0432\u043B\u0438\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E";
18153
18479
  var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
18154
18480
  var REVIEW_CYCLE_LIMIT = Number(process.env.SWARM_REVIEW_CYCLE_LIMIT) || 3;
18155
18481
  var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
@@ -18231,14 +18557,46 @@ function lastConflictAttemptMs(db, runId) {
18231
18557
  if (!evs.length) return null;
18232
18558
  return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
18233
18559
  }
18560
+ function lastBaseMergeWasNoop(db, runId) {
18561
+ const outcomes = db.events(runId).filter((e) => e.kind === "conflict" && ((e.text ?? "").includes(BASE_NOOP_MARK) || (e.text ?? "").includes(BASE_MERGED_MARK)));
18562
+ return (outcomes[outcomes.length - 1]?.text ?? "").includes(BASE_NOOP_MARK);
18563
+ }
18564
+ function resolveEscalated(db, runId) {
18565
+ return db.events(runId).some((e) => e.kind === "conflict" && (e.text ?? "").includes(RESOLVE_ESCALATION_MARK));
18566
+ }
18234
18567
  async function needsBaseRefreshNow(deps, run, stormActive, now) {
18235
18568
  if (!deps.mergeQueue || !deps.resolveConflict || stormActive) return false;
18236
18569
  if (deps.db.activeResolverFor(run.id)) return false;
18237
- if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) return false;
18570
+ if (resolveEscalated(deps.db, run.id)) return false;
18238
18571
  const last = lastConflictAttemptMs(deps.db, run.id);
18239
18572
  if (last != null && now - last < CONFLICT_RESOLVE_COOLDOWN_MS) return false;
18240
18573
  return needsBaseRefresh(await deps.mergeQueue(run).catch(() => null));
18241
18574
  }
18575
+ async function dropMergeIntent(deps, run, reason) {
18576
+ if (!deps.clearMergeIntent) return false;
18577
+ if (!await deps.clearMergeIntent(run).catch(() => false)) return false;
18578
+ deps.db.updateRun(run.id, { merge_requested_at: null });
18579
+ deps.db.addEvent(run.id, "step", `\u043C\u0435\u0440\u0436 \u0441\u043D\u044F\u0442: ${reason}`);
18580
+ narrate(deps, `\u0421\u043D\u044F\u043B \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u0438\u0435 \u0441\u043C\u0435\u0440\u0436\u0438\u0442\u044C PR ${droneLabel(run)} \u2014 ${reason}.`);
18581
+ return true;
18582
+ }
18583
+ function mergeIntentDropReason(deps, run, conflicting) {
18584
+ if (needsFixIteration(run)) return "\u043F\u043E PR \u043F\u0440\u0438\u0448\u0451\u043B \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB \u2014 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0430";
18585
+ if (conflicting && resolveEscalated(deps.db, run.id)) return "\u0440\u043E\u0439 \u043D\u0435 \u0441\u043C\u043E\u0433 \u0441\u0432\u0435\u0441\u0442\u0438 \u0432\u0435\u0442\u043A\u0443 \u0441 \u0431\u0430\u0437\u043E\u0439 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A";
18586
+ return null;
18587
+ }
18588
+ var MERGE_INTENT_FAIL_MARK = "\u043C\u0435\u0440\u0436 \u043F\u043E \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u0438\u044E \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B";
18589
+ var MERGE_INTENT_FAIL_NOTE_MS = 60 * 6e4;
18590
+ function noteMergeIntentFailure(deps, run, error, now) {
18591
+ const text = `${MERGE_INTENT_FAIL_MARK}: ${error} \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C`;
18592
+ const prev = deps.db.events(run.id).filter((e) => (e.text ?? "").startsWith(MERGE_INTENT_FAIL_MARK));
18593
+ const last = prev[prev.length - 1];
18594
+ if (last && last.text === text) {
18595
+ const at = Date.parse(last.at.replace(" ", "T") + "Z");
18596
+ if (!Number.isNaN(at) && now - at < MERGE_INTENT_FAIL_NOTE_MS) return;
18597
+ }
18598
+ deps.db.addEvent(run.id, "step", text);
18599
+ }
18242
18600
  function lastCiCheckMs(db, runId) {
18243
18601
  const evs = db.events(runId).filter((e) => e.kind === "ci_wake" || e.kind === "ci_checked");
18244
18602
  if (!evs.length) return null;
@@ -19669,6 +20027,13 @@ async function zoneConflictFor(db, models, activeFiles, run, repo, repoFallback)
19669
20027
  if (!hasZoneModel(model)) return { kind: "unknown" };
19670
20028
  const holders = db.listRuns().filter((r) => holdsFiles(r));
19671
20029
  activeFiles.prune(holders.map((r) => r.id));
20030
+ const fileExists = (p) => {
20031
+ try {
20032
+ return statSync13(join34(repo, p), { throwIfNoEntry: false })?.isFile() ?? false;
20033
+ } catch {
20034
+ return false;
20035
+ }
20036
+ };
19672
20037
  const active = [];
19673
20038
  for (const r of holders) {
19674
20039
  if (r.id === run.id) continue;
@@ -19677,10 +20042,10 @@ async function zoneConflictFor(db, models, activeFiles, run, repo, repoFallback)
19677
20042
  const files = snapshot.length ? snapshot : await activeFiles.filesOf(r);
19678
20043
  const prUrl = r.status === "review" ? r.pr_url : null;
19679
20044
  active.push(
19680
- files.length ? { run: r, files, actual: true, prUrl } : { run: r, files: predictForRun(model, r).map((p) => p.path), actual: false, prUrl }
20045
+ files.length ? { run: r, files, actual: true, prUrl } : { run: r, files: heldByPrediction(predictForRun(model, r, fileExists)), actual: false, prUrl }
19681
20046
  );
19682
20047
  }
19683
- return decideConflict({ model, candidate: run, active });
20048
+ return decideConflict({ model, candidate: run, active, fileExists });
19684
20049
  }
19685
20050
  function activeDronesOnRepo(deps, repo, exceptRunId) {
19686
20051
  return deps.db.listRuns().filter(
@@ -20868,6 +21233,7 @@ async function tickOnce(deps) {
20868
21233
  continue;
20869
21234
  }
20870
21235
  if (prLiveness(st) === "closed") {
21236
+ if (hasMergeIntent(run)) await dropMergeIntent(deps, run, "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430");
20871
21237
  await finalizeClosedPrReview(deps, run, "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E, \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442");
20872
21238
  narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 ${run.task_slug} \u0441\u043D\u0438\u043C\u0430\u0435\u0442\u0441\u044F \u0441 \u0440\u0435\u0432\u044C\u044E, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
20873
21239
  continue;
@@ -20897,18 +21263,62 @@ async function tickOnce(deps) {
20897
21263
  continue;
20898
21264
  }
20899
21265
  await retireOutdatedReviewers(deps, run);
20900
- const baseStale = !st.conflicting && await needsBaseRefreshNow(deps, run, stormActive, now);
21266
+ let intentBaseStale = false;
21267
+ if (hasMergeIntent(run)) {
21268
+ const drop = mergeIntentDropReason(deps, run, st.conflicting === true);
21269
+ if (drop) {
21270
+ if (await dropMergeIntent(deps, run, drop)) run = deps.db.getRun(run.id) ?? run;
21271
+ } else if (deps.mergeIntent && !stormActive) {
21272
+ const decision = await deps.mergeIntent(run).catch(() => null);
21273
+ if (decision?.action === "clear") {
21274
+ if (await dropMergeIntent(deps, run, decision.reason)) run = deps.db.getRun(run.id) ?? run;
21275
+ } else if (decision?.action === "refresh_base") {
21276
+ if (resolveEscalated(deps.db, run.id)) {
21277
+ if (await dropMergeIntent(deps, run, "\u0440\u043E\u0439 \u043D\u0435 \u0441\u043C\u043E\u0433 \u043F\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u0432\u0435\u0442\u043A\u0443 \u043A \u0441\u0432\u0435\u0436\u0435\u0439 \u0431\u0430\u0437\u0435 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A")) run = deps.db.getRun(run.id) ?? run;
21278
+ } else {
21279
+ intentBaseStale = true;
21280
+ }
21281
+ } else if (decision?.action === "merge" && deps.mergePr && !st.conflicting) {
21282
+ const res = await deps.mergePr(run, decision.headSha).catch((e) => ({ ok: false, error: describeError(e) }));
21283
+ if (res.ok) {
21284
+ deps.db.addEvent(run.id, "step", `\u0440\u043E\u0439 \u0432\u043B\u0438\u043B PR \u043F\u043E \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u043C\u0435\u0440\u0436\u0430: ${decision.reason} (TASWM-577)`);
21285
+ narrate(deps, `\u0412\u043B\u0438\u043B PR ${droneLabel(run)} \u043F\u043E \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u043C\u0435\u0440\u0436\u0430: ${decision.reason}.`);
21286
+ continue;
21287
+ }
21288
+ if ("headMoved" in res && res.headMoved) {
21289
+ deps.db.addEvent(run.id, "step", `\u043C\u0435\u0440\u0436 \u043F\u043E \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u0438\u044E \u043E\u0442\u043B\u043E\u0436\u0435\u043D: HEAD PR \u0441\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 ${(decision.headSha ?? "").slice(0, 7)} \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044E \u043D\u043E\u0432\u044B\u0439 \u043A\u043E\u043C\u043C\u0438\u0442 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C (TASWM-580)`);
21290
+ } else {
21291
+ noteMergeIntentFailure(deps, run, res.error, now);
21292
+ }
21293
+ }
21294
+ }
21295
+ }
21296
+ const baseStale = !st.conflicting && (intentBaseStale || await needsBaseRefreshNow(deps, run, stormActive, now));
20901
21297
  if ((st.conflicting || baseStale) && deps.resolveConflict && !stormActive) {
20902
- if (deps.resourcesOk && !deps.resourcesOk()) continue;
20903
- if (countActiveDrones(deps.db) >= droneCap) continue;
20904
- if (!await projectQuotaGateAllows(deps, run)) continue;
20905
21298
  if (deps.db.activeResolverFor(run.id)) continue;
20906
21299
  if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) {
20907
- if (!deps.db.events(run.id).some((e) => e.kind === "conflict" && (e.text ?? "").includes(RESOLVE_ESCALATION_MARK))) {
20908
- deps.db.addEvent(run.id, "conflict", `${RESOLVE_ESCALATION_MARK}: \u0430\u0432\u0442\u043E-\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043D\u0435 \u0441\u043F\u0440\u0430\u0432\u0438\u043B\u0441\u044F \u0437\u0430 ${RESOLVE_LIMIT} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
21300
+ if (!resolveEscalated(deps.db, run.id)) {
21301
+ deps.db.addEvent(
21302
+ run.id,
21303
+ "conflict",
21304
+ st.conflicting ? `${RESOLVE_ESCALATION_MARK}: \u0430\u0432\u0442\u043E-\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043D\u0435 \u0441\u043F\u0440\u0430\u0432\u0438\u043B\u0441\u044F \u0437\u0430 ${RESOLVE_LIMIT} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A` : `${RESOLVE_ESCALATION_MARK}: \u0431\u0430\u0437\u0430 \u043F\u043E\u0434 PR \u0443\u0435\u0445\u0430\u043B\u0430 \u0432\u043F\u0435\u0440\u0451\u0434, \u0430 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0430\u0432\u0442\u043E-\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440\u043E\u0432 (${RESOLVE_LIMIT}) \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u2014 \u0432\u043B\u0438\u0442\u044C \u0431\u0430\u0437\u0443 \u0434\u043E\u043B\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`
21305
+ );
20909
21306
  }
20910
21307
  continue;
20911
21308
  }
21309
+ if (baseStale && lastBaseMergeWasNoop(deps.db, run.id)) {
21310
+ if (!resolveEscalated(deps.db, run.id)) {
21311
+ deps.db.addEvent(
21312
+ run.id,
21313
+ "conflict",
21314
+ `${RESOLVE_ESCALATION_MARK}: \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043C\u0435\u0440\u0436\u0430 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u0431\u0430\u0437\u0443 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0435\u0439, \u0430 \u0432\u043B\u0438\u0432\u0430\u0442\u044C \u0432 \u0432\u0435\u0442\u043A\u0443 \u043D\u0435\u0447\u0435\u0433\u043E \u2014 \u0441\u0430\u043C\u0430 \u0441\u043E\u0431\u043E\u0439 \u0442\u0430\u043A\u0430\u044F \u0431\u0430\u0437\u0430 \u043D\u0435 \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`
21315
+ );
21316
+ }
21317
+ continue;
21318
+ }
21319
+ if (deps.resourcesOk && !deps.resourcesOk()) continue;
21320
+ if (countActiveDrones(deps.db) >= droneCap) continue;
21321
+ if (!await projectQuotaGateAllows(deps, run)) continue;
20912
21322
  const last = lastConflictAttemptMs(deps.db, run.id);
20913
21323
  if (last != null && now - last < CONFLICT_RESOLVE_COOLDOWN_MS) continue;
20914
21324
  deps.db.addEvent(
@@ -21514,13 +21924,14 @@ async function resolveConflict(deps, reviewRun) {
21514
21924
  if (m.changed) await deps.git.pushHeadTo(wt, branch);
21515
21925
  await deps.git.removeWorktree(repo, wt).catch(() => {
21516
21926
  });
21517
- deps.db.updateRun(resolver.id, { status: "done" });
21927
+ const cleanWhy = m.changed ? `\u0447\u0438\u0441\u0442\u044B\u0439 merge ${mainRef} \u2014 \u0434\u0440\u043E\u043D \u043D\u0435 \u043F\u043E\u043D\u0430\u0434\u043E\u0431\u0438\u043B\u0441\u044F` : `${mainRef} \u0443\u0436\u0435 \u0432\u043B\u0438\u0442 \u2014 \u043D\u0435\u0447\u0435\u0433\u043E \u0434\u0435\u043B\u0430\u0442\u044C`;
21928
+ deps.db.updateRun(resolver.id, { status: "done", stop_reason: `${RESOLVE_CLEAN_MARK}: ${cleanWhy}` });
21929
+ deps.db.addEvent(resolver.id, "done", cleanWhy);
21518
21930
  deps.db.addEvent(
21519
- resolver.id,
21520
- "done",
21521
- m.changed ? `\u0447\u0438\u0441\u0442\u044B\u0439 merge ${mainRef} \u2014 \u0434\u0440\u043E\u043D \u043D\u0435 \u043F\u043E\u043D\u0430\u0434\u043E\u0431\u0438\u043B\u0441\u044F` : `${mainRef} \u0443\u0436\u0435 \u0432\u043B\u0438\u0442 \u2014 \u043D\u0435\u0447\u0435\u0433\u043E \u0434\u0435\u043B\u0430\u0442\u044C`
21931
+ reviewRun.id,
21932
+ "conflict",
21933
+ m.changed ? `${BASE_MERGED_MARK} (${mainRef}, \u0431\u0435\u0437 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u043E\u0432) \u2014 PR \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D` : `${mainRef} \u0443\u0436\u0435 \u0432 \u0432\u0435\u0442\u043A\u0435 \u2014 ${BASE_NOOP_MARK}, \u0431\u0430\u0437\u0430 \u043E\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u0441\u044F`
21522
21934
  );
21523
- deps.db.addEvent(reviewRun.id, "conflict", `\u0431\u0430\u0437\u0430 \u0432\u043B\u0438\u0442\u0430 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 (${mainRef}, \u0431\u0435\u0437 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u043E\u0432) \u2014 PR \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D`);
21524
21935
  } else {
21525
21936
  const prompt = buildResolverPrompt(reviewRun, resolver, m.conflicts, branch, mainRef);
21526
21937
  const pid = deps.launch({ worktree: wt, run: resolver, prompt });
@@ -22400,7 +22811,10 @@ async function runDaemon(opts) {
22400
22811
  const ci = await prCiStatus(url);
22401
22812
  if (ci.bucket === "denied" || ci.bucket === "unavailable") return null;
22402
22813
  return { bucket: ci.bucket };
22403
- }
22814
+ },
22815
+ // TASWM-577: прогоны GitHub Actions на HEAD — только вливанию по намерению: рой
22816
+ // вливает сам и обязан дождаться, пока CI на этом коммите добежит целиком.
22817
+ actionsRuns: (url, sha) => prActionsRuns(url, sha)
22404
22818
  } : {}
22405
22819
  });
22406
22820
  const deps = {
@@ -22410,6 +22824,24 @@ async function runDaemon(opts) {
22410
22824
  worktreesDir,
22411
22825
  // TASWM-472: реконсайлу от очереди нужен ровно исход `base_stale` (см. needsBaseRefreshNow).
22412
22826
  ...SWARM_MERGE_QUEUE ? { mergeQueue: mergeParts.queue } : {},
22827
+ // TASWM-577: вливание ПО НАМЕРЕНИЮ — та же ось, тот же вход (mergeParts), но очередь из
22828
+ // одних отмеченных PR. Выключается тем же ключом, что и очередь: SWARM_MERGE_QUEUE=0 —
22829
+ // рой не вливает ничего, мержит человек кнопкой, как до этой задачи.
22830
+ ...SWARM_MERGE_QUEUE ? { mergeIntent: mergeParts.intent } : {},
22831
+ // Сам мерж и снятие намерения идут в ПРОД-ручки (`POST /swarm/merge` — GitHub App
22832
+ // воркспейса, squash; `DELETE /swarm/merge-request/:runId`), а адресуются они cuid'ом
22833
+ // прогона в проде. Он есть только у клиентского бэкенда: в sqlite-режиме прогонов в
22834
+ // проде нет вовсе, и рой там по-прежнему не мержит — мержит человек.
22835
+ ...db instanceof SwarmClient ? {
22836
+ mergePr: async (run, sha) => {
22837
+ const prodId = db.prodRunId(run.id);
22838
+ return prodId ? prodMergePr(prodId, sha) : { ok: false, error: "\u043F\u0440\u043E\u0433\u043E\u043D \u0435\u0449\u0451 \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B \u0434\u043E \u043F\u0440\u043E\u0434\u0430 \u2014 \u0432\u043B\u0438\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u043C" };
22839
+ },
22840
+ clearMergeIntent: async (run) => {
22841
+ const prodId = db.prodRunId(run.id);
22842
+ return prodId ? prodClearMergeIntent(prodId) : false;
22843
+ }
22844
+ } : {},
22413
22845
  narrate: narrator.narrate,
22414
22846
  control,
22415
22847
  spawn: async (run) => {