@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.
- package/dist/cli.js +262 -25
- package/dist/overmind.js +485 -53
- 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 *
|
|
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.
|
|
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: "
|
|
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
|
-
/**
|
|
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
|
|
10483
|
+
const read = deps.headState ?? makeHeadStateReader(deps);
|
|
10368
10484
|
const evaluate = async (e, prUrl, ready) => {
|
|
10369
|
-
const
|
|
10370
|
-
|
|
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
|
-
|
|
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
|