@taskless-app/swarm 0.6.6 → 0.6.8
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 +141 -2
- package/dist/overmind.js +133 -2
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -322,6 +322,35 @@ var init_reviewPhase = __esm({
|
|
|
322
322
|
}
|
|
323
323
|
});
|
|
324
324
|
|
|
325
|
+
// ../swarm-core/src/mergeGate.ts
|
|
326
|
+
function computeMergeGate(input) {
|
|
327
|
+
if (!input.hasRun) {
|
|
328
|
+
return { allowed: true, code: "unknown_pr", reason: "PR \u043D\u0435 \u043F\u0440\u0438\u043D\u0430\u0434\u043B\u0435\u0436\u0438\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0443 \u0440\u043E\u044F \u2014 \u0433\u0435\u0439\u0442 \u0440\u0435\u0432\u044C\u044E \u043A \u043D\u0435\u043C\u0443 \u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u044F\u0435\u0442\u0441\u044F" };
|
|
329
|
+
}
|
|
330
|
+
if (!input.autoReview) {
|
|
331
|
+
return { allowed: true, code: "auto_review_off", reason: "\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u0440\u0435\u0432\u044C\u044E \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F" };
|
|
332
|
+
}
|
|
333
|
+
if (input.reviewVerdict === "approve") {
|
|
334
|
+
return { allowed: true, code: "approved", reason: "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u0430\u043B approve \u2014 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043C\u043E\u0436\u043D\u043E" };
|
|
335
|
+
}
|
|
336
|
+
if (input.reviewVerdict === "changes") {
|
|
337
|
+
return { allowed: false, code: "changes_requested", reason: "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u043B \u043F\u0440\u0430\u0432\u043A\u0438 (REQUEST CHANGES) \u2014 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F, \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0438 \u0438 \u043D\u043E\u0432\u043E\u0433\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430" };
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
allowed: false,
|
|
341
|
+
code: "no_verdict",
|
|
342
|
+
reason: "\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u0440\u0435\u0432\u044C\u044E, \u0430 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043F\u043E \u044D\u0442\u043E\u043C\u0443 PR \u0435\u0449\u0451 \u043D\u0435\u0442 \u2014 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F (TASWM-355)"
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function mergeGateLine(d) {
|
|
346
|
+
return `${d.allowed ? "\u2705 \u043C\u0435\u0440\u0436 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D" : "\u26D4 \u043C\u0435\u0440\u0436 \u0437\u0430\u043F\u0440\u0435\u0449\u0451\u043D"}: ${d.reason}`;
|
|
347
|
+
}
|
|
348
|
+
var init_mergeGate = __esm({
|
|
349
|
+
"../swarm-core/src/mergeGate.ts"() {
|
|
350
|
+
"use strict";
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
325
354
|
// ../swarm-core/src/status.ts
|
|
326
355
|
function statusMeta(status) {
|
|
327
356
|
return STATUS_META[status] ?? FALLBACK;
|
|
@@ -1153,6 +1182,22 @@ var init_effort = __esm({
|
|
|
1153
1182
|
}
|
|
1154
1183
|
});
|
|
1155
1184
|
|
|
1185
|
+
// ../swarm-core/src/queuePlan.ts
|
|
1186
|
+
var MIN_MS, QUEUE_NOMINAL_RUN_MS, QUEUE_MIN_TAIL_MS, QUEUE_REVIEW_TAIL_MS;
|
|
1187
|
+
var init_queuePlan = __esm({
|
|
1188
|
+
"../swarm-core/src/queuePlan.ts"() {
|
|
1189
|
+
"use strict";
|
|
1190
|
+
init_status();
|
|
1191
|
+
init_hostPool();
|
|
1192
|
+
init_hosts();
|
|
1193
|
+
init_time();
|
|
1194
|
+
MIN_MS = 6e4;
|
|
1195
|
+
QUEUE_NOMINAL_RUN_MS = 25 * MIN_MS;
|
|
1196
|
+
QUEUE_MIN_TAIL_MS = 3 * MIN_MS;
|
|
1197
|
+
QUEUE_REVIEW_TAIL_MS = 10 * MIN_MS;
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1156
1201
|
// ../swarm-core/src/index.ts
|
|
1157
1202
|
var init_src = __esm({
|
|
1158
1203
|
"../swarm-core/src/index.ts"() {
|
|
@@ -1160,6 +1205,7 @@ var init_src = __esm({
|
|
|
1160
1205
|
init_types();
|
|
1161
1206
|
init_timeline();
|
|
1162
1207
|
init_reviewPhase();
|
|
1208
|
+
init_mergeGate();
|
|
1163
1209
|
init_status();
|
|
1164
1210
|
init_deps();
|
|
1165
1211
|
init_time();
|
|
@@ -1178,6 +1224,7 @@ var init_src = __esm({
|
|
|
1178
1224
|
init_runGuards();
|
|
1179
1225
|
init_codeZones();
|
|
1180
1226
|
init_effort();
|
|
1227
|
+
init_queuePlan();
|
|
1181
1228
|
}
|
|
1182
1229
|
});
|
|
1183
1230
|
|
|
@@ -1988,7 +2035,7 @@ var init_package = __esm({
|
|
|
1988
2035
|
"package.json"() {
|
|
1989
2036
|
package_default = {
|
|
1990
2037
|
name: "@taskless-app/swarm",
|
|
1991
|
-
version: "0.6.
|
|
2038
|
+
version: "0.6.8",
|
|
1992
2039
|
type: "module",
|
|
1993
2040
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
1994
2041
|
keywords: [
|
|
@@ -2038,7 +2085,7 @@ var init_package = __esm({
|
|
|
2038
2085
|
execa: "^10.0.0",
|
|
2039
2086
|
zod: "3.25.76"
|
|
2040
2087
|
},
|
|
2041
|
-
gitHead: "
|
|
2088
|
+
gitHead: "c06a3749a3ef7d0c8fcae83fd79b84ba2318859b"
|
|
2042
2089
|
};
|
|
2043
2090
|
}
|
|
2044
2091
|
});
|
|
@@ -2353,6 +2400,16 @@ async function daemonStatus2() {
|
|
|
2353
2400
|
return null;
|
|
2354
2401
|
}
|
|
2355
2402
|
}
|
|
2403
|
+
async function daemonMergeGate(ref) {
|
|
2404
|
+
try {
|
|
2405
|
+
const res = await fetch(`${BASE}/merge-gate?pr=${encodeURIComponent(ref)}`, { signal: AbortSignal.timeout(TIMEOUT_MS) });
|
|
2406
|
+
if (!res.ok) return null;
|
|
2407
|
+
const body = await res.json();
|
|
2408
|
+
return typeof body?.allowed === "boolean" && typeof body?.reason === "string" ? body : null;
|
|
2409
|
+
} catch {
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2356
2413
|
var TIMEOUT_MS, ACTION_TIMEOUT_MS, BASE, NO_DAEMON_NOTE;
|
|
2357
2414
|
var init_daemonLink = __esm({
|
|
2358
2415
|
"src/daemonLink.ts"() {
|
|
@@ -5619,6 +5676,27 @@ var SwarmClient = class _SwarmClient {
|
|
|
5619
5676
|
const v = sc.config?.maxDrones;
|
|
5620
5677
|
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
|
|
5621
5678
|
}
|
|
5679
|
+
/**
|
|
5680
|
+
* Live АВТОРЕВЬЮ проекта из Taskless (TASWM-355): тот же swarm_repo_config по
|
|
5681
|
+
* repoFullName — Project.swarmConfig.autoReview. true → у проекта перед мержем обязан быть
|
|
5682
|
+
* вердикт дрона-ревьюера, и гейт мержа (mergeGate.ts) не пускает `gh pr merge`, пока
|
|
5683
|
+
* вердикта нет. До этой ручки демон знал только ГЛОБАЛЬНЫЙ env SWARM_AUTO_REVIEW
|
|
5684
|
+
* («поднимать ли ревьюеров на этой машине»), а настройка проекта до него не доезжала —
|
|
5685
|
+
* гейту же нужен ответ про КОНКРЕТНЫЙ проект. null (прод недоступен / repo не смэплен /
|
|
5686
|
+
* поля нет) → ридер (makeAutoReviewReader) возьмёт env, то есть прежнее поведение.
|
|
5687
|
+
*/
|
|
5688
|
+
async repoAutoReview(repoFullName) {
|
|
5689
|
+
let res;
|
|
5690
|
+
try {
|
|
5691
|
+
res = await this.call("swarm_repo_config", { repoFullName });
|
|
5692
|
+
} catch {
|
|
5693
|
+
return null;
|
|
5694
|
+
}
|
|
5695
|
+
if (res?.isError) return null;
|
|
5696
|
+
const sc = res.structuredContent ?? {};
|
|
5697
|
+
const v = sc.config?.autoReview;
|
|
5698
|
+
return typeof v === "boolean" ? v : null;
|
|
5699
|
+
}
|
|
5622
5700
|
/**
|
|
5623
5701
|
* TASWM-291: МАШИНЫ роя владельца (swarm_list_hosts) — чтобы `swarm spawn --host
|
|
5624
5702
|
* <имя>` резолвил человеческое имя в hostId, а `swarm hosts` показывал, какие
|
|
@@ -7102,6 +7180,41 @@ async function ghChecks(pr, run = realChecksRunner) {
|
|
|
7102
7180
|
|
|
7103
7181
|
// src/web.ts
|
|
7104
7182
|
init_config();
|
|
7183
|
+
|
|
7184
|
+
// src/mergeGate.ts
|
|
7185
|
+
init_src();
|
|
7186
|
+
function prNumberOf(ref) {
|
|
7187
|
+
const s = (ref || "").trim();
|
|
7188
|
+
if (!s) return null;
|
|
7189
|
+
const url = s.match(/\/pull\/(\d+)/);
|
|
7190
|
+
if (url) return Number(url[1]);
|
|
7191
|
+
const bare = s.match(/^#?(\d+)$/);
|
|
7192
|
+
return bare ? Number(bare[1]) : null;
|
|
7193
|
+
}
|
|
7194
|
+
function findRunByPr(runs, ref) {
|
|
7195
|
+
const num = prNumberOf(ref);
|
|
7196
|
+
if (num == null) return null;
|
|
7197
|
+
let best = null;
|
|
7198
|
+
for (const r of runs) {
|
|
7199
|
+
if (!r.pr_url || prNumberOf(r.pr_url) !== num) continue;
|
|
7200
|
+
if (!best || r.id > best.id) best = r;
|
|
7201
|
+
}
|
|
7202
|
+
return best;
|
|
7203
|
+
}
|
|
7204
|
+
function makeMergeGateResolver(deps) {
|
|
7205
|
+
return async (ref) => {
|
|
7206
|
+
const run = findRunByPr(deps.listRuns(), ref);
|
|
7207
|
+
if (!run) return { ...computeMergeGate({ hasRun: false, autoReview: false, reviewVerdict: null }), run: null };
|
|
7208
|
+
const autoReview = deps.autoReview ? await deps.autoReview(run.repo).catch(() => deps.fallbackAutoReview) : deps.fallbackAutoReview;
|
|
7209
|
+
const decision = computeMergeGate({ hasRun: true, autoReview, reviewVerdict: run.review_verdict });
|
|
7210
|
+
return {
|
|
7211
|
+
...decision,
|
|
7212
|
+
run: { id: run.id, task_slug: run.task_slug, name: run.name, pr_url: run.pr_url, review_verdict: run.review_verdict }
|
|
7213
|
+
};
|
|
7214
|
+
};
|
|
7215
|
+
}
|
|
7216
|
+
|
|
7217
|
+
// src/web.ts
|
|
7105
7218
|
init_droneChannel();
|
|
7106
7219
|
|
|
7107
7220
|
// src/oauth.ts
|
|
@@ -8653,6 +8766,12 @@ function makeHandler(deps) {
|
|
|
8653
8766
|
hostId: deps.selfHost?.hostId ?? null
|
|
8654
8767
|
});
|
|
8655
8768
|
}
|
|
8769
|
+
if (p === "/merge-gate") {
|
|
8770
|
+
const ref = url.searchParams.get("pr") ?? "";
|
|
8771
|
+
if (!ref.trim()) return Response.json({ error: "\u043D\u0443\u0436\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 pr=<url|\u043D\u043E\u043C\u0435\u0440>" }, { status: 400 });
|
|
8772
|
+
const resolve4 = deps.mergeGate ?? makeMergeGateResolver({ listRuns: () => deps.db.listRuns(), fallbackAutoReview: SWARM_AUTO_REVIEW });
|
|
8773
|
+
return Response.json(await resolve4(ref));
|
|
8774
|
+
}
|
|
8656
8775
|
if (p === "/resources") return Response.json(hostResourceSnapshot());
|
|
8657
8776
|
if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
|
|
8658
8777
|
const diffMatch = p.match(/^\/run\/(\d+)\/diff$/);
|
|
@@ -10165,6 +10284,9 @@ function usage() {
|
|
|
10165
10284
|
swarm wake <runId> \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D (id \u0438\u0437 swarm ls); \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E \u2192 \u043D\u0430\u0447\u043D\u0451\u0442 \u0441 \u043D\u0443\u043B\u044F
|
|
10166
10285
|
swarm cancel <slug> <\u043F\u0440\u0438\u0447\u0438\u043D\u0430\u2026>
|
|
10167
10286
|
\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430: \u0441\u0442\u0430\u0442\u0443\u0441 \xAB\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\xBB \u0441 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439, \u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u043E\u0441\u0442\u0430\u043D\u0435\u0442\u0441\u044F
|
|
10287
|
+
swarm merge-gate <pr> \u043C\u043E\u0436\u043D\u043E \u043B\u0438 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u044D\u0442\u043E\u0442 PR (\u0441\u0441\u044B\u043B\u043A\u0430 \u0438\u043B\u0438 \u043D\u043E\u043C\u0435\u0440): \u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441
|
|
10288
|
+
\u0430\u0432\u0442\u043E\u0440\u0435\u0432\u044C\u044E \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u043B\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430. \u0412\u044B\u0445\u043E\u0434 1 = \u043D\u0435\u043B\u044C\u0437\u044F.
|
|
10289
|
+
\u0422\u043E\u0442 \u0436\u0435 \u043E\u0442\u0432\u0435\u0442 \u043F\u043E\u043B\u0443\u0447\u0430\u0435\u0442 \u0445\u0443\u043A, \u043F\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u044B\u0432\u0430\u044E\u0449\u0438\u0439 gh pr merge
|
|
10168
10290
|
swarm questions \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0434\u0440\u043E\u043D\u043E\u0432
|
|
10169
10291
|
swarm answer <qid> <\u0442\u0435\u043A\u0441\u0442\u2026>
|
|
10170
10292
|
\u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0430 \u0432\u043E\u043F\u0440\u043E\u0441 \u0438 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430
|
|
@@ -10449,6 +10571,23 @@ if (cmd === "status") {
|
|
|
10449
10571
|
for (const line of repoMapLines(st.repos)) console.log(line);
|
|
10450
10572
|
process.exit(0);
|
|
10451
10573
|
}
|
|
10574
|
+
if (cmd === "merge-gate") {
|
|
10575
|
+
const ref = rawArgs[0];
|
|
10576
|
+
if (!ref) {
|
|
10577
|
+
console.error("\u043D\u0443\u0436\u043D\u043E: swarm merge-gate <\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 PR|\u043D\u043E\u043C\u0435\u0440>");
|
|
10578
|
+
process.exit(2);
|
|
10579
|
+
}
|
|
10580
|
+
const answer = await daemonMergeGate(ref);
|
|
10581
|
+
if (!answer) {
|
|
10582
|
+
console.error(
|
|
10583
|
+
`\u26D4 \u043C\u0435\u0440\u0436 \u0437\u0430\u043F\u0440\u0435\u0449\u0451\u043D: \u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B (\u043C\u043E\u0440\u0434\u0430 127.0.0.1:${WEB_PORT}) \u2014 \u0440\u043E\u0439 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C, \u0447\u0442\u043E \u043F\u043E PR \u0435\u0441\u0442\u044C \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430. \u041F\u043E\u0434\u043D\u0438\u043C\u0438 \u0434\u0435\u043C\u043E\u043D\u0430 (swarm start) \u0438\u043B\u0438, \u0435\u0441\u043B\u0438 \u043C\u0435\u0440\u0436\u0438\u0448\u044C \u043E\u0441\u043E\u0437\u043D\u0430\u043D\u043D\u043E \u0440\u0443\u043A\u0430\u043C\u0438, \u0441\u043D\u0438\u043C\u0438 \u0433\u0435\u0439\u0442: SWARM_MERGE_GATE=off`
|
|
10584
|
+
);
|
|
10585
|
+
process.exit(1);
|
|
10586
|
+
}
|
|
10587
|
+
const who = answer.run ? ` (\u043F\u0440\u043E\u0433\u043E\u043D #${answer.run.id} ${answer.run.task_slug}, \u0434\u0440\u043E\u043D ${answer.run.name})` : "";
|
|
10588
|
+
console.log(`${mergeGateLine(answer)}${who}`);
|
|
10589
|
+
process.exit(answer.allowed ? 0 : 1);
|
|
10590
|
+
}
|
|
10452
10591
|
if (cmd === "pause" || cmd === "resume") {
|
|
10453
10592
|
const wantPause = cmd === "pause";
|
|
10454
10593
|
const reason = wantPause ? rawArgs.join(" ").trim() || null : null;
|
package/dist/overmind.js
CHANGED
|
@@ -319,6 +319,32 @@ var init_reviewPhase = __esm({
|
|
|
319
319
|
}
|
|
320
320
|
});
|
|
321
321
|
|
|
322
|
+
// ../swarm-core/src/mergeGate.ts
|
|
323
|
+
function computeMergeGate(input) {
|
|
324
|
+
if (!input.hasRun) {
|
|
325
|
+
return { allowed: true, code: "unknown_pr", reason: "PR \u043D\u0435 \u043F\u0440\u0438\u043D\u0430\u0434\u043B\u0435\u0436\u0438\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0443 \u0440\u043E\u044F \u2014 \u0433\u0435\u0439\u0442 \u0440\u0435\u0432\u044C\u044E \u043A \u043D\u0435\u043C\u0443 \u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u044F\u0435\u0442\u0441\u044F" };
|
|
326
|
+
}
|
|
327
|
+
if (!input.autoReview) {
|
|
328
|
+
return { allowed: true, code: "auto_review_off", reason: "\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u0440\u0435\u0432\u044C\u044E \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F" };
|
|
329
|
+
}
|
|
330
|
+
if (input.reviewVerdict === "approve") {
|
|
331
|
+
return { allowed: true, code: "approved", reason: "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u0430\u043B approve \u2014 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043C\u043E\u0436\u043D\u043E" };
|
|
332
|
+
}
|
|
333
|
+
if (input.reviewVerdict === "changes") {
|
|
334
|
+
return { allowed: false, code: "changes_requested", reason: "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u043B \u043F\u0440\u0430\u0432\u043A\u0438 (REQUEST CHANGES) \u2014 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F, \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0438 \u0438 \u043D\u043E\u0432\u043E\u0433\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430" };
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
allowed: false,
|
|
338
|
+
code: "no_verdict",
|
|
339
|
+
reason: "\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u0440\u0435\u0432\u044C\u044E, \u0430 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043F\u043E \u044D\u0442\u043E\u043C\u0443 PR \u0435\u0449\u0451 \u043D\u0435\u0442 \u2014 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F (TASWM-355)"
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
var init_mergeGate = __esm({
|
|
343
|
+
"../swarm-core/src/mergeGate.ts"() {
|
|
344
|
+
"use strict";
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
322
348
|
// ../swarm-core/src/status.ts
|
|
323
349
|
function isAwaitingCi(status, stopReason) {
|
|
324
350
|
return status === "paused" && stopReason === AWAITING_CI_REASON;
|
|
@@ -1313,6 +1339,22 @@ var init_effort = __esm({
|
|
|
1313
1339
|
}
|
|
1314
1340
|
});
|
|
1315
1341
|
|
|
1342
|
+
// ../swarm-core/src/queuePlan.ts
|
|
1343
|
+
var MIN_MS, QUEUE_NOMINAL_RUN_MS, QUEUE_MIN_TAIL_MS, QUEUE_REVIEW_TAIL_MS;
|
|
1344
|
+
var init_queuePlan = __esm({
|
|
1345
|
+
"../swarm-core/src/queuePlan.ts"() {
|
|
1346
|
+
"use strict";
|
|
1347
|
+
init_status();
|
|
1348
|
+
init_hostPool();
|
|
1349
|
+
init_hosts();
|
|
1350
|
+
init_time();
|
|
1351
|
+
MIN_MS = 6e4;
|
|
1352
|
+
QUEUE_NOMINAL_RUN_MS = 25 * MIN_MS;
|
|
1353
|
+
QUEUE_MIN_TAIL_MS = 3 * MIN_MS;
|
|
1354
|
+
QUEUE_REVIEW_TAIL_MS = 10 * MIN_MS;
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1316
1358
|
// ../swarm-core/src/index.ts
|
|
1317
1359
|
var init_src = __esm({
|
|
1318
1360
|
"../swarm-core/src/index.ts"() {
|
|
@@ -1320,6 +1362,7 @@ var init_src = __esm({
|
|
|
1320
1362
|
init_types();
|
|
1321
1363
|
init_timeline();
|
|
1322
1364
|
init_reviewPhase();
|
|
1365
|
+
init_mergeGate();
|
|
1323
1366
|
init_status();
|
|
1324
1367
|
init_deps();
|
|
1325
1368
|
init_time();
|
|
@@ -1338,6 +1381,7 @@ var init_src = __esm({
|
|
|
1338
1381
|
init_runGuards();
|
|
1339
1382
|
init_codeZones();
|
|
1340
1383
|
init_effort();
|
|
1384
|
+
init_queuePlan();
|
|
1341
1385
|
}
|
|
1342
1386
|
});
|
|
1343
1387
|
|
|
@@ -7025,6 +7069,27 @@ var SwarmClient = class _SwarmClient {
|
|
|
7025
7069
|
const v = sc.config?.maxDrones;
|
|
7026
7070
|
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
|
|
7027
7071
|
}
|
|
7072
|
+
/**
|
|
7073
|
+
* Live АВТОРЕВЬЮ проекта из Taskless (TASWM-355): тот же swarm_repo_config по
|
|
7074
|
+
* repoFullName — Project.swarmConfig.autoReview. true → у проекта перед мержем обязан быть
|
|
7075
|
+
* вердикт дрона-ревьюера, и гейт мержа (mergeGate.ts) не пускает `gh pr merge`, пока
|
|
7076
|
+
* вердикта нет. До этой ручки демон знал только ГЛОБАЛЬНЫЙ env SWARM_AUTO_REVIEW
|
|
7077
|
+
* («поднимать ли ревьюеров на этой машине»), а настройка проекта до него не доезжала —
|
|
7078
|
+
* гейту же нужен ответ про КОНКРЕТНЫЙ проект. null (прод недоступен / repo не смэплен /
|
|
7079
|
+
* поля нет) → ридер (makeAutoReviewReader) возьмёт env, то есть прежнее поведение.
|
|
7080
|
+
*/
|
|
7081
|
+
async repoAutoReview(repoFullName) {
|
|
7082
|
+
let res;
|
|
7083
|
+
try {
|
|
7084
|
+
res = await this.call("swarm_repo_config", { repoFullName });
|
|
7085
|
+
} catch {
|
|
7086
|
+
return null;
|
|
7087
|
+
}
|
|
7088
|
+
if (res?.isError) return null;
|
|
7089
|
+
const sc = res.structuredContent ?? {};
|
|
7090
|
+
const v = sc.config?.autoReview;
|
|
7091
|
+
return typeof v === "boolean" ? v : null;
|
|
7092
|
+
}
|
|
7028
7093
|
/**
|
|
7029
7094
|
* TASWM-291: МАШИНЫ роя владельца (swarm_list_hosts) — чтобы `swarm spawn --host
|
|
7030
7095
|
* <имя>` резолвил человеческое имя в hostId, а `swarm hosts` показывал, какие
|
|
@@ -10894,6 +10959,15 @@ function makeExclusiveReader(opts) {
|
|
|
10894
10959
|
...opts.now !== void 0 ? { now: opts.now } : {}
|
|
10895
10960
|
});
|
|
10896
10961
|
}
|
|
10962
|
+
function makeAutoReviewReader(opts) {
|
|
10963
|
+
return makeRepoValueReader({
|
|
10964
|
+
fetch: opts.fetch,
|
|
10965
|
+
fallback: opts.fallback,
|
|
10966
|
+
valid: (v) => typeof v === "boolean",
|
|
10967
|
+
...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {},
|
|
10968
|
+
...opts.now !== void 0 ? { now: opts.now } : {}
|
|
10969
|
+
});
|
|
10970
|
+
}
|
|
10897
10971
|
function makeDroneCapReader(opts) {
|
|
10898
10972
|
const read = makeRepoValueReader({
|
|
10899
10973
|
fetch: opts.fetch,
|
|
@@ -10909,6 +10983,39 @@ function makeDroneCapReader(opts) {
|
|
|
10909
10983
|
};
|
|
10910
10984
|
}
|
|
10911
10985
|
|
|
10986
|
+
// src/mergeGate.ts
|
|
10987
|
+
init_src();
|
|
10988
|
+
function prNumberOf(ref) {
|
|
10989
|
+
const s = (ref || "").trim();
|
|
10990
|
+
if (!s) return null;
|
|
10991
|
+
const url = s.match(/\/pull\/(\d+)/);
|
|
10992
|
+
if (url) return Number(url[1]);
|
|
10993
|
+
const bare = s.match(/^#?(\d+)$/);
|
|
10994
|
+
return bare ? Number(bare[1]) : null;
|
|
10995
|
+
}
|
|
10996
|
+
function findRunByPr(runs, ref) {
|
|
10997
|
+
const num = prNumberOf(ref);
|
|
10998
|
+
if (num == null) return null;
|
|
10999
|
+
let best = null;
|
|
11000
|
+
for (const r of runs) {
|
|
11001
|
+
if (!r.pr_url || prNumberOf(r.pr_url) !== num) continue;
|
|
11002
|
+
if (!best || r.id > best.id) best = r;
|
|
11003
|
+
}
|
|
11004
|
+
return best;
|
|
11005
|
+
}
|
|
11006
|
+
function makeMergeGateResolver(deps) {
|
|
11007
|
+
return async (ref) => {
|
|
11008
|
+
const run = findRunByPr(deps.listRuns(), ref);
|
|
11009
|
+
if (!run) return { ...computeMergeGate({ hasRun: false, autoReview: false, reviewVerdict: null }), run: null };
|
|
11010
|
+
const autoReview = deps.autoReview ? await deps.autoReview(run.repo).catch(() => deps.fallbackAutoReview) : deps.fallbackAutoReview;
|
|
11011
|
+
const decision = computeMergeGate({ hasRun: true, autoReview, reviewVerdict: run.review_verdict });
|
|
11012
|
+
return {
|
|
11013
|
+
...decision,
|
|
11014
|
+
run: { id: run.id, task_slug: run.task_slug, name: run.name, pr_url: run.pr_url, review_verdict: run.review_verdict }
|
|
11015
|
+
};
|
|
11016
|
+
};
|
|
11017
|
+
}
|
|
11018
|
+
|
|
10912
11019
|
// src/envGate.ts
|
|
10913
11020
|
import { existsSync as existsSync13 } from "fs";
|
|
10914
11021
|
import { join as join24 } from "path";
|
|
@@ -11347,7 +11454,7 @@ async function changedPaths(worktree, opts = {}) {
|
|
|
11347
11454
|
// package.json
|
|
11348
11455
|
var package_default = {
|
|
11349
11456
|
name: "@taskless-app/swarm",
|
|
11350
|
-
version: "0.6.
|
|
11457
|
+
version: "0.6.8",
|
|
11351
11458
|
type: "module",
|
|
11352
11459
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
11353
11460
|
keywords: [
|
|
@@ -11397,7 +11504,7 @@ var package_default = {
|
|
|
11397
11504
|
execa: "^10.0.0",
|
|
11398
11505
|
zod: "3.25.76"
|
|
11399
11506
|
},
|
|
11400
|
-
gitHead: "
|
|
11507
|
+
gitHead: "c06a3749a3ef7d0c8fcae83fd79b84ba2318859b"
|
|
11401
11508
|
};
|
|
11402
11509
|
|
|
11403
11510
|
// src/version.ts
|
|
@@ -12820,6 +12927,12 @@ function makeHandler(deps) {
|
|
|
12820
12927
|
hostId: deps.selfHost?.hostId ?? null
|
|
12821
12928
|
});
|
|
12822
12929
|
}
|
|
12930
|
+
if (p === "/merge-gate") {
|
|
12931
|
+
const ref = url.searchParams.get("pr") ?? "";
|
|
12932
|
+
if (!ref.trim()) return Response.json({ error: "\u043D\u0443\u0436\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 pr=<url|\u043D\u043E\u043C\u0435\u0440>" }, { status: 400 });
|
|
12933
|
+
const resolve4 = deps.mergeGate ?? makeMergeGateResolver({ listRuns: () => deps.db.listRuns(), fallbackAutoReview: SWARM_AUTO_REVIEW });
|
|
12934
|
+
return Response.json(await resolve4(ref));
|
|
12935
|
+
}
|
|
12823
12936
|
if (p === "/resources") return Response.json(hostResourceSnapshot());
|
|
12824
12937
|
if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
|
|
12825
12938
|
const diffMatch = p.match(/^\/run\/(\d+)\/diff$/);
|
|
@@ -16703,6 +16816,7 @@ async function runDaemon(opts) {
|
|
|
16703
16816
|
let envGate;
|
|
16704
16817
|
let exclusiveProject;
|
|
16705
16818
|
let projectDroneCap;
|
|
16819
|
+
let autoReviewProject;
|
|
16706
16820
|
if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
|
|
16707
16821
|
const client = db;
|
|
16708
16822
|
verifyModeReader = makeVerifyModeReader({
|
|
@@ -16749,6 +16863,14 @@ async function runDaemon(opts) {
|
|
|
16749
16863
|
},
|
|
16750
16864
|
ttlMs: 6e4
|
|
16751
16865
|
});
|
|
16866
|
+
autoReviewProject = makeAutoReviewReader({
|
|
16867
|
+
fetch: async (repo) => {
|
|
16868
|
+
const repoFullName = await remoteRepoFullName(repo).catch(() => null);
|
|
16869
|
+
return repoFullName ? client.repoAutoReview(repoFullName) : null;
|
|
16870
|
+
},
|
|
16871
|
+
fallback: SWARM_AUTO_REVIEW,
|
|
16872
|
+
ttlMs: 6e4
|
|
16873
|
+
});
|
|
16752
16874
|
}
|
|
16753
16875
|
let spawnReviewerFn;
|
|
16754
16876
|
let verdictCommentFn;
|
|
@@ -17317,6 +17439,15 @@ async function runDaemon(opts) {
|
|
|
17317
17439
|
// печатает `swarm status`. Без него человек снова гадал бы, какие репозитории машина
|
|
17318
17440
|
// считает своими и в каком дереве работает при двух копиях одного клона.
|
|
17319
17441
|
repoMap: () => repoMapRows(repoMapState),
|
|
17442
|
+
// Гейт мержа (TASWM-355): ручка /merge-gate отвечает по ЖИВОЙ настройке autoReview того
|
|
17443
|
+
// проекта, которому принадлежит PR (ридер выше), а не по глобальному env машины. Именно
|
|
17444
|
+
// этот ответ и превращается в запрет: PreToolUse-хук на `gh pr merge` спрашивает его
|
|
17445
|
+
// через `swarm merge-gate` и отказывает в команде, пока вердикта ревьюера нет.
|
|
17446
|
+
mergeGate: makeMergeGateResolver({
|
|
17447
|
+
listRuns: () => db.listRuns(),
|
|
17448
|
+
...autoReviewProject ? { autoReview: autoReviewProject } : {},
|
|
17449
|
+
fallbackAutoReview: SWARM_AUTO_REVIEW
|
|
17450
|
+
}),
|
|
17320
17451
|
// Оператор-пауза + «Оживить» (TASWM-214 Фаза 2): тот же shared-флаг, что читает тик;
|
|
17321
17452
|
// narrate — операторские действия видны в ленте роя; revive — сброс залипшего шторма.
|
|
17322
17453
|
control,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taskless-app/swarm",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
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": [
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"execa": "^10.0.0",
|
|
39
39
|
"zod": "3.25.76"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "c06a3749a3ef7d0c8fcae83fd79b84ba2318859b",
|
|
42
42
|
"scripts": {
|
|
43
43
|
"overmind": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/overmind.ts",
|
|
44
44
|
"swarm": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/cli.ts",
|