@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/cli.js CHANGED
@@ -672,7 +672,7 @@ var init_modelPolicy = __esm({
672
672
  label: "Anthropic",
673
673
  executorId: "claude-code",
674
674
  selectable: true,
675
- roles: { executor: "claude-opus-5", review: "claude-sonnet-5" }
675
+ roles: { executor: "claude-opus-5-5", review: "claude-sonnet-5" }
676
676
  },
677
677
  // TASWM-565 · шаг 1 из четырёх: провайдер в реестре ЕСТЬ, выбрать его ещё нельзя.
678
678
  // Имён моделей здесь нет СОЗНАТЕЛЬНО: у владельца подписка, состав моделей в ней нам
@@ -1169,6 +1169,9 @@ var init_cost = __esm({
1169
1169
  });
1170
1170
 
1171
1171
  // ../swarm-core/src/modelPricing.ts
1172
+ function cacheReadFactorOf(pricing) {
1173
+ return pricing.cacheReadFactor ?? CACHE_READ_FACTOR;
1174
+ }
1172
1175
  function findModelPricing(model, table = MODEL_PRICING) {
1173
1176
  if (!model) return void 0;
1174
1177
  const exact = table[model];
@@ -1190,6 +1193,10 @@ var init_modelPricing = __esm({
1190
1193
  // и «почему прогон вчера стоил иначе» приходится выяснять по коду, а не по таблице.
1191
1194
  // Меняется цена — меняется строка, одним видимым коммитом.
1192
1195
  "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
1196
+ // Opus 5.5 — 4/20, и чтение из кеша у него 5 % входа ($0.20), а не обычные 10 %.
1197
+ // У дронов чтение из кеша — основная часть токенов: с общим множителем прогон на
1198
+ // Opus 5.5 стоил бы в расчёте почти вдвое больше настоящего.
1199
+ "claude-opus-5-5": { inputPerMTok: 4, outputPerMTok: 20, cacheReadFactor: 0.05 },
1193
1200
  "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
1194
1201
  "claude-fable-5": { inputPerMTok: 10, outputPerMTok: 50 },
1195
1202
  // Прогоны и вызовы на 4.x остаются в истории — строки держим ради их стоимости.
@@ -1281,6 +1288,10 @@ function pathZoneCandidate(path) {
1281
1288
  if (parts[0] === "src" && parts.length >= 3 && !GENERIC_DIRS.has(parts[1])) return parts[1];
1282
1289
  return parts[0];
1283
1290
  }
1291
+ function dirOf(path) {
1292
+ const i = path.lastIndexOf("/");
1293
+ return i < 0 ? "" : path.slice(0, i);
1294
+ }
1284
1295
  function tokenize(text) {
1285
1296
  const out = [];
1286
1297
  for (const raw of text.split(/[^\p{L}\p{N}]+/u)) {
@@ -1514,13 +1525,61 @@ function similarTasks(model, text, limit = 5) {
1514
1525
  }
1515
1526
  return scored.sort((a, b) => b.score - a.score).slice(0, limit);
1516
1527
  }
1528
+ function historyFiles(model) {
1529
+ let idx = historyIndex.get(model);
1530
+ if (!idx) {
1531
+ const files = new Set(model.entries.flatMap((e) => e.files));
1532
+ const byBase = /* @__PURE__ */ new Map();
1533
+ for (const f of files) {
1534
+ const base3 = f.slice(f.lastIndexOf("/") + 1);
1535
+ byBase.set(base3, [...byBase.get(base3) ?? [], f]);
1536
+ }
1537
+ idx = { files, byBase };
1538
+ historyIndex.set(model, idx);
1539
+ }
1540
+ return idx;
1541
+ }
1542
+ function namedPaths(model, text, fileExists) {
1543
+ const { files, byBase } = historyFiles(model);
1544
+ const known = (p) => files.has(p) || (fileExists?.(p) ?? false);
1545
+ const raw = [...new Set((text.match(PATH_CANDIDATE) ?? []).map((c) => c.replace(/^\.?\/+/, "").replace(/\.+$/, "")))].filter((c) => c && !c.split("/").includes(".."));
1546
+ raw.sort((a, b) => b.split("/").length - a.split("/").length);
1547
+ const out = [];
1548
+ const dirs = /* @__PURE__ */ new Set();
1549
+ const take = (p) => {
1550
+ if (out.includes(p)) return;
1551
+ out.push(p);
1552
+ dirs.add(dirOf(p));
1553
+ };
1554
+ for (const c of raw) {
1555
+ if (known(c)) {
1556
+ take(c);
1557
+ continue;
1558
+ }
1559
+ const base3 = c.slice(c.lastIndexOf("/") + 1);
1560
+ let matches = (byBase.get(base3) ?? []).filter((f) => f === c || f.endsWith(`/${c}`));
1561
+ if (matches.length > 1) matches = matches.filter((f) => dirs.has(dirOf(f)));
1562
+ if (matches.length === 1) {
1563
+ take(matches[0]);
1564
+ continue;
1565
+ }
1566
+ if (!c.includes("/") && fileExists) {
1567
+ const sibling = [...dirs].map((d) => d ? `${d}/${c}` : c).filter(fileExists);
1568
+ if (sibling.length === 1) take(sibling[0]);
1569
+ }
1570
+ }
1571
+ return out;
1572
+ }
1517
1573
  function predictFiles(model, opts) {
1518
1574
  if (!hasZoneModel(model)) return [];
1519
1575
  const limit = opts.limit ?? 40;
1520
1576
  const best = /* @__PURE__ */ new Map();
1577
+ const named = opts.text ? namedPaths(model, opts.text, opts.fileExists) : [];
1578
+ 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 });
1579
+ const factor = named.length ? NAMED_BACKGROUND_FACTOR : 1;
1521
1580
  const put = (path, weight, why) => {
1522
1581
  const cur = best.get(path);
1523
- if (!cur || cur.weight < weight) best.set(path, { path, weight, why });
1582
+ if (!cur || cur.weight < weight * factor) best.set(path, { path, weight: weight * factor, why });
1524
1583
  };
1525
1584
  if (opts.text) {
1526
1585
  const sim = similarTasks(model, opts.text, opts.similar ?? 5);
@@ -1557,7 +1616,7 @@ function checkZoneAgreement(model, entries = model.entries) {
1557
1616
  const n = rows.length;
1558
1617
  return { checked: n, matched, compatible, rate: n ? matched / n : 0, looseRate: n ? (matched + compatible) / n : 0, rows };
1559
1618
  }
1560
- var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS;
1619
+ var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS, PATH_CANDIDATE, historyIndex, NAMED_BACKGROUND_FACTOR;
1561
1620
  var init_codeZones = __esm({
1562
1621
  "../swarm-core/src/codeZones.ts"() {
1563
1622
  "use strict";
@@ -1636,6 +1695,9 @@ var init_codeZones = __esm({
1636
1695
  "\u043F\u0435\u0440\u0444",
1637
1696
  "wip"
1638
1697
  ]);
1698
+ PATH_CANDIDATE = /[\w@.\-/]+\.[A-Za-z][A-Za-z0-9]{0,5}/g;
1699
+ historyIndex = /* @__PURE__ */ new WeakMap();
1700
+ NAMED_BACKGROUND_FACTOR = 0.1;
1639
1701
  }
1640
1702
  });
1641
1703
 
@@ -1964,7 +2026,7 @@ function costFromUsage(usage2, model, table = MODEL_PRICING) {
1964
2026
  const output = usage2.output_tokens ?? 0;
1965
2027
  const cacheWrite = usage2.cache_creation_input_tokens ?? 0;
1966
2028
  const cacheRead = usage2.cache_read_input_tokens ?? 0;
1967
- const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
2029
+ const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * cacheReadFactorOf(pricing)) / 1e6;
1968
2030
  return { costUsd };
1969
2031
  }
1970
2032
  var init_pricing = __esm({
@@ -2943,6 +3005,18 @@ var init_zoneNotes = __esm({
2943
3005
  }
2944
3006
  });
2945
3007
 
3008
+ // src/store.ts
3009
+ function isCleanResolve(run) {
3010
+ return (run.stop_reason ?? "").includes(RESOLVE_CLEAN_MARK);
3011
+ }
3012
+ var RESOLVE_CLEAN_MARK;
3013
+ var init_store = __esm({
3014
+ "src/store.ts"() {
3015
+ "use strict";
3016
+ RESOLVE_CLEAN_MARK = "\u0447\u0438\u0441\u0442\u043E\u0435 \u0432\u043B\u0438\u0432\u0430\u043D\u0438\u0435 \u0431\u0430\u0437\u044B";
3017
+ }
3018
+ });
3019
+
2946
3020
  // src/agentToken.ts
2947
3021
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
2948
3022
  import { join as join7 } from "path";
@@ -3412,7 +3486,7 @@ var init_package = __esm({
3412
3486
  "package.json"() {
3413
3487
  package_default = {
3414
3488
  name: "@taskless-app/swarm",
3415
- version: "0.6.48",
3489
+ version: "0.6.50",
3416
3490
  type: "module",
3417
3491
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
3418
3492
  keywords: [
@@ -3463,7 +3537,7 @@ var init_package = __esm({
3463
3537
  execa: "^10.0.0",
3464
3538
  zod: "3.25.76"
3465
3539
  },
3466
- gitHead: "62a77b0a48d5a07ae71b45c5f0f6212dc6069829"
3540
+ gitHead: "859ffe2d91bf4c791af067147515b14a41b96739"
3467
3541
  };
3468
3542
  }
3469
3543
  });
@@ -4798,6 +4872,10 @@ function rowToRun(row) {
4798
4872
  // вычисляемая фаза ревью (TASWM-230)
4799
4873
  review_cycle: row.review_cycle ?? 0,
4800
4874
  // старые прогоны без колонки → 0
4875
+ // TASWM-577: намерение смержить (человек отметил «пора влить»). В sqlite-бэкенде оно
4876
+ // живёт только здесь — ставить его некому, кроме самого демона, зато читает его
4877
+ // реконсайл, и колонка нужна, чтобы отметка пережила рестарт.
4878
+ merge_requested_at: row.merge_requested_at ?? null,
4801
4879
  task_title: row.task_title ?? null,
4802
4880
  task_desc: row.task_desc ?? null,
4803
4881
  // TASWM-556: когда заведена сама задача — по ней проверка дубля отличает свой
@@ -4835,6 +4913,7 @@ var init_db = __esm({
4835
4913
  init_names2();
4836
4914
  init_zoneNotes();
4837
4915
  init_storm();
4916
+ init_store();
4838
4917
  init_src();
4839
4918
  TERMINAL_SQL = TERMINAL_STATUSES.map((s) => `'${s}'`).join(",");
4840
4919
  RESOLVER_TERMINAL_SQL = [...TERMINAL_STATUSES, "stalled"].map((s) => `'${s}'`).join(",");
@@ -4872,6 +4951,7 @@ CREATE TABLE IF NOT EXISTS runs (
4872
4951
  verifies_run_id INTEGER,
4873
4952
  verify_verdict TEXT,
4874
4953
  review_cycle INTEGER NOT NULL DEFAULT 0,
4954
+ merge_requested_at TEXT,
4875
4955
  task_title TEXT,
4876
4956
  task_desc TEXT,
4877
4957
  task_created_at TEXT,
@@ -4966,6 +5046,8 @@ CREATE TABLE IF NOT EXISTS imports (
4966
5046
  "ALTER TABLE runs ADD COLUMN verify_verdict TEXT",
4967
5047
  // Авто-ревью v2 (TASWM-162): номер цикла ревью — пере-ревью после фикс-итерации.
4968
5048
  "ALTER TABLE runs ADD COLUMN review_cycle INTEGER NOT NULL DEFAULT 0",
5049
+ // TASWM-577: намерение смержить — по нему рой вливает PR сам, по очереди мержа.
5050
+ "ALTER TABLE runs ADD COLUMN merge_requested_at TEXT",
4969
5051
  // Вычисляемая фаза ревью (TASWM-230): человеко-фраза «чего ждёт PR», её считает демон.
4970
5052
  "ALTER TABLE runs ADD COLUMN review_phase TEXT",
4971
5053
  // Приоритетная очередь (TASWM-269): явный приоритет прогона, выше — раньше из queued.
@@ -5188,9 +5270,19 @@ CREATE TABLE IF NOT EXISTS imports (
5188
5270
  ).get(reviewRunId);
5189
5271
  return row ? rowToRun(row) : null;
5190
5272
  }
5191
- /** Сколько всего резолверов уже поднимали на этот review-PR — потолок авто-попыток. */
5273
+ /**
5274
+ * Сколько резолверов С ДРОНОМ уже поднимали на этот review-PR — потолок авто-попыток.
5275
+ *
5276
+ * TASWM-575: чистые вливания базы (RESOLVE_CLEAN_MARK в stop_reason — git свёл сам, дрон
5277
+ * не поднимался, стоимость ноль) в счёт НЕ идут. Потолок защищает от карусели ДРОНОВ на
5278
+ * нетривиальном конфликте (TASWM-127); бесплатное вливание не карусель, а на оживлённом
5279
+ * main оно случается на каждый чужой мерж — три сдвига main выбирали потолок, и PR с
5280
+ * отставшей базой застревал молча (гейт мержа не пускает base_stale, а вливать уже
5281
+ * некому). Всё остальное (failed/stalled резолверы, в т.ч. транзиентные сбои ОС) считается
5282
+ * как раньше — иначе потолок потерял бы смысл на органических смертях.
5283
+ */
5192
5284
  resolverCountFor(reviewRunId) {
5193
- const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE resolves_run_id = ?").get(reviewRunId);
5285
+ 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}%`);
5194
5286
  return row.n;
5195
5287
  }
5196
5288
  /**
@@ -7232,6 +7324,7 @@ function parsePendingRepoRules(raw) {
7232
7324
 
7233
7325
  // src/swarmClient.ts
7234
7326
  init_zoneNotes();
7327
+ init_store();
7235
7328
  init_toolFailure();
7236
7329
 
7237
7330
  // src/logTail.ts
@@ -7286,6 +7379,12 @@ function depsFailureRetriable(code) {
7286
7379
  function runRef(runKey) {
7287
7380
  return { __runRef: runKey };
7288
7381
  }
7382
+ function isoOrNull(v) {
7383
+ if (v == null) return null;
7384
+ const raw = v instanceof Date ? v.toISOString() : typeof v === "string" ? v : "";
7385
+ const at = Date.parse(raw);
7386
+ return Number.isNaN(at) ? null : new Date(at).toISOString();
7387
+ }
7289
7388
  function isRunRef(v) {
7290
7389
  return typeof v === "object" && v !== null && "__runRef" in v;
7291
7390
  }
@@ -7753,6 +7852,17 @@ var SwarmClient = class _SwarmClient {
7753
7852
  runKeyOf(id) {
7754
7853
  return this.runs.get(id)?.runKey ?? null;
7755
7854
  }
7855
+ /**
7856
+ * Cuid прогона В ПРОДЕ — им адресуются прод-РУЧКИ, а не MCP-тулы (TASWM-577: демон
7857
+ * вливает PR тем же `POST /swarm/merge`, что и кнопка, и снимает намерение тем же
7858
+ * `DELETE /swarm/merge-request/:runId`). null — прогон до прода ещё не долетел
7859
+ * (создан этим демоном, флаш не прошёл): адресовать его там нечем, и вызывающий
7860
+ * обязан прочитать это как «сейчас нельзя», а не как ошибку.
7861
+ */
7862
+ prodRunId(id) {
7863
+ const key = this.runs.get(id)?.runKey;
7864
+ return key ? this.prodIdFor(key) ?? null : null;
7865
+ }
7756
7866
  runByKey(key) {
7757
7867
  const direct = [...this.runs.values()].find((r) => r.runKey === key);
7758
7868
  if (direct) return this.mat(direct);
@@ -7823,8 +7933,10 @@ var SwarmClient = class _SwarmClient {
7823
7933
  const cands = [...this.runs.values()].filter((r) => r.resolves_run_id === reviewRunId && !RESOLVER_TERMINAL.has(r.status)).sort((a, b) => b.id - a.id);
7824
7934
  return cands.length ? this.mat(cands[0]) : null;
7825
7935
  }
7936
+ // Потолок авто-попыток: чистые вливания базы (дрон не поднимался, стоимость ноль) не
7937
+ // считаются — см. db.ts:resolverCountFor и store.ts:RESOLVE_CLEAN_MARK (TASWM-575).
7826
7938
  resolverCountFor(reviewRunId) {
7827
- return [...this.runs.values()].filter((r) => r.resolves_run_id === reviewRunId).length;
7939
+ return [...this.runs.values()].filter((r) => r.resolves_run_id === reviewRunId && !isCleanResolve(r)).length;
7828
7940
  }
7829
7941
  // failed/stalled ревьюеры (сорвался спавн ДО вердикта) в счёт НЕ идут; stopped/superseded
7830
7942
  // тоже (TASWM-253: оператор закрыл зомби-ревьюера/сбросил попытки ревью — POST /swarm/
@@ -9297,6 +9409,9 @@ var SwarmClient = class _SwarmClient {
9297
9409
  verify_verdict: null,
9298
9410
  review_phase: null,
9299
9411
  // вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
9412
+ // TASWM-577: намерение смержить — прод-поле (его ставит человек ручкой
9413
+ // /swarm/merge-request), заполняется ниже из ответа.
9414
+ merge_requested_at: null,
9300
9415
  // Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
9301
9416
  // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
9302
9417
  // в отличие от respawns, — авто-ревью v2, TASWM-162.
@@ -9332,6 +9447,7 @@ var SwarmClient = class _SwarmClient {
9332
9447
  local.task_title = pr.taskTitle ?? local.task_title;
9333
9448
  local.task_desc = pr.taskDesc ?? local.task_desc;
9334
9449
  local.task_created_at = pr.taskCreatedAt ?? local.task_created_at;
9450
+ if ("mergeRequestedAt" in pr) local.merge_requested_at = isoOrNull(pr.mergeRequestedAt);
9335
9451
  local.name = pr.droneName ?? local.name;
9336
9452
  local.kind = pr.role ?? local.kind;
9337
9453
  const heroProd = pr.heroId ?? null;
@@ -10364,22 +10480,10 @@ function mergeQueueCandidates(runs, repo, repoFallback) {
10364
10480
  }
10365
10481
  var BASE_TTL_MS = 2e4;
10366
10482
  function makeMergeQueueResolver(deps) {
10367
- const memo = /* @__PURE__ */ new Map();
10483
+ const read = deps.headState ?? makeHeadStateReader(deps);
10368
10484
  const evaluate = async (e, prUrl, ready) => {
10369
- const now = Date.now();
10370
- let cached = memo.get(prUrl);
10371
- const ttl = deps.ttlMs ?? BASE_TTL_MS;
10372
- if (!cached || now - cached.at >= ttl) {
10373
- const base3 = await deps.baseSync(prUrl).catch(() => null);
10374
- let checks = "none";
10375
- if (base3?.behindBy === 0 && deps.checks) {
10376
- const ci = await deps.checks(prUrl).catch(() => null);
10377
- checks = ci ? ci.bucket : null;
10378
- }
10379
- cached = { at: now, base: base3, checks };
10380
- memo.set(prUrl, cached);
10381
- }
10382
- return computeMergeQueue({ entry: e, ready, behindBy: cached.base ? cached.base.behindBy : null, checks: cached.checks });
10485
+ const st = await read(prUrl);
10486
+ return computeMergeQueue({ entry: e, ready, behindBy: st.behindBy, checks: st.checks });
10383
10487
  };
10384
10488
  return async (run) => {
10385
10489
  if (!deps.baseSync || !run.pr_url) return null;
@@ -10414,6 +10518,124 @@ function makeMergeQueueResolver(deps) {
10414
10518
  function entryOf(r) {
10415
10519
  return { runId: r.id, prNumber: prNumberOf(r.pr_url), slug: r.task_slug };
10416
10520
  }
10521
+ function makeHeadStateReader(deps) {
10522
+ const memo = /* @__PURE__ */ new Map();
10523
+ return async (prUrl) => {
10524
+ const now = Date.now();
10525
+ const ttl = deps.ttlMs ?? BASE_TTL_MS;
10526
+ const cached = memo.get(prUrl);
10527
+ if (cached && now - cached.at < ttl) return { behindBy: cached.behindBy, checks: cached.checks, headSha: cached.headSha };
10528
+ const base3 = deps.baseSync ? await deps.baseSync(prUrl).catch(() => null) : null;
10529
+ let checks = "none";
10530
+ if (base3?.behindBy === 0 && deps.checks) {
10531
+ const ci = await deps.checks(prUrl).catch(() => null);
10532
+ checks = ci ? ci.bucket : null;
10533
+ }
10534
+ const fresh = { behindBy: base3 ? base3.behindBy : null, checks, headSha: base3?.headSha ?? null };
10535
+ memo.set(prUrl, { at: now, ...fresh });
10536
+ return fresh;
10537
+ };
10538
+ }
10539
+ function hasMergeIntent(r) {
10540
+ return !!(r.merge_requested_at ?? "").trim();
10541
+ }
10542
+ var NO_CI_GRACE_MS = 10 * 6e4;
10543
+ var shortSha = (sha) => sha.slice(0, 7);
10544
+ function makeMergeIntentResolver(deps) {
10545
+ const read = deps.headState ?? makeHeadStateReader(deps);
10546
+ const firstSeen = /* @__PURE__ */ new Map();
10547
+ const ciSettled = async (prUrl, st, now) => {
10548
+ const sha = st.headSha;
10549
+ if (!sha) {
10550
+ 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" };
10551
+ }
10552
+ if (!deps.actionsRuns) {
10553
+ 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" };
10554
+ }
10555
+ const runs = await deps.actionsRuns(prUrl, sha).catch(() => null);
10556
+ if (!runs) {
10557
+ 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` };
10558
+ }
10559
+ if (runs.failed.length > 0) {
10560
+ 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` };
10561
+ }
10562
+ if (runs.active.length > 0) {
10563
+ return {
10564
+ action: "wait",
10565
+ code: "checks_pending",
10566
+ 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`
10567
+ };
10568
+ }
10569
+ if (st.checks === "none") {
10570
+ if (runs.total > 0) {
10571
+ 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` };
10572
+ }
10573
+ const seen = firstSeen.get(prUrl);
10574
+ const waited = seen && seen.sha === sha ? now - seen.at : 0;
10575
+ const graceMin = NO_CI_GRACE_MS / 6e4;
10576
+ if (waited < NO_CI_GRACE_MS) {
10577
+ return {
10578
+ action: "wait",
10579
+ code: "checks_pending",
10580
+ 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)})`
10581
+ };
10582
+ }
10583
+ 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` };
10584
+ }
10585
+ 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` };
10586
+ };
10587
+ const confirmLive = async (prUrl, sha, verdict) => {
10588
+ if (!sha) {
10589
+ 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" };
10590
+ }
10591
+ const live = deps.baseSync ? await deps.baseSync(prUrl).catch(() => null) : null;
10592
+ if (!live || !live.headSha) {
10593
+ 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" };
10594
+ }
10595
+ if (verdict.action === "merge" && live.behindBy > 0) {
10596
+ return {
10597
+ action: "refresh_base",
10598
+ code: "base_stale",
10599
+ 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`
10600
+ };
10601
+ }
10602
+ if (live.headSha !== sha) {
10603
+ 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` };
10604
+ }
10605
+ return verdict;
10606
+ };
10607
+ return async (run) => {
10608
+ if (!deps.baseSync || !run.pr_url || !hasMergeIntent(run)) return null;
10609
+ const num = prNumberOf(run.pr_url);
10610
+ if (num == null) return null;
10611
+ const marked = mergeQueueCandidates(deps.listRuns(), effectiveRepoOf(run.repo, deps.repoFallback), deps.repoFallback).filter(hasMergeIntent);
10612
+ const mine = marked.find((c) => prNumberOf(c.pr_url) === num);
10613
+ if (!mine) return null;
10614
+ const queue = orderMergeQueue(marked.map(entryOf));
10615
+ const entry = entryOf(mine);
10616
+ const position = queue.findIndex((e) => e.runId === entry.runId) + 1;
10617
+ const head = queue[0];
10618
+ if (head.runId !== entry.runId) {
10619
+ return {
10620
+ action: "wait",
10621
+ code: "queue_wait",
10622
+ 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}`,
10623
+ position,
10624
+ size: queue.length
10625
+ };
10626
+ }
10627
+ const st = await read(mine.pr_url);
10628
+ const now = deps.now ? deps.now() : Date.now();
10629
+ if (st.headSha) {
10630
+ const seen = firstSeen.get(mine.pr_url);
10631
+ if (!seen || seen.sha !== st.headSha) firstSeen.set(mine.pr_url, { sha: st.headSha, at: now });
10632
+ }
10633
+ const d = computeMergeQueue({ entry, ready: queue, behindBy: st.behindBy, checks: st.checks });
10634
+ 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 };
10635
+ const final = verdict.action === "merge" || verdict.action === "clear" ? await confirmLive(mine.pr_url, st.headSha, verdict) : verdict;
10636
+ return { ...final, position, size: queue.length, ...st.headSha ? { headSha: st.headSha } : {} };
10637
+ };
10638
+ }
10417
10639
 
10418
10640
  // src/mergeGate.ts
10419
10641
  function findRunByPr(runs, ref, scope = {}) {
@@ -10442,9 +10664,15 @@ function makeMergeGateParts(deps) {
10442
10664
  });
10443
10665
  };
10444
10666
  const repoOfRun = deps.repoFullNameOf ? (r) => deps.repoFullNameOf(effectiveRepoOf(r.repo, deps.repoFallback)) : void 0;
10667
+ const headState = makeHeadStateReader({
10668
+ ...deps.baseSync ? { baseSync: deps.baseSync } : {},
10669
+ ...deps.checks ? { checks: deps.checks } : {},
10670
+ ...deps.ttlMs != null ? { ttlMs: deps.ttlMs } : {}
10671
+ });
10445
10672
  const queueFor = makeMergeQueueResolver({
10446
10673
  listRuns: deps.listRuns,
10447
10674
  gateAllows: async (r) => (await gateFor(r)).allowed,
10675
+ headState,
10448
10676
  ...deps.repoFallback ? { repoFallback: deps.repoFallback } : {},
10449
10677
  ...deps.baseSync ? { baseSync: deps.baseSync } : {},
10450
10678
  ...deps.checks ? { checks: deps.checks } : {},
@@ -10470,7 +10698,16 @@ function makeMergeGateParts(deps) {
10470
10698
  }
10471
10699
  };
10472
10700
  };
10473
- return { gate, queue: queueFor };
10701
+ const intentFor = makeMergeIntentResolver({
10702
+ listRuns: deps.listRuns,
10703
+ headState,
10704
+ ...deps.repoFallback ? { repoFallback: deps.repoFallback } : {},
10705
+ ...deps.baseSync ? { baseSync: deps.baseSync } : {},
10706
+ ...deps.checks ? { checks: deps.checks } : {},
10707
+ ...deps.actionsRuns ? { actionsRuns: deps.actionsRuns } : {},
10708
+ ...deps.ttlMs != null ? { ttlMs: deps.ttlMs } : {}
10709
+ });
10710
+ return { gate, queue: queueFor, intent: intentFor };
10474
10711
  }
10475
10712
 
10476
10713
  // src/web.ts