@taskless-app/swarm 0.6.7 → 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;
@@ -1176,6 +1205,7 @@ var init_src = __esm({
1176
1205
  init_types();
1177
1206
  init_timeline();
1178
1207
  init_reviewPhase();
1208
+ init_mergeGate();
1179
1209
  init_status();
1180
1210
  init_deps();
1181
1211
  init_time();
@@ -2005,7 +2035,7 @@ var init_package = __esm({
2005
2035
  "package.json"() {
2006
2036
  package_default = {
2007
2037
  name: "@taskless-app/swarm",
2008
- version: "0.6.7",
2038
+ version: "0.6.8",
2009
2039
  type: "module",
2010
2040
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
2011
2041
  keywords: [
@@ -2055,7 +2085,7 @@ var init_package = __esm({
2055
2085
  execa: "^10.0.0",
2056
2086
  zod: "3.25.76"
2057
2087
  },
2058
- gitHead: "08ee2a1a4bdb53ba1c25c92066104f0de3a23433"
2088
+ gitHead: "c06a3749a3ef7d0c8fcae83fd79b84ba2318859b"
2059
2089
  };
2060
2090
  }
2061
2091
  });
@@ -2370,6 +2400,16 @@ async function daemonStatus2() {
2370
2400
  return null;
2371
2401
  }
2372
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
+ }
2373
2413
  var TIMEOUT_MS, ACTION_TIMEOUT_MS, BASE, NO_DAEMON_NOTE;
2374
2414
  var init_daemonLink = __esm({
2375
2415
  "src/daemonLink.ts"() {
@@ -5636,6 +5676,27 @@ var SwarmClient = class _SwarmClient {
5636
5676
  const v = sc.config?.maxDrones;
5637
5677
  return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
5638
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
+ }
5639
5700
  /**
5640
5701
  * TASWM-291: МАШИНЫ роя владельца (swarm_list_hosts) — чтобы `swarm spawn --host
5641
5702
  * <имя>` резолвил человеческое имя в hostId, а `swarm hosts` показывал, какие
@@ -7119,6 +7180,41 @@ async function ghChecks(pr, run = realChecksRunner) {
7119
7180
 
7120
7181
  // src/web.ts
7121
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
7122
7218
  init_droneChannel();
7123
7219
 
7124
7220
  // src/oauth.ts
@@ -8670,6 +8766,12 @@ function makeHandler(deps) {
8670
8766
  hostId: deps.selfHost?.hostId ?? null
8671
8767
  });
8672
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
+ }
8673
8775
  if (p === "/resources") return Response.json(hostResourceSnapshot());
8674
8776
  if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
8675
8777
  const diffMatch = p.match(/^\/run\/(\d+)\/diff$/);
@@ -10182,6 +10284,9 @@ function usage() {
10182
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
10183
10285
  swarm cancel <slug> <\u043F\u0440\u0438\u0447\u0438\u043D\u0430\u2026>
10184
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
10185
10290
  swarm questions \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0434\u0440\u043E\u043D\u043E\u0432
10186
10291
  swarm answer <qid> <\u0442\u0435\u043A\u0441\u0442\u2026>
10187
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
@@ -10466,6 +10571,23 @@ if (cmd === "status") {
10466
10571
  for (const line of repoMapLines(st.repos)) console.log(line);
10467
10572
  process.exit(0);
10468
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
+ }
10469
10591
  if (cmd === "pause" || cmd === "resume") {
10470
10592
  const wantPause = cmd === "pause";
10471
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;
@@ -1336,6 +1362,7 @@ var init_src = __esm({
1336
1362
  init_types();
1337
1363
  init_timeline();
1338
1364
  init_reviewPhase();
1365
+ init_mergeGate();
1339
1366
  init_status();
1340
1367
  init_deps();
1341
1368
  init_time();
@@ -7042,6 +7069,27 @@ var SwarmClient = class _SwarmClient {
7042
7069
  const v = sc.config?.maxDrones;
7043
7070
  return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
7044
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
+ }
7045
7093
  /**
7046
7094
  * TASWM-291: МАШИНЫ роя владельца (swarm_list_hosts) — чтобы `swarm spawn --host
7047
7095
  * <имя>` резолвил человеческое имя в hostId, а `swarm hosts` показывал, какие
@@ -10911,6 +10959,15 @@ function makeExclusiveReader(opts) {
10911
10959
  ...opts.now !== void 0 ? { now: opts.now } : {}
10912
10960
  });
10913
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
+ }
10914
10971
  function makeDroneCapReader(opts) {
10915
10972
  const read = makeRepoValueReader({
10916
10973
  fetch: opts.fetch,
@@ -10926,6 +10983,39 @@ function makeDroneCapReader(opts) {
10926
10983
  };
10927
10984
  }
10928
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
+
10929
11019
  // src/envGate.ts
10930
11020
  import { existsSync as existsSync13 } from "fs";
10931
11021
  import { join as join24 } from "path";
@@ -11364,7 +11454,7 @@ async function changedPaths(worktree, opts = {}) {
11364
11454
  // package.json
11365
11455
  var package_default = {
11366
11456
  name: "@taskless-app/swarm",
11367
- version: "0.6.7",
11457
+ version: "0.6.8",
11368
11458
  type: "module",
11369
11459
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
11370
11460
  keywords: [
@@ -11414,7 +11504,7 @@ var package_default = {
11414
11504
  execa: "^10.0.0",
11415
11505
  zod: "3.25.76"
11416
11506
  },
11417
- gitHead: "08ee2a1a4bdb53ba1c25c92066104f0de3a23433"
11507
+ gitHead: "c06a3749a3ef7d0c8fcae83fd79b84ba2318859b"
11418
11508
  };
11419
11509
 
11420
11510
  // src/version.ts
@@ -12837,6 +12927,12 @@ function makeHandler(deps) {
12837
12927
  hostId: deps.selfHost?.hostId ?? null
12838
12928
  });
12839
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
+ }
12840
12936
  if (p === "/resources") return Response.json(hostResourceSnapshot());
12841
12937
  if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
12842
12938
  const diffMatch = p.match(/^\/run\/(\d+)\/diff$/);
@@ -16720,6 +16816,7 @@ async function runDaemon(opts) {
16720
16816
  let envGate;
16721
16817
  let exclusiveProject;
16722
16818
  let projectDroneCap;
16819
+ let autoReviewProject;
16723
16820
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
16724
16821
  const client = db;
16725
16822
  verifyModeReader = makeVerifyModeReader({
@@ -16766,6 +16863,14 @@ async function runDaemon(opts) {
16766
16863
  },
16767
16864
  ttlMs: 6e4
16768
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
+ });
16769
16874
  }
16770
16875
  let spawnReviewerFn;
16771
16876
  let verdictCommentFn;
@@ -17334,6 +17439,15 @@ async function runDaemon(opts) {
17334
17439
  // печатает `swarm status`. Без него человек снова гадал бы, какие репозитории машина
17335
17440
  // считает своими и в каком дереве работает при двух копиях одного клона.
17336
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
+ }),
17337
17451
  // Оператор-пауза + «Оживить» (TASWM-214 Фаза 2): тот же shared-флаг, что читает тик;
17338
17452
  // narrate — операторские действия видны в ленте роя; revive — сброс залипшего шторма.
17339
17453
  control,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.7",
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": "08ee2a1a4bdb53ba1c25c92066104f0de3a23433",
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",