@taskless-app/swarm 0.6.49 → 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 +224 -20
- package/dist/overmind.js +411 -37
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1288,6 +1288,10 @@ function pathZoneCandidate(path) {
|
|
|
1288
1288
|
if (parts[0] === "src" && parts.length >= 3 && !GENERIC_DIRS.has(parts[1])) return parts[1];
|
|
1289
1289
|
return parts[0];
|
|
1290
1290
|
}
|
|
1291
|
+
function dirOf(path) {
|
|
1292
|
+
const i = path.lastIndexOf("/");
|
|
1293
|
+
return i < 0 ? "" : path.slice(0, i);
|
|
1294
|
+
}
|
|
1291
1295
|
function tokenize(text) {
|
|
1292
1296
|
const out = [];
|
|
1293
1297
|
for (const raw of text.split(/[^\p{L}\p{N}]+/u)) {
|
|
@@ -1521,13 +1525,61 @@ function similarTasks(model, text, limit = 5) {
|
|
|
1521
1525
|
}
|
|
1522
1526
|
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1523
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
|
+
}
|
|
1524
1573
|
function predictFiles(model, opts) {
|
|
1525
1574
|
if (!hasZoneModel(model)) return [];
|
|
1526
1575
|
const limit = opts.limit ?? 40;
|
|
1527
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;
|
|
1528
1580
|
const put = (path, weight, why) => {
|
|
1529
1581
|
const cur = best.get(path);
|
|
1530
|
-
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 });
|
|
1531
1583
|
};
|
|
1532
1584
|
if (opts.text) {
|
|
1533
1585
|
const sim = similarTasks(model, opts.text, opts.similar ?? 5);
|
|
@@ -1564,7 +1616,7 @@ function checkZoneAgreement(model, entries = model.entries) {
|
|
|
1564
1616
|
const n = rows.length;
|
|
1565
1617
|
return { checked: n, matched, compatible, rate: n ? matched / n : 0, looseRate: n ? (matched + compatible) / n : 0, rows };
|
|
1566
1618
|
}
|
|
1567
|
-
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;
|
|
1568
1620
|
var init_codeZones = __esm({
|
|
1569
1621
|
"../swarm-core/src/codeZones.ts"() {
|
|
1570
1622
|
"use strict";
|
|
@@ -1643,6 +1695,9 @@ var init_codeZones = __esm({
|
|
|
1643
1695
|
"\u043F\u0435\u0440\u0444",
|
|
1644
1696
|
"wip"
|
|
1645
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;
|
|
1646
1701
|
}
|
|
1647
1702
|
});
|
|
1648
1703
|
|
|
@@ -3431,7 +3486,7 @@ var init_package = __esm({
|
|
|
3431
3486
|
"package.json"() {
|
|
3432
3487
|
package_default = {
|
|
3433
3488
|
name: "@taskless-app/swarm",
|
|
3434
|
-
version: "0.6.
|
|
3489
|
+
version: "0.6.50",
|
|
3435
3490
|
type: "module",
|
|
3436
3491
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
3437
3492
|
keywords: [
|
|
@@ -3482,7 +3537,7 @@ var init_package = __esm({
|
|
|
3482
3537
|
execa: "^10.0.0",
|
|
3483
3538
|
zod: "3.25.76"
|
|
3484
3539
|
},
|
|
3485
|
-
gitHead: "
|
|
3540
|
+
gitHead: "859ffe2d91bf4c791af067147515b14a41b96739"
|
|
3486
3541
|
};
|
|
3487
3542
|
}
|
|
3488
3543
|
});
|
|
@@ -4817,6 +4872,10 @@ function rowToRun(row) {
|
|
|
4817
4872
|
// вычисляемая фаза ревью (TASWM-230)
|
|
4818
4873
|
review_cycle: row.review_cycle ?? 0,
|
|
4819
4874
|
// старые прогоны без колонки → 0
|
|
4875
|
+
// TASWM-577: намерение смержить (человек отметил «пора влить»). В sqlite-бэкенде оно
|
|
4876
|
+
// живёт только здесь — ставить его некому, кроме самого демона, зато читает его
|
|
4877
|
+
// реконсайл, и колонка нужна, чтобы отметка пережила рестарт.
|
|
4878
|
+
merge_requested_at: row.merge_requested_at ?? null,
|
|
4820
4879
|
task_title: row.task_title ?? null,
|
|
4821
4880
|
task_desc: row.task_desc ?? null,
|
|
4822
4881
|
// TASWM-556: когда заведена сама задача — по ней проверка дубля отличает свой
|
|
@@ -4892,6 +4951,7 @@ CREATE TABLE IF NOT EXISTS runs (
|
|
|
4892
4951
|
verifies_run_id INTEGER,
|
|
4893
4952
|
verify_verdict TEXT,
|
|
4894
4953
|
review_cycle INTEGER NOT NULL DEFAULT 0,
|
|
4954
|
+
merge_requested_at TEXT,
|
|
4895
4955
|
task_title TEXT,
|
|
4896
4956
|
task_desc TEXT,
|
|
4897
4957
|
task_created_at TEXT,
|
|
@@ -4986,6 +5046,8 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
4986
5046
|
"ALTER TABLE runs ADD COLUMN verify_verdict TEXT",
|
|
4987
5047
|
// Авто-ревью v2 (TASWM-162): номер цикла ревью — пере-ревью после фикс-итерации.
|
|
4988
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",
|
|
4989
5051
|
// Вычисляемая фаза ревью (TASWM-230): человеко-фраза «чего ждёт PR», её считает демон.
|
|
4990
5052
|
"ALTER TABLE runs ADD COLUMN review_phase TEXT",
|
|
4991
5053
|
// Приоритетная очередь (TASWM-269): явный приоритет прогона, выше — раньше из queued.
|
|
@@ -7317,6 +7379,12 @@ function depsFailureRetriable(code) {
|
|
|
7317
7379
|
function runRef(runKey) {
|
|
7318
7380
|
return { __runRef: runKey };
|
|
7319
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
|
+
}
|
|
7320
7388
|
function isRunRef(v) {
|
|
7321
7389
|
return typeof v === "object" && v !== null && "__runRef" in v;
|
|
7322
7390
|
}
|
|
@@ -7784,6 +7852,17 @@ var SwarmClient = class _SwarmClient {
|
|
|
7784
7852
|
runKeyOf(id) {
|
|
7785
7853
|
return this.runs.get(id)?.runKey ?? null;
|
|
7786
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
|
+
}
|
|
7787
7866
|
runByKey(key) {
|
|
7788
7867
|
const direct = [...this.runs.values()].find((r) => r.runKey === key);
|
|
7789
7868
|
if (direct) return this.mat(direct);
|
|
@@ -9330,6 +9409,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
9330
9409
|
verify_verdict: null,
|
|
9331
9410
|
review_phase: null,
|
|
9332
9411
|
// вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
|
|
9412
|
+
// TASWM-577: намерение смержить — прод-поле (его ставит человек ручкой
|
|
9413
|
+
// /swarm/merge-request), заполняется ниже из ответа.
|
|
9414
|
+
merge_requested_at: null,
|
|
9333
9415
|
// Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
|
|
9334
9416
|
// существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
|
|
9335
9417
|
// в отличие от respawns, — авто-ревью v2, TASWM-162.
|
|
@@ -9365,6 +9447,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
9365
9447
|
local.task_title = pr.taskTitle ?? local.task_title;
|
|
9366
9448
|
local.task_desc = pr.taskDesc ?? local.task_desc;
|
|
9367
9449
|
local.task_created_at = pr.taskCreatedAt ?? local.task_created_at;
|
|
9450
|
+
if ("mergeRequestedAt" in pr) local.merge_requested_at = isoOrNull(pr.mergeRequestedAt);
|
|
9368
9451
|
local.name = pr.droneName ?? local.name;
|
|
9369
9452
|
local.kind = pr.role ?? local.kind;
|
|
9370
9453
|
const heroProd = pr.heroId ?? null;
|
|
@@ -10397,22 +10480,10 @@ function mergeQueueCandidates(runs, repo, repoFallback) {
|
|
|
10397
10480
|
}
|
|
10398
10481
|
var BASE_TTL_MS = 2e4;
|
|
10399
10482
|
function makeMergeQueueResolver(deps) {
|
|
10400
|
-
const
|
|
10483
|
+
const read = deps.headState ?? makeHeadStateReader(deps);
|
|
10401
10484
|
const evaluate = async (e, prUrl, ready) => {
|
|
10402
|
-
const
|
|
10403
|
-
|
|
10404
|
-
const ttl = deps.ttlMs ?? BASE_TTL_MS;
|
|
10405
|
-
if (!cached || now - cached.at >= ttl) {
|
|
10406
|
-
const base3 = await deps.baseSync(prUrl).catch(() => null);
|
|
10407
|
-
let checks = "none";
|
|
10408
|
-
if (base3?.behindBy === 0 && deps.checks) {
|
|
10409
|
-
const ci = await deps.checks(prUrl).catch(() => null);
|
|
10410
|
-
checks = ci ? ci.bucket : null;
|
|
10411
|
-
}
|
|
10412
|
-
cached = { at: now, base: base3, checks };
|
|
10413
|
-
memo.set(prUrl, cached);
|
|
10414
|
-
}
|
|
10415
|
-
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 });
|
|
10416
10487
|
};
|
|
10417
10488
|
return async (run) => {
|
|
10418
10489
|
if (!deps.baseSync || !run.pr_url) return null;
|
|
@@ -10447,6 +10518,124 @@ function makeMergeQueueResolver(deps) {
|
|
|
10447
10518
|
function entryOf(r) {
|
|
10448
10519
|
return { runId: r.id, prNumber: prNumberOf(r.pr_url), slug: r.task_slug };
|
|
10449
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
|
+
}
|
|
10450
10639
|
|
|
10451
10640
|
// src/mergeGate.ts
|
|
10452
10641
|
function findRunByPr(runs, ref, scope = {}) {
|
|
@@ -10475,9 +10664,15 @@ function makeMergeGateParts(deps) {
|
|
|
10475
10664
|
});
|
|
10476
10665
|
};
|
|
10477
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
|
+
});
|
|
10478
10672
|
const queueFor = makeMergeQueueResolver({
|
|
10479
10673
|
listRuns: deps.listRuns,
|
|
10480
10674
|
gateAllows: async (r) => (await gateFor(r)).allowed,
|
|
10675
|
+
headState,
|
|
10481
10676
|
...deps.repoFallback ? { repoFallback: deps.repoFallback } : {},
|
|
10482
10677
|
...deps.baseSync ? { baseSync: deps.baseSync } : {},
|
|
10483
10678
|
...deps.checks ? { checks: deps.checks } : {},
|
|
@@ -10503,7 +10698,16 @@ function makeMergeGateParts(deps) {
|
|
|
10503
10698
|
}
|
|
10504
10699
|
};
|
|
10505
10700
|
};
|
|
10506
|
-
|
|
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 };
|
|
10507
10711
|
}
|
|
10508
10712
|
|
|
10509
10713
|
// src/web.ts
|
package/dist/overmind.js
CHANGED
|
@@ -1723,13 +1723,61 @@ function similarTasks(model, text, limit = 5) {
|
|
|
1723
1723
|
}
|
|
1724
1724
|
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1725
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
|
+
}
|
|
1726
1771
|
function predictFiles(model, opts) {
|
|
1727
1772
|
if (!hasZoneModel(model)) return [];
|
|
1728
1773
|
const limit = opts.limit ?? 40;
|
|
1729
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;
|
|
1730
1778
|
const put = (path, weight, why) => {
|
|
1731
1779
|
const cur = best.get(path);
|
|
1732
|
-
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 });
|
|
1733
1781
|
};
|
|
1734
1782
|
if (opts.text) {
|
|
1735
1783
|
const sim = similarTasks(model, opts.text, opts.similar ?? 5);
|
|
@@ -1746,12 +1794,18 @@ function predictFiles(model, opts) {
|
|
|
1746
1794
|
}
|
|
1747
1795
|
return [...best.values()].sort((a, b) => b.weight - a.weight || a.path.localeCompare(b.path)).slice(0, limit);
|
|
1748
1796
|
}
|
|
1797
|
+
function heldByPrediction(predicted) {
|
|
1798
|
+
const named = predicted.filter((p) => p.named);
|
|
1799
|
+
return (named.length ? named : predicted).map((p) => p.path);
|
|
1800
|
+
}
|
|
1749
1801
|
function overlapRisk(predicted, actual, opts = {}) {
|
|
1750
1802
|
const o = { ...OVERLAP_DEFAULTS, ...opts };
|
|
1751
1803
|
const actualSet = new Set(actual);
|
|
1752
1804
|
const actualDirs = new Set([...actualSet].map(dirOf).filter(Boolean));
|
|
1753
1805
|
const totalWeight = predicted.reduce((a, p) => a + p.weight, 0);
|
|
1754
1806
|
if (!totalWeight) return { overlap: false, score: 0, sharedFiles: [], sharedDirs: [] };
|
|
1807
|
+
const hasNamed = predicted.some((p) => p.named);
|
|
1808
|
+
let namedHit = false;
|
|
1755
1809
|
let hit = 0;
|
|
1756
1810
|
let strong = false;
|
|
1757
1811
|
const sharedFiles = [];
|
|
@@ -1760,24 +1814,26 @@ function overlapRisk(predicted, actual, opts = {}) {
|
|
|
1760
1814
|
if (actualSet.has(p.path)) {
|
|
1761
1815
|
hit += p.weight;
|
|
1762
1816
|
sharedFiles.push(p.path);
|
|
1763
|
-
if (p.
|
|
1817
|
+
if (p.named) namedHit = true;
|
|
1818
|
+
if (p.weight >= o.strongFile && (p.named || !hasNamed)) strong = true;
|
|
1764
1819
|
continue;
|
|
1765
1820
|
}
|
|
1766
1821
|
const d = dirOf(p.path);
|
|
1767
1822
|
if (d && actualDirs.has(d)) {
|
|
1768
1823
|
hit += p.weight * 0.25;
|
|
1769
1824
|
sharedDirs.add(d);
|
|
1825
|
+
if (p.named) namedHit = true;
|
|
1770
1826
|
}
|
|
1771
1827
|
}
|
|
1772
1828
|
const score = Math.min(1, hit / totalWeight);
|
|
1773
1829
|
return {
|
|
1774
|
-
overlap: strong || score >= o.threshold,
|
|
1830
|
+
overlap: strong || score >= o.threshold && (!hasNamed || namedHit),
|
|
1775
1831
|
score,
|
|
1776
1832
|
sharedFiles: sharedFiles.slice(0, 10),
|
|
1777
1833
|
sharedDirs: [...sharedDirs].slice(0, 5)
|
|
1778
1834
|
};
|
|
1779
1835
|
}
|
|
1780
|
-
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;
|
|
1781
1837
|
var init_codeZones = __esm({
|
|
1782
1838
|
"../swarm-core/src/codeZones.ts"() {
|
|
1783
1839
|
"use strict";
|
|
@@ -1856,6 +1912,9 @@ var init_codeZones = __esm({
|
|
|
1856
1912
|
"\u043F\u0435\u0440\u0444",
|
|
1857
1913
|
"wip"
|
|
1858
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;
|
|
1859
1918
|
OVERLAP_DEFAULTS = { threshold: 0.2, strongFile: 0.35 };
|
|
1860
1919
|
}
|
|
1861
1920
|
});
|
|
@@ -6253,6 +6312,10 @@ function rowToRun(row) {
|
|
|
6253
6312
|
// вычисляемая фаза ревью (TASWM-230)
|
|
6254
6313
|
review_cycle: row.review_cycle ?? 0,
|
|
6255
6314
|
// старые прогоны без колонки → 0
|
|
6315
|
+
// TASWM-577: намерение смержить (человек отметил «пора влить»). В sqlite-бэкенде оно
|
|
6316
|
+
// живёт только здесь — ставить его некому, кроме самого демона, зато читает его
|
|
6317
|
+
// реконсайл, и колонка нужна, чтобы отметка пережила рестарт.
|
|
6318
|
+
merge_requested_at: row.merge_requested_at ?? null,
|
|
6256
6319
|
task_title: row.task_title ?? null,
|
|
6257
6320
|
task_desc: row.task_desc ?? null,
|
|
6258
6321
|
// TASWM-556: когда заведена сама задача — по ней проверка дубля отличает свой
|
|
@@ -6328,6 +6391,7 @@ CREATE TABLE IF NOT EXISTS runs (
|
|
|
6328
6391
|
verifies_run_id INTEGER,
|
|
6329
6392
|
verify_verdict TEXT,
|
|
6330
6393
|
review_cycle INTEGER NOT NULL DEFAULT 0,
|
|
6394
|
+
merge_requested_at TEXT,
|
|
6331
6395
|
task_title TEXT,
|
|
6332
6396
|
task_desc TEXT,
|
|
6333
6397
|
task_created_at TEXT,
|
|
@@ -6422,6 +6486,8 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
6422
6486
|
"ALTER TABLE runs ADD COLUMN verify_verdict TEXT",
|
|
6423
6487
|
// Авто-ревью v2 (TASWM-162): номер цикла ревью — пере-ревью после фикс-итерации.
|
|
6424
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",
|
|
6425
6491
|
// Вычисляемая фаза ревью (TASWM-230): человеко-фраза «чего ждёт PR», её считает демон.
|
|
6426
6492
|
"ALTER TABLE runs ADD COLUMN review_phase TEXT",
|
|
6427
6493
|
// Приоритетная очередь (TASWM-269): явный приоритет прогона, выше — раньше из queued.
|
|
@@ -7207,7 +7273,7 @@ var init_droneStore = __esm({
|
|
|
7207
7273
|
|
|
7208
7274
|
// src/overmind.ts
|
|
7209
7275
|
import { homedir as homedir7 } from "os";
|
|
7210
|
-
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";
|
|
7211
7277
|
import { basename as basename6, dirname as dirname5, join as join34 } from "path";
|
|
7212
7278
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
|
|
7213
7279
|
import { spawn as spawn4 } from "child_process";
|
|
@@ -7555,6 +7621,12 @@ function depsFailureRetriable(code) {
|
|
|
7555
7621
|
function runRef(runKey) {
|
|
7556
7622
|
return { __runRef: runKey };
|
|
7557
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
|
+
}
|
|
7558
7630
|
function isRunRef(v) {
|
|
7559
7631
|
return typeof v === "object" && v !== null && "__runRef" in v;
|
|
7560
7632
|
}
|
|
@@ -8022,6 +8094,17 @@ var SwarmClient = class _SwarmClient {
|
|
|
8022
8094
|
runKeyOf(id) {
|
|
8023
8095
|
return this.runs.get(id)?.runKey ?? null;
|
|
8024
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
|
+
}
|
|
8025
8108
|
runByKey(key) {
|
|
8026
8109
|
const direct = [...this.runs.values()].find((r) => r.runKey === key);
|
|
8027
8110
|
if (direct) return this.mat(direct);
|
|
@@ -9568,6 +9651,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
9568
9651
|
verify_verdict: null,
|
|
9569
9652
|
review_phase: null,
|
|
9570
9653
|
// вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
|
|
9654
|
+
// TASWM-577: намерение смержить — прод-поле (его ставит человек ручкой
|
|
9655
|
+
// /swarm/merge-request), заполняется ниже из ответа.
|
|
9656
|
+
merge_requested_at: null,
|
|
9571
9657
|
// Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
|
|
9572
9658
|
// существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
|
|
9573
9659
|
// в отличие от respawns, — авто-ревью v2, TASWM-162.
|
|
@@ -9603,6 +9689,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
9603
9689
|
local.task_title = pr.taskTitle ?? local.task_title;
|
|
9604
9690
|
local.task_desc = pr.taskDesc ?? local.task_desc;
|
|
9605
9691
|
local.task_created_at = pr.taskCreatedAt ?? local.task_created_at;
|
|
9692
|
+
if ("mergeRequestedAt" in pr) local.merge_requested_at = isoOrNull(pr.mergeRequestedAt);
|
|
9606
9693
|
local.name = pr.droneName ?? local.name;
|
|
9607
9694
|
local.kind = pr.role ?? local.kind;
|
|
9608
9695
|
const heroProd = pr.heroId ?? null;
|
|
@@ -12393,7 +12480,7 @@ function parkReasonUnretriable(reason) {
|
|
|
12393
12480
|
// package.json
|
|
12394
12481
|
var package_default = {
|
|
12395
12482
|
name: "@taskless-app/swarm",
|
|
12396
|
-
version: "0.6.
|
|
12483
|
+
version: "0.6.50",
|
|
12397
12484
|
type: "module",
|
|
12398
12485
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
12399
12486
|
keywords: [
|
|
@@ -12444,7 +12531,7 @@ var package_default = {
|
|
|
12444
12531
|
execa: "^10.0.0",
|
|
12445
12532
|
zod: "3.25.76"
|
|
12446
12533
|
},
|
|
12447
|
-
gitHead: "
|
|
12534
|
+
gitHead: "859ffe2d91bf4c791af067147515b14a41b96739"
|
|
12448
12535
|
};
|
|
12449
12536
|
|
|
12450
12537
|
// src/version.ts
|
|
@@ -12762,18 +12849,48 @@ async function prHeadBranch(prUrl, run = realRunner) {
|
|
|
12762
12849
|
return null;
|
|
12763
12850
|
}
|
|
12764
12851
|
}
|
|
12765
|
-
|
|
12852
|
+
function prRepoPath(prUrl) {
|
|
12766
12853
|
const m = prUrl.trim().match(/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/\d+/i);
|
|
12767
|
-
|
|
12768
|
-
|
|
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;
|
|
12769
12859
|
try {
|
|
12770
12860
|
const j = JSON.parse(await run(["pr", "view", prUrl, "--json", "baseRefName,headRefOid"]));
|
|
12771
12861
|
const base3 = j.baseRefName;
|
|
12772
12862
|
const head = j.headRefOid;
|
|
12773
12863
|
if (!base3 || !head) return null;
|
|
12774
|
-
const out = await run(["api", `repos/${
|
|
12864
|
+
const out = await run(["api", `repos/${repoPath}/compare/${base3}...${head}?per_page=1`, "--jq", ".behind_by"]);
|
|
12775
12865
|
const behindBy = Number(out.trim());
|
|
12776
|
-
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
|
+
};
|
|
12777
12894
|
} catch {
|
|
12778
12895
|
return null;
|
|
12779
12896
|
}
|
|
@@ -15337,22 +15454,10 @@ function mergeQueueCandidates(runs, repo, repoFallback) {
|
|
|
15337
15454
|
}
|
|
15338
15455
|
var BASE_TTL_MS = 2e4;
|
|
15339
15456
|
function makeMergeQueueResolver(deps) {
|
|
15340
|
-
const
|
|
15457
|
+
const read = deps.headState ?? makeHeadStateReader(deps);
|
|
15341
15458
|
const evaluate = async (e, prUrl, ready) => {
|
|
15342
|
-
const
|
|
15343
|
-
|
|
15344
|
-
const ttl = deps.ttlMs ?? BASE_TTL_MS;
|
|
15345
|
-
if (!cached || now - cached.at >= ttl) {
|
|
15346
|
-
const base3 = await deps.baseSync(prUrl).catch(() => null);
|
|
15347
|
-
let checks = "none";
|
|
15348
|
-
if (base3?.behindBy === 0 && deps.checks) {
|
|
15349
|
-
const ci = await deps.checks(prUrl).catch(() => null);
|
|
15350
|
-
checks = ci ? ci.bucket : null;
|
|
15351
|
-
}
|
|
15352
|
-
cached = { at: now, base: base3, checks };
|
|
15353
|
-
memo.set(prUrl, cached);
|
|
15354
|
-
}
|
|
15355
|
-
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 });
|
|
15356
15461
|
};
|
|
15357
15462
|
return async (run) => {
|
|
15358
15463
|
if (!deps.baseSync || !run.pr_url) return null;
|
|
@@ -15387,6 +15492,124 @@ function makeMergeQueueResolver(deps) {
|
|
|
15387
15492
|
function entryOf(r) {
|
|
15388
15493
|
return { runId: r.id, prNumber: prNumberOf(r.pr_url), slug: r.task_slug };
|
|
15389
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
|
+
}
|
|
15390
15613
|
function needsBaseRefresh(d) {
|
|
15391
15614
|
return d?.code === "base_stale";
|
|
15392
15615
|
}
|
|
@@ -15418,9 +15641,15 @@ function makeMergeGateParts(deps) {
|
|
|
15418
15641
|
});
|
|
15419
15642
|
};
|
|
15420
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
|
+
});
|
|
15421
15649
|
const queueFor = makeMergeQueueResolver({
|
|
15422
15650
|
listRuns: deps.listRuns,
|
|
15423
15651
|
gateAllows: async (r) => (await gateFor(r)).allowed,
|
|
15652
|
+
headState,
|
|
15424
15653
|
...deps.repoFallback ? { repoFallback: deps.repoFallback } : {},
|
|
15425
15654
|
...deps.baseSync ? { baseSync: deps.baseSync } : {},
|
|
15426
15655
|
...deps.checks ? { checks: deps.checks } : {},
|
|
@@ -15446,7 +15675,68 @@ function makeMergeGateParts(deps) {
|
|
|
15446
15675
|
}
|
|
15447
15676
|
};
|
|
15448
15677
|
};
|
|
15449
|
-
|
|
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
|
+
}
|
|
15450
15740
|
}
|
|
15451
15741
|
|
|
15452
15742
|
// src/envGate.ts
|
|
@@ -18034,8 +18324,8 @@ async function headSha(repo) {
|
|
|
18034
18324
|
function runText(run) {
|
|
18035
18325
|
return [run.task_title ?? "", run.task_desc ?? ""].join(" ").trim();
|
|
18036
18326
|
}
|
|
18037
|
-
function predictForRun(model, run) {
|
|
18038
|
-
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 } : {} });
|
|
18039
18329
|
}
|
|
18040
18330
|
function short(files, n = 3) {
|
|
18041
18331
|
return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
|
|
@@ -18050,17 +18340,17 @@ var SPINE_PREDICTION_WEIGHT = 0.7;
|
|
|
18050
18340
|
function spineOfPrediction(spine, predicted) {
|
|
18051
18341
|
return predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
|
|
18052
18342
|
}
|
|
18053
|
-
function decideConflict({ model, candidate, active }) {
|
|
18343
|
+
function decideConflict({ model, candidate, active, fileExists }) {
|
|
18054
18344
|
if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
|
|
18055
18345
|
if (!hasZoneModel(model)) return { kind: "unknown" };
|
|
18056
|
-
const predicted = predictForRun(model, candidate);
|
|
18346
|
+
const predicted = predictForRun(model, candidate, fileExists);
|
|
18057
18347
|
if (!predicted.length) return { kind: "unknown" };
|
|
18058
18348
|
if (!active.length) return { kind: "clear" };
|
|
18059
18349
|
const spine = new Set(model.spine);
|
|
18060
18350
|
const candidateSpine = new Set(spineOfPrediction(spine, predicted));
|
|
18061
18351
|
if (candidateSpine.size) {
|
|
18062
18352
|
for (const w of active) {
|
|
18063
|
-
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));
|
|
18064
18354
|
const shared = theirSpine.filter((f) => candidateSpine.has(f));
|
|
18065
18355
|
if (!shared.length) continue;
|
|
18066
18356
|
return {
|
|
@@ -18282,6 +18572,31 @@ async function needsBaseRefreshNow(deps, run, stormActive, now) {
|
|
|
18282
18572
|
if (last != null && now - last < CONFLICT_RESOLVE_COOLDOWN_MS) return false;
|
|
18283
18573
|
return needsBaseRefresh(await deps.mergeQueue(run).catch(() => null));
|
|
18284
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
|
+
}
|
|
18285
18600
|
function lastCiCheckMs(db, runId) {
|
|
18286
18601
|
const evs = db.events(runId).filter((e) => e.kind === "ci_wake" || e.kind === "ci_checked");
|
|
18287
18602
|
if (!evs.length) return null;
|
|
@@ -19712,6 +20027,13 @@ async function zoneConflictFor(db, models, activeFiles, run, repo, repoFallback)
|
|
|
19712
20027
|
if (!hasZoneModel(model)) return { kind: "unknown" };
|
|
19713
20028
|
const holders = db.listRuns().filter((r) => holdsFiles(r));
|
|
19714
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
|
+
};
|
|
19715
20037
|
const active = [];
|
|
19716
20038
|
for (const r of holders) {
|
|
19717
20039
|
if (r.id === run.id) continue;
|
|
@@ -19720,10 +20042,10 @@ async function zoneConflictFor(db, models, activeFiles, run, repo, repoFallback)
|
|
|
19720
20042
|
const files = snapshot.length ? snapshot : await activeFiles.filesOf(r);
|
|
19721
20043
|
const prUrl = r.status === "review" ? r.pr_url : null;
|
|
19722
20044
|
active.push(
|
|
19723
|
-
files.length ? { run: r, files, actual: true, prUrl } : { run: r, files: predictForRun(model, r)
|
|
20045
|
+
files.length ? { run: r, files, actual: true, prUrl } : { run: r, files: heldByPrediction(predictForRun(model, r, fileExists)), actual: false, prUrl }
|
|
19724
20046
|
);
|
|
19725
20047
|
}
|
|
19726
|
-
return decideConflict({ model, candidate: run, active });
|
|
20048
|
+
return decideConflict({ model, candidate: run, active, fileExists });
|
|
19727
20049
|
}
|
|
19728
20050
|
function activeDronesOnRepo(deps, repo, exceptRunId) {
|
|
19729
20051
|
return deps.db.listRuns().filter(
|
|
@@ -20911,6 +21233,7 @@ async function tickOnce(deps) {
|
|
|
20911
21233
|
continue;
|
|
20912
21234
|
}
|
|
20913
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");
|
|
20914
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");
|
|
20915
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.`);
|
|
20916
21239
|
continue;
|
|
@@ -20940,7 +21263,37 @@ async function tickOnce(deps) {
|
|
|
20940
21263
|
continue;
|
|
20941
21264
|
}
|
|
20942
21265
|
await retireOutdatedReviewers(deps, run);
|
|
20943
|
-
|
|
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));
|
|
20944
21297
|
if ((st.conflicting || baseStale) && deps.resolveConflict && !stormActive) {
|
|
20945
21298
|
if (deps.db.activeResolverFor(run.id)) continue;
|
|
20946
21299
|
if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) {
|
|
@@ -22458,7 +22811,10 @@ async function runDaemon(opts) {
|
|
|
22458
22811
|
const ci = await prCiStatus(url);
|
|
22459
22812
|
if (ci.bucket === "denied" || ci.bucket === "unavailable") return null;
|
|
22460
22813
|
return { bucket: ci.bucket };
|
|
22461
|
-
}
|
|
22814
|
+
},
|
|
22815
|
+
// TASWM-577: прогоны GitHub Actions на HEAD — только вливанию по намерению: рой
|
|
22816
|
+
// вливает сам и обязан дождаться, пока CI на этом коммите добежит целиком.
|
|
22817
|
+
actionsRuns: (url, sha) => prActionsRuns(url, sha)
|
|
22462
22818
|
} : {}
|
|
22463
22819
|
});
|
|
22464
22820
|
const deps = {
|
|
@@ -22468,6 +22824,24 @@ async function runDaemon(opts) {
|
|
|
22468
22824
|
worktreesDir,
|
|
22469
22825
|
// TASWM-472: реконсайлу от очереди нужен ровно исход `base_stale` (см. needsBaseRefreshNow).
|
|
22470
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
|
+
} : {},
|
|
22471
22845
|
narrate: narrator.narrate,
|
|
22472
22846
|
control,
|
|
22473
22847
|
spawn: async (run) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taskless-app/swarm",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.50",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
6
6
|
"keywords": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"execa": "^10.0.0",
|
|
40
40
|
"zod": "3.25.76"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "859ffe2d91bf4c791af067147515b14a41b96739",
|
|
43
43
|
"scripts": {
|
|
44
44
|
"overmind": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/overmind.ts",
|
|
45
45
|
"swarm": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/cli.ts",
|