@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 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.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: "89e3fe0078755d130512c75ac85f195e6b950392"
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.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: "89e3fe0078755d130512c75ac85f195e6b950392"
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.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": "89e3fe0078755d130512c75ac85f195e6b950392",
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",