@taskless-app/swarm 0.6.7 → 0.6.9

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
@@ -110,7 +110,11 @@ var init_types = __esm({
110
110
  // прогона перед самым спавном: «поехал молча» означало бы, что причина запуска исчезла из
111
111
  // истории, и следующий разбор «почему он стартовал поверх незакрытого блокера» упёрся бы в
112
112
  // пустоту. Успешное снятие (блокер смержен) события не порождает — это штатный ход.
113
- "dep_cleared"
113
+ "dep_cleared",
114
+ // TASWM-412: дрон вернул сырую задачу автору (см. RunStatus "returned"). Веха, а не step:
115
+ // прогон на этом кончается, и в истории должно быть видно, что он кончился ВОЗВРАТОМ —
116
+ // иначе возврат читается как брошенная посреди работы задача.
117
+ "returned"
114
118
  ];
115
119
  MILESTONE_KINDS = [
116
120
  "spawned",
@@ -133,8 +137,10 @@ var init_types = __esm({
133
137
  // надзор overmind (TASWM-194) — та же нотификация, что улетела в инбокс, вехой в Истории
134
138
  "note",
135
139
  // заметка дрона «замечено по пути» (TASWM-279) — веха, иначе находка тонет в ленте step'ов
136
- "dep_cleared"
140
+ "dep_cleared",
137
141
  // зависимость снята неуспешным финалом блокера (TASWM-361) — решение роя, веха как conflict
142
+ "returned"
143
+ // дрон вернул сырую задачу автору (TASWM-412) — финал прогона, веха как done/failed
138
144
  ];
139
145
  MILESTONE_KIND_SET = new Set(MILESTONE_KINDS);
140
146
  }
@@ -212,6 +218,14 @@ function buildSegments(created, events, laneEnd) {
212
218
  push(e.at);
213
219
  phase = "superseded";
214
220
  break;
221
+ // TASWM-412: возврат автору — конец прогона, хвост дорожки не должен дорисовываться
222
+ // «работающим» зелёным (та же готча, что чинил TASWM-166 для stopped/superseded).
223
+ // Фаза — ЧУЖАЯ, "stopped": своя фаза значит свой цвет-токен в обеих темах web, то есть
224
+ // правку дизайн-системы; здесь важнее, что полоса перестаёт врать про живую работу.
225
+ case "returned":
226
+ push(e.at);
227
+ phase = "stopped";
228
+ break;
215
229
  }
216
230
  }
217
231
  push(laneEnd);
@@ -249,6 +263,8 @@ function computeBlocker(status, stopReason, lastStep, pr, ci, deps, openQ) {
249
263
  return { icon: "\u23F9", text: stopReason || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", tone: "stopped" };
250
264
  case "superseded":
251
265
  return { icon: "\u21BA", text: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442", tone: "superseded" };
266
+ case "returned":
267
+ return { icon: "\u21A9", text: stopReason || "\u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u0430 \u0430\u0432\u0442\u043E\u0440\u0443 \u043D\u0430 \u0443\u0442\u043E\u0447\u043D\u0435\u043D\u0438\u0435", tone: "stopped" };
252
268
  case "done":
253
269
  return { icon: "\u2705", text: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", tone: "done" };
254
270
  default:
@@ -322,6 +338,35 @@ var init_reviewPhase = __esm({
322
338
  }
323
339
  });
324
340
 
341
+ // ../swarm-core/src/mergeGate.ts
342
+ function computeMergeGate(input) {
343
+ if (!input.hasRun) {
344
+ 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" };
345
+ }
346
+ if (!input.autoReview) {
347
+ 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" };
348
+ }
349
+ if (input.reviewVerdict === "approve") {
350
+ 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" };
351
+ }
352
+ if (input.reviewVerdict === "changes") {
353
+ 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" };
354
+ }
355
+ return {
356
+ allowed: false,
357
+ code: "no_verdict",
358
+ 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)"
359
+ };
360
+ }
361
+ function mergeGateLine(d) {
362
+ 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}`;
363
+ }
364
+ var init_mergeGate = __esm({
365
+ "../swarm-core/src/mergeGate.ts"() {
366
+ "use strict";
367
+ }
368
+ });
369
+
325
370
  // ../swarm-core/src/status.ts
326
371
  function statusMeta(status) {
327
372
  return STATUS_META[status] ?? FALLBACK;
@@ -354,7 +399,11 @@ var init_status = __esm({
354
399
  storm_dead: { pill: "storm", label: "\u0448\u0442\u043E\u0440\u043C \u2014 \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D", color: "--warning", terminal: false },
355
400
  // умер в шторм Anthropic, поднимется после отбоя (TASWM-23)
356
401
  stopped: { pill: "stopped", label: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", color: "--muted-foreground", terminal: true },
357
- superseded: { pill: "superseded", label: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D", color: "--muted-foreground", terminal: true }
402
+ superseded: { pill: "superseded", label: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D", color: "--muted-foreground", terminal: true },
403
+ // TASWM-412: дрон вернул сырую задачу автору (вопросы комментом, задача назначена на автора
404
+ // и переведена в работу) и освободил слот. Терминальный — работа этого прогона закончена;
405
+ // цвет предупреждающий, а не destructive: смотреть должен АВТОР задачи, чинить нечего.
406
+ returned: { pill: "returned", label: "\u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u0430 \u0430\u0432\u0442\u043E\u0440\u0443", color: "--warning", terminal: true }
358
407
  };
359
408
  FALLBACK = STATUS_META.queued;
360
409
  TERMINAL_STATUSES = Object.keys(STATUS_META).filter(
@@ -1176,6 +1225,7 @@ var init_src = __esm({
1176
1225
  init_types();
1177
1226
  init_timeline();
1178
1227
  init_reviewPhase();
1228
+ init_mergeGate();
1179
1229
  init_status();
1180
1230
  init_deps();
1181
1231
  init_time();
@@ -2005,7 +2055,7 @@ var init_package = __esm({
2005
2055
  "package.json"() {
2006
2056
  package_default = {
2007
2057
  name: "@taskless-app/swarm",
2008
- version: "0.6.7",
2058
+ version: "0.6.9",
2009
2059
  type: "module",
2010
2060
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
2011
2061
  keywords: [
@@ -2055,7 +2105,7 @@ var init_package = __esm({
2055
2105
  execa: "^10.0.0",
2056
2106
  zod: "3.25.76"
2057
2107
  },
2058
- gitHead: "08ee2a1a4bdb53ba1c25c92066104f0de3a23433"
2108
+ gitHead: "cefe25ffbc0e9791234055c8ba552ff3f8d07a3f"
2059
2109
  };
2060
2110
  }
2061
2111
  });
@@ -2370,6 +2420,16 @@ async function daemonStatus2() {
2370
2420
  return null;
2371
2421
  }
2372
2422
  }
2423
+ async function daemonMergeGate(ref) {
2424
+ try {
2425
+ const res = await fetch(`${BASE}/merge-gate?pr=${encodeURIComponent(ref)}`, { signal: AbortSignal.timeout(TIMEOUT_MS) });
2426
+ if (!res.ok) return null;
2427
+ const body = await res.json();
2428
+ return typeof body?.allowed === "boolean" && typeof body?.reason === "string" ? body : null;
2429
+ } catch {
2430
+ return null;
2431
+ }
2432
+ }
2373
2433
  var TIMEOUT_MS, ACTION_TIMEOUT_MS, BASE, NO_DAEMON_NOTE;
2374
2434
  var init_daemonLink = __esm({
2375
2435
  "src/daemonLink.ts"() {
@@ -5636,6 +5696,27 @@ var SwarmClient = class _SwarmClient {
5636
5696
  const v = sc.config?.maxDrones;
5637
5697
  return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
5638
5698
  }
5699
+ /**
5700
+ * Live АВТОРЕВЬЮ проекта из Taskless (TASWM-355): тот же swarm_repo_config по
5701
+ * repoFullName — Project.swarmConfig.autoReview. true → у проекта перед мержем обязан быть
5702
+ * вердикт дрона-ревьюера, и гейт мержа (mergeGate.ts) не пускает `gh pr merge`, пока
5703
+ * вердикта нет. До этой ручки демон знал только ГЛОБАЛЬНЫЙ env SWARM_AUTO_REVIEW
5704
+ * («поднимать ли ревьюеров на этой машине»), а настройка проекта до него не доезжала —
5705
+ * гейту же нужен ответ про КОНКРЕТНЫЙ проект. null (прод недоступен / repo не смэплен /
5706
+ * поля нет) → ридер (makeAutoReviewReader) возьмёт env, то есть прежнее поведение.
5707
+ */
5708
+ async repoAutoReview(repoFullName) {
5709
+ let res;
5710
+ try {
5711
+ res = await this.call("swarm_repo_config", { repoFullName });
5712
+ } catch {
5713
+ return null;
5714
+ }
5715
+ if (res?.isError) return null;
5716
+ const sc = res.structuredContent ?? {};
5717
+ const v = sc.config?.autoReview;
5718
+ return typeof v === "boolean" ? v : null;
5719
+ }
5639
5720
  /**
5640
5721
  * TASWM-291: МАШИНЫ роя владельца (swarm_list_hosts) — чтобы `swarm spawn --host
5641
5722
  * <имя>` резолвил человеческое имя в hostId, а `swarm hosts` показывал, какие
@@ -7119,6 +7200,41 @@ async function ghChecks(pr, run = realChecksRunner) {
7119
7200
 
7120
7201
  // src/web.ts
7121
7202
  init_config();
7203
+
7204
+ // src/mergeGate.ts
7205
+ init_src();
7206
+ function prNumberOf(ref) {
7207
+ const s = (ref || "").trim();
7208
+ if (!s) return null;
7209
+ const url = s.match(/\/pull\/(\d+)/);
7210
+ if (url) return Number(url[1]);
7211
+ const bare = s.match(/^#?(\d+)$/);
7212
+ return bare ? Number(bare[1]) : null;
7213
+ }
7214
+ function findRunByPr(runs, ref) {
7215
+ const num = prNumberOf(ref);
7216
+ if (num == null) return null;
7217
+ let best = null;
7218
+ for (const r of runs) {
7219
+ if (!r.pr_url || prNumberOf(r.pr_url) !== num) continue;
7220
+ if (!best || r.id > best.id) best = r;
7221
+ }
7222
+ return best;
7223
+ }
7224
+ function makeMergeGateResolver(deps) {
7225
+ return async (ref) => {
7226
+ const run = findRunByPr(deps.listRuns(), ref);
7227
+ if (!run) return { ...computeMergeGate({ hasRun: false, autoReview: false, reviewVerdict: null }), run: null };
7228
+ const autoReview = deps.autoReview ? await deps.autoReview(run.repo).catch(() => deps.fallbackAutoReview) : deps.fallbackAutoReview;
7229
+ const decision = computeMergeGate({ hasRun: true, autoReview, reviewVerdict: run.review_verdict });
7230
+ return {
7231
+ ...decision,
7232
+ run: { id: run.id, task_slug: run.task_slug, name: run.name, pr_url: run.pr_url, review_verdict: run.review_verdict }
7233
+ };
7234
+ };
7235
+ }
7236
+
7237
+ // src/web.ts
7122
7238
  init_droneChannel();
7123
7239
 
7124
7240
  // src/oauth.ts
@@ -8670,6 +8786,12 @@ function makeHandler(deps) {
8670
8786
  hostId: deps.selfHost?.hostId ?? null
8671
8787
  });
8672
8788
  }
8789
+ if (p === "/merge-gate") {
8790
+ const ref = url.searchParams.get("pr") ?? "";
8791
+ 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 });
8792
+ const resolve4 = deps.mergeGate ?? makeMergeGateResolver({ listRuns: () => deps.db.listRuns(), fallbackAutoReview: SWARM_AUTO_REVIEW });
8793
+ return Response.json(await resolve4(ref));
8794
+ }
8673
8795
  if (p === "/resources") return Response.json(hostResourceSnapshot());
8674
8796
  if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
8675
8797
  const diffMatch = p.match(/^\/run\/(\d+)\/diff$/);
@@ -10182,6 +10304,9 @@ function usage() {
10182
10304
  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
10305
  swarm cancel <slug> <\u043F\u0440\u0438\u0447\u0438\u043D\u0430\u2026>
10184
10306
  \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
10307
+ 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
10308
+ \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.
10309
+ \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
10310
  swarm questions \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0434\u0440\u043E\u043D\u043E\u0432
10186
10311
  swarm answer <qid> <\u0442\u0435\u043A\u0441\u0442\u2026>
10187
10312
  \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 +10591,23 @@ if (cmd === "status") {
10466
10591
  for (const line of repoMapLines(st.repos)) console.log(line);
10467
10592
  process.exit(0);
10468
10593
  }
10594
+ if (cmd === "merge-gate") {
10595
+ const ref = rawArgs[0];
10596
+ if (!ref) {
10597
+ console.error("\u043D\u0443\u0436\u043D\u043E: swarm merge-gate <\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 PR|\u043D\u043E\u043C\u0435\u0440>");
10598
+ process.exit(2);
10599
+ }
10600
+ const answer = await daemonMergeGate(ref);
10601
+ if (!answer) {
10602
+ console.error(
10603
+ `\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`
10604
+ );
10605
+ process.exit(1);
10606
+ }
10607
+ const who = answer.run ? ` (\u043F\u0440\u043E\u0433\u043E\u043D #${answer.run.id} ${answer.run.task_slug}, \u0434\u0440\u043E\u043D ${answer.run.name})` : "";
10608
+ console.log(`${mergeGateLine(answer)}${who}`);
10609
+ process.exit(answer.allowed ? 0 : 1);
10610
+ }
10469
10611
  if (cmd === "pause" || cmd === "resume") {
10470
10612
  const wantPause = cmd === "pause";
10471
10613
  const reason = wantPause ? rawArgs.join(" ").trim() || null : null;
package/dist/overmind.js CHANGED
@@ -75,7 +75,11 @@ var init_types = __esm({
75
75
  // прогона перед самым спавном: «поехал молча» означало бы, что причина запуска исчезла из
76
76
  // истории, и следующий разбор «почему он стартовал поверх незакрытого блокера» упёрся бы в
77
77
  // пустоту. Успешное снятие (блокер смержен) события не порождает — это штатный ход.
78
- "dep_cleared"
78
+ "dep_cleared",
79
+ // TASWM-412: дрон вернул сырую задачу автору (см. RunStatus "returned"). Веха, а не step:
80
+ // прогон на этом кончается, и в истории должно быть видно, что он кончился ВОЗВРАТОМ —
81
+ // иначе возврат читается как брошенная посреди работы задача.
82
+ "returned"
79
83
  ];
80
84
  MILESTONE_KINDS = [
81
85
  "spawned",
@@ -98,8 +102,10 @@ var init_types = __esm({
98
102
  // надзор overmind (TASWM-194) — та же нотификация, что улетела в инбокс, вехой в Истории
99
103
  "note",
100
104
  // заметка дрона «замечено по пути» (TASWM-279) — веха, иначе находка тонет в ленте step'ов
101
- "dep_cleared"
105
+ "dep_cleared",
102
106
  // зависимость снята неуспешным финалом блокера (TASWM-361) — решение роя, веха как conflict
107
+ "returned"
108
+ // дрон вернул сырую задачу автору (TASWM-412) — финал прогона, веха как done/failed
103
109
  ];
104
110
  MILESTONE_KIND_SET = new Set(MILESTONE_KINDS);
105
111
  }
@@ -177,6 +183,14 @@ function buildSegments(created, events, laneEnd) {
177
183
  push(e.at);
178
184
  phase = "superseded";
179
185
  break;
186
+ // TASWM-412: возврат автору — конец прогона, хвост дорожки не должен дорисовываться
187
+ // «работающим» зелёным (та же готча, что чинил TASWM-166 для stopped/superseded).
188
+ // Фаза — ЧУЖАЯ, "stopped": своя фаза значит свой цвет-токен в обеих темах web, то есть
189
+ // правку дизайн-системы; здесь важнее, что полоса перестаёт врать про живую работу.
190
+ case "returned":
191
+ push(e.at);
192
+ phase = "stopped";
193
+ break;
180
194
  }
181
195
  }
182
196
  push(laneEnd);
@@ -214,6 +228,8 @@ function computeBlocker(status, stopReason, lastStep, pr, ci, deps, openQ) {
214
228
  return { icon: "\u23F9", text: stopReason || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", tone: "stopped" };
215
229
  case "superseded":
216
230
  return { icon: "\u21BA", text: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442", tone: "superseded" };
231
+ case "returned":
232
+ return { icon: "\u21A9", text: stopReason || "\u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u0430 \u0430\u0432\u0442\u043E\u0440\u0443 \u043D\u0430 \u0443\u0442\u043E\u0447\u043D\u0435\u043D\u0438\u0435", tone: "stopped" };
217
233
  case "done":
218
234
  return { icon: "\u2705", text: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", tone: "done" };
219
235
  default:
@@ -319,6 +335,32 @@ var init_reviewPhase = __esm({
319
335
  }
320
336
  });
321
337
 
338
+ // ../swarm-core/src/mergeGate.ts
339
+ function computeMergeGate(input) {
340
+ if (!input.hasRun) {
341
+ 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" };
342
+ }
343
+ if (!input.autoReview) {
344
+ 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" };
345
+ }
346
+ if (input.reviewVerdict === "approve") {
347
+ 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" };
348
+ }
349
+ if (input.reviewVerdict === "changes") {
350
+ 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" };
351
+ }
352
+ return {
353
+ allowed: false,
354
+ code: "no_verdict",
355
+ 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)"
356
+ };
357
+ }
358
+ var init_mergeGate = __esm({
359
+ "../swarm-core/src/mergeGate.ts"() {
360
+ "use strict";
361
+ }
362
+ });
363
+
322
364
  // ../swarm-core/src/status.ts
323
365
  function isAwaitingCi(status, stopReason) {
324
366
  return status === "paused" && stopReason === AWAITING_CI_REASON;
@@ -354,7 +396,11 @@ var init_status = __esm({
354
396
  storm_dead: { pill: "storm", label: "\u0448\u0442\u043E\u0440\u043C \u2014 \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D", color: "--warning", terminal: false },
355
397
  // умер в шторм Anthropic, поднимется после отбоя (TASWM-23)
356
398
  stopped: { pill: "stopped", label: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", color: "--muted-foreground", terminal: true },
357
- superseded: { pill: "superseded", label: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D", color: "--muted-foreground", terminal: true }
399
+ superseded: { pill: "superseded", label: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D", color: "--muted-foreground", terminal: true },
400
+ // TASWM-412: дрон вернул сырую задачу автору (вопросы комментом, задача назначена на автора
401
+ // и переведена в работу) и освободил слот. Терминальный — работа этого прогона закончена;
402
+ // цвет предупреждающий, а не destructive: смотреть должен АВТОР задачи, чинить нечего.
403
+ returned: { pill: "returned", label: "\u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u0430 \u0430\u0432\u0442\u043E\u0440\u0443", color: "--warning", terminal: true }
358
404
  };
359
405
  AWAITING_CI_REASON = "\u0436\u0434\u0451\u0442 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 CI \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 PR \u0435\u0449\u0451 \u0438\u0434\u0443\u0442";
360
406
  FALLBACK = STATUS_META.queued;
@@ -1336,6 +1382,7 @@ var init_src = __esm({
1336
1382
  init_types();
1337
1383
  init_timeline();
1338
1384
  init_reviewPhase();
1385
+ init_mergeGate();
1339
1386
  init_status();
1340
1387
  init_deps();
1341
1388
  init_time();
@@ -4204,13 +4251,17 @@ __export(taskless_exports, {
4204
4251
  requireTaskBody: () => requireTaskBody,
4205
4252
  resetLegacyTokenWarning: () => resetLegacyTokenWarning,
4206
4253
  resolveApiToken: () => resolveApiToken,
4207
- swarmToolArgs: () => swarmToolArgs
4254
+ swarmToolArgs: () => swarmToolArgs,
4255
+ taskAuthor: () => taskAuthor
4208
4256
  });
4209
4257
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4210
4258
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4211
4259
  function isClosingTaskCategory(category) {
4212
4260
  return category === "completed" || category === "cancelled" || category === "archived";
4213
4261
  }
4262
+ function taskAuthor(u) {
4263
+ return u?.id ? { id: u.id, name: u.displayName || "\u0430\u0432\u0442\u043E\u0440 \u0437\u0430\u0434\u0430\u0447\u0438" } : null;
4264
+ }
4214
4265
  function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl = fetch) {
4215
4266
  const ws = workspaceId ? { workspaceId } : {};
4216
4267
  const idCache = /* @__PURE__ */ new Map();
@@ -4250,7 +4301,10 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
4250
4301
  documents: (task.documents ?? []).filter((d) => !!d.id).map((d) => ({ id: d.id, title: d.title ?? d.id, url: d.url ?? null })),
4251
4302
  // TASWM-283: только ССЫЛКИ (порядок = порядок в задаче). Скачивание — на спавне,
4252
4303
  // в worktree конкретного дрона; getTask зовут и там, где выкладывать некуда.
4253
- attachments: collectTaskAttachments({ description: task.description, comments: task.comments ?? [] })
4304
+ attachments: collectTaskAttachments({ description: task.description, comments: task.comments ?? [] }),
4305
+ // TASWM-412: creator, иначе requester. Прод старее этой правки автора не отдаёт вовсе —
4306
+ // тогда null, и возврат просто обойдётся без переназначения (комментарий важнее).
4307
+ author: taskAuthor(task.creator) ?? taskAuthor(task.requester)
4254
4308
  };
4255
4309
  idCache.set(slug, body.id);
4256
4310
  idCache.set(body.id, body.id);
@@ -4306,6 +4360,17 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
4306
4360
  startTask: (idOrSlug) => transition("start_task", idOrSlug),
4307
4361
  // TASWM-294: intent — «человек, посмотри», а не «Готово» (см. докблок в TasklessClient).
4308
4362
  finishTask: (idOrSlug) => transition("finish_task", idOrSlug, { intent: "needs_check" }),
4363
+ // TASWM-412: назначение — через общий update_task (тот же путь, что и у UI-роута),
4364
+ // отдельного «assign»-тула у Taskless нет. Успех считаем по факту ответа тула:
4365
+ // safeCall уже отсеял isError и падения, вернув null.
4366
+ assignTask: async (idOrSlug, userId) => {
4367
+ const id = await resolveId(idOrSlug);
4368
+ if (!id) {
4369
+ console.error(`[taskless] update_task(assigneeId) \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: \u0437\u0430\u0434\u0430\u0447\u0430 ${idOrSlug} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430`);
4370
+ return false;
4371
+ }
4372
+ return await safeCall("update_task", { id, assigneeId: userId }) != null;
4373
+ },
4309
4374
  uploadShot: (file) => uploadShot(safeCall, file, fetchImpl),
4310
4375
  addComment: async (idOrSlug, body, label, attachments) => {
4311
4376
  const id = await resolveId(idOrSlug);
@@ -7042,6 +7107,27 @@ var SwarmClient = class _SwarmClient {
7042
7107
  const v = sc.config?.maxDrones;
7043
7108
  return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
7044
7109
  }
7110
+ /**
7111
+ * Live АВТОРЕВЬЮ проекта из Taskless (TASWM-355): тот же swarm_repo_config по
7112
+ * repoFullName — Project.swarmConfig.autoReview. true → у проекта перед мержем обязан быть
7113
+ * вердикт дрона-ревьюера, и гейт мержа (mergeGate.ts) не пускает `gh pr merge`, пока
7114
+ * вердикта нет. До этой ручки демон знал только ГЛОБАЛЬНЫЙ env SWARM_AUTO_REVIEW
7115
+ * («поднимать ли ревьюеров на этой машине»), а настройка проекта до него не доезжала —
7116
+ * гейту же нужен ответ про КОНКРЕТНЫЙ проект. null (прод недоступен / repo не смэплен /
7117
+ * поля нет) → ридер (makeAutoReviewReader) возьмёт env, то есть прежнее поведение.
7118
+ */
7119
+ async repoAutoReview(repoFullName) {
7120
+ let res;
7121
+ try {
7122
+ res = await this.call("swarm_repo_config", { repoFullName });
7123
+ } catch {
7124
+ return null;
7125
+ }
7126
+ if (res?.isError) return null;
7127
+ const sc = res.structuredContent ?? {};
7128
+ const v = sc.config?.autoReview;
7129
+ return typeof v === "boolean" ? v : null;
7130
+ }
7045
7131
  /**
7046
7132
  * TASWM-291: МАШИНЫ роя владельца (swarm_list_hosts) — чтобы `swarm spawn --host
7047
7133
  * <имя>` резолвил человеческое имя в hostId, а `swarm hosts` показывал, какие
@@ -9703,6 +9789,7 @@ async function buildDroneServer(store, opts = {}) {
9703
9789
  const server = new McpServer({ name: "overmind-drone", version: "0.1.0" });
9704
9790
  const overmindDead = async () => !await store.alive();
9705
9791
  const slugForInstruction = (fallback) => opts.expectedTaskSlug ?? fallback ?? "<slug>";
9792
+ const iAmReviewer = (run) => run?.reviews_run_id != null || run?.kind === "reviewer" || opts.reviewsRunId != null || opts.runKind === "reviewer";
9706
9793
  const freeze = async (withdraw = false) => {
9707
9794
  let slug;
9708
9795
  let branch;
@@ -9855,6 +9942,120 @@ ${notFoundText}` : notFoundText }],
9855
9942
  return { content: [{ type: "text", text: "\u0437\u0430\u043C\u0435\u0442\u043A\u0430 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u0430 \u0438 \u0443\u0435\u0434\u0435\u0442 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443; \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0440\u0430\u0431\u043E\u0442\u0443 \u2014 \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0435 \u0436\u0434\u0438." }] };
9856
9943
  })
9857
9944
  );
9945
+ server.tool(
9946
+ "return_task",
9947
+ "\u0412\u0435\u0440\u043D\u0443\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 \u0410\u0412\u0422\u041E\u0420\u0423 \u043D\u0430 \u0443\u0442\u043E\u0447\u043D\u0435\u043D\u0438\u0435 \u0438 \u0437\u0430\u043A\u0440\u044B\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D (\u0441\u043B\u043E\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u0441\u044F, \u0440\u043E\u0439 \u0432\u043E\u0437\u044C\u043C\u0451\u0442 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443). \u0417\u043E\u0432\u0438, \u0422\u041E\u041B\u042C\u041A\u041E \u0435\u0441\u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435\u043B\u044C\u0437\u044F \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u043D\u0438 \u0432 \u043E\u0434\u043D\u043E\u043C \u0440\u0430\u0437\u0443\u043C\u043D\u043E\u043C \u0442\u043E\u043B\u043A\u043E\u0432\u0430\u043D\u0438\u0438 \u0418\u041B\u0418 \u043D\u0435\u043F\u043E\u043D\u044F\u0442\u043D\u043E, \u0447\u0435\u043C \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0442\u044C \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442. \u0414\u0432\u0430 \u043E\u0441\u043C\u044B\u0441\u043B\u0435\u043D\u043D\u044B\u0445 \u0442\u043E\u043B\u043A\u043E\u0432\u0430\u043D\u0438\u044F \u2014 \u041D\u0415 \u043F\u043E\u0432\u043E\u0434: \u0432\u044B\u0431\u0435\u0440\u0438 \u043E\u0447\u0435\u0432\u0438\u0434\u043D\u043E\u0435, \u0441\u0434\u0435\u043B\u0430\u0439 \u0438 \u043D\u0430\u0437\u043E\u0432\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0443 \u0432 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0438 PR. \u041F\u043E\u043D\u044F\u0442\u043D\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430, \u043A\u043E\u0442\u043E\u0440\u0430\u044F \u0432 \u043E\u0434\u043D\u043E\u043C \u043F\u0443\u043D\u043A\u0442\u0435 \u043F\u0440\u043E\u0442\u0438\u0432\u043E\u0440\u0435\u0447\u0438\u0442 \u043F\u0440\u0430\u0432\u0438\u043B\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430, \u2014 \u0442\u043E\u0436\u0435 \u043D\u0435 \u043F\u043E\u0432\u043E\u0434: \u044D\u0442\u043E ask. \u0417\u0430\u0434\u0430\u0447\u0430 \u0443\u0439\u0434\u0451\u0442 \u0430\u0432\u0442\u043E\u0440\u0443 (\u043D\u0430\u0437\u043D\u0430\u0447\u0438\u043C \u043D\u0430 \u043D\u0435\u0433\u043E \u0438 \u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0451\u043C \u0432 \u0440\u0430\u0431\u043E\u0442\u0443), \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0443\u0435\u0434\u0443\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u043E\u0442 \u0442\u0432\u043E\u0435\u0433\u043E \u0438\u043C\u0435\u043D\u0438. \u041F\u043E\u0441\u043B\u0435 \u0432\u044B\u0437\u043E\u0432\u0430 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0437\u043E\u0432\u0438 \u2014 \u0437\u0430\u0432\u0435\u0440\u0448\u0438 \u0441\u0435\u0441\u0441\u0438\u044E.",
9948
+ {
9949
+ missing: z.string().min(30).describe("\u0447\u0435\u0433\u043E \u0432 \u0437\u0430\u0434\u0430\u0447\u0435 \u043D\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442, \u0447\u0442\u043E\u0431\u044B \u0435\u0451 \u0441\u0434\u0435\u043B\u0430\u0442\u044C \u0438\u043B\u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u2014 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E, \u0430 \u043D\u0435 \xAB\u043D\u0435\u043F\u043E\u043D\u044F\u0442\u043D\u043E\xBB"),
9950
+ readings: z.array(z.string().min(10)).optional().describe("\u0442\u043E\u043B\u043A\u043E\u0432\u0430\u043D\u0438\u044F \u043F\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0442\u044B \u0440\u0430\u0441\u0441\u043C\u043E\u0442\u0440\u0435\u043B (\u0438 \u0447\u0435\u043C \u043A\u0430\u0436\u0434\u043E\u0435 \u043F\u043B\u043E\u0445\u043E/\u043F\u0440\u043E\u0442\u0438\u0432\u043E\u0440\u0435\u0447\u0438\u0432\u043E)"),
9951
+ assumed: z.string().min(30).describe("\u0447\u0442\u043E \u0442\u044B \u043F\u0440\u0435\u0434\u043F\u043E\u043B\u043E\u0436\u0438\u043B \u0431\u044B \u0441\u0434\u0435\u043B\u0430\u0442\u044C, \u0435\u0441\u043B\u0438 \u0431\u044B \u043F\u0440\u0438\u0448\u043B\u043E\u0441\u044C \u0433\u0430\u0434\u0430\u0442\u044C, \u0438 \u043F\u043E\u0447\u0435\u043C\u0443 \u043D\u0435 \u0441\u0442\u0430\u043B"),
9952
+ questions: z.array(z.string().min(10)).min(1).max(7).describe("\u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u044B\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0430\u0432\u0442\u043E\u0440\u0443 \u2014 \u043E\u0442\u0432\u0435\u0442\u044B \u043D\u0430 \u043D\u0438\u0445 \u0434\u043E\u043B\u0436\u043D\u044B \u0434\u0435\u043B\u0430\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u043C\u043E\u0439")
9953
+ },
9954
+ guard(
9955
+ async ({
9956
+ missing,
9957
+ readings,
9958
+ assumed,
9959
+ questions
9960
+ }) => {
9961
+ if (await overmindDead()) return freeze();
9962
+ const run = await store.myRun();
9963
+ if (iAmReviewer(run) || run?.kind === "hero" || opts.runKind === "hero") {
9964
+ return {
9965
+ content: [
9966
+ {
9967
+ type: "text",
9968
+ text: "return_task \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0434\u0440\u043E\u043D\u0430-\u0418\u0421\u041F\u041E\u041B\u041D\u0418\u0422\u0415\u041B\u042F: \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E\u0442 \u0421\u0412\u041E\u042E \u0437\u0430\u0434\u0430\u0447\u0443 \u0430\u0432\u0442\u043E\u0440\u0443. \u0420\u0435\u0432\u044C\u044E\u0435\u0440 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u043E\u043C \u0432 done(), \u0413\u0435\u0440\u043E\u0439 \u2014 rest()."
9969
+ }
9970
+ ],
9971
+ isError: true
9972
+ };
9973
+ }
9974
+ const slug = opts.expectedTaskSlug ?? run?.task_slug;
9975
+ if (!opts.taskless || !slug) {
9976
+ return {
9977
+ content: [
9978
+ {
9979
+ type: "text",
9980
+ text: "\u0412\u0435\u0440\u043D\u0443\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435\u0447\u0435\u043C: Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430 \u2014 \u043D\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u0430\u0432\u0442\u043E\u0440\u0443 \u0438 \u043F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0438\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E. \u0421\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 \u0447\u0435\u0440\u0435\u0437 ask(), \u0447\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C."
9981
+ }
9982
+ ],
9983
+ isError: true
9984
+ };
9985
+ }
9986
+ const droneName = run?.name ?? "(\u0438\u043C\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)";
9987
+ const task = await opts.taskless.getTask(slug);
9988
+ const author = task?.author ?? null;
9989
+ const body = [
9990
+ `\u21A9\uFE0F **\u0412\u043E\u0437\u0432\u0440\u0430\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 \u0430\u0432\u0442\u043E\u0440\u0443** \u2014 \u0434\u0440\u043E\u043D ${droneName}${run ? `, \u043F\u0440\u043E\u0433\u043E\u043D #${run.id}` : ""}.`,
9991
+ "",
9992
+ `${author ? `${author.name}, ` : ""}\u044F \u043D\u0435 \u0431\u0435\u0440\u0443\u0441\u044C \u0437\u0430 \u044D\u0442\u0443 \u0437\u0430\u0434\u0430\u0447\u0443: \u0435\u0451 \u043D\u0435\u043B\u044C\u0437\u044F \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C, \u043D\u0435 \u0432\u044B\u0434\u0443\u043C\u0430\u0432 \u043F\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0443 \u0437\u0430 \u0442\u0435\u0431\u044F. \u0427\u0442\u043E \u043C\u0435\u0448\u0430\u0435\u0442:`,
9993
+ "",
9994
+ `**\u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442.** ${missing.trim()}`,
9995
+ ...readings?.length ? ["", "**\u041A\u0430\u043A \u044D\u0442\u043E \u043C\u043E\u0436\u043D\u043E \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C.**", ...readings.map((r) => `- ${r.trim()}`)] : [],
9996
+ "",
9997
+ `**\u0427\u0442\u043E \u044F \u043F\u0440\u0435\u0434\u043F\u043E\u043B\u043E\u0436\u0438\u043B \u0431\u044B \u0438 \u043F\u043E\u0447\u0435\u043C\u0443 \u043D\u0435 \u0441\u0442\u0430\u043B.** ${assumed.trim()}`,
9998
+ "",
9999
+ "**\u0412\u043E\u043F\u0440\u043E\u0441\u044B, \u043F\u043E\u0441\u043B\u0435 \u043E\u0442\u0432\u0435\u0442\u043E\u0432 \u043D\u0430 \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0443 \u043C\u043E\u0436\u043D\u043E \u0431\u0440\u0430\u0442\u044C:**",
10000
+ ...questions.map((q, i) => `${i + 1}. ${q.trim()}`)
10001
+ ].join("\n");
10002
+ const failures = [];
10003
+ try {
10004
+ await opts.taskless.addComment(slug, body);
10005
+ } catch (e) {
10006
+ failures.push(`\u043A\u043E\u043C\u043C\u0435\u043D\u0442 \u0441 \u0432\u043E\u043F\u0440\u043E\u0441\u0430\u043C\u0438 \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D (${e instanceof Error ? e.message : String(e)})`);
10007
+ }
10008
+ let assignedOk = false;
10009
+ if (author) {
10010
+ assignedOk = await opts.taskless.assignTask(slug, author.id);
10011
+ if (!assignedOk) failures.push("\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u043F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0430 \u043D\u0430 \u0430\u0432\u0442\u043E\u0440\u0430");
10012
+ } else {
10013
+ failures.push("\u0430\u0432\u0442\u043E\u0440 \u0437\u0430\u0434\u0430\u0447\u0438 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D (get_task \u0435\u0433\u043E \u043D\u0435 \u043E\u0442\u0434\u0430\u043B) \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0441\u0442\u0430\u043B\u0430\u0441\u044C \u043D\u0430 \u043F\u0440\u0435\u0436\u043D\u0435\u043C \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u0435");
10014
+ }
10015
+ await opts.taskless.startTask(slug).catch((e) => {
10016
+ failures.push(`\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0435\u043D\u0430 \u0432 \u0440\u0430\u0431\u043E\u0442\u0443 (${e instanceof Error ? e.message : String(e)})`);
10017
+ });
10018
+ if (!assignedOk) {
10019
+ const after = await opts.taskless.getTask(slug).catch(() => null);
10020
+ if (!after || after.statusCategory === "unstarted") {
10021
+ await store.addEvent(
10022
+ null,
10023
+ "step",
10024
+ `\u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0442: \u0437\u0430\u0434\u0430\u0447\u0430 ${slug} \u043E\u0441\u0442\u0430\u043B\u0430\u0441\u044C claimable (${failures.join("; ")}) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D running, \u0447\u0442\u043E\u0431\u044B \u0440\u043E\u0439 \u043D\u0435 \u0432\u0437\u044F\u043B \u0435\u0451 \u043F\u043E \u043A\u0440\u0443\u0433\u0443`
10025
+ );
10026
+ return {
10027
+ content: [
10028
+ {
10029
+ type: "text",
10030
+ text: [
10031
+ `\u0412\u043E\u0437\u0432\u0440\u0430\u0442 \u041D\u0415 \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D: ${failures.join("; ")}.`,
10032
+ `\u0417\u0430\u0434\u0430\u0447\u0430 ${slug} \u043E\u0441\u0442\u0430\u043B\u0430\u0441\u044C \u0432 \u0442\u043E\u043C \u0436\u0435 \u0441\u0442\u0430\u0442\u0443\u0441\u0435 \u0438 \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u0435 \u2014 \u0437\u0430\u043A\u0440\u043E\u0439 \u044F \u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0435\u0439\u0447\u0430\u0441, \u0440\u043E\u0439 \u0432\u0437\u044F\u043B \u0431\u044B \u0435\u0451 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0434\u0440\u043E\u043D\u043E\u043C \u043F\u043E \u043A\u0440\u0443\u0433\u0443.`,
10033
+ "\u0412\u043E\u043F\u0440\u043E\u0441\u044B \u0430\u0432\u0442\u043E\u0440\u0443 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0443\u0436\u0435 \u0443\u0435\u0445\u0430\u043B\u0438. \u041F\u043E\u0432\u0442\u043E\u0440\u0438 return_task \u0447\u0435\u0440\u0435\u0437 \u043C\u0438\u043D\u0443\u0442\u0443 (\u043A\u043E\u043C\u043C\u0435\u043D\u0442 \u0437\u0430\u0434\u0432\u043E\u0438\u0442\u0441\u044F \u2014 \u044D\u0442\u043E \u043C\u0435\u043D\u044C\u0448\u0435\u0435 \u0437\u043B\u043E) \u043B\u0438\u0431\u043E \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 \u0447\u0435\u0440\u0435\u0437 ask()."
10034
+ ].join("\n")
10035
+ }
10036
+ ],
10037
+ isError: true
10038
+ };
10039
+ }
10040
+ }
10041
+ const reason = `\u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u0430 \u0430\u0432\u0442\u043E\u0440\u0443${author ? ` (${author.name})` : ""}: ${missing.trim()}`;
10042
+ await store.updateRun(null, { status: "returned", stop_reason: reason });
10043
+ await store.addEvent(null, "returned", failures.length ? `${reason} \xB7 \u043D\u0435 \u0434\u043E\u0435\u0445\u0430\u043B\u043E: ${failures.join("; ")}` : reason);
10044
+ return {
10045
+ content: [
10046
+ {
10047
+ type: "text",
10048
+ text: [
10049
+ `\u0417\u0430\u0434\u0430\u0447\u0430 ${slug} \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u0430 \u0430\u0432\u0442\u043E\u0440\u0443${author ? ` (${author.name})` : ""}, \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442 \u043A\u0430\u043A \xAB\u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u0430 \u0430\u0432\u0442\u043E\u0440\u0443\xBB, \u0441\u043B\u043E\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0451\u043D.`,
10050
+ ...failures.length ? [`\u26A0\uFE0F \u041D\u0435 \u0434\u043E\u0435\u0445\u0430\u043B\u043E: ${failures.join("; ")} \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0443\u0432\u0438\u0434\u0438\u0442 \u044D\u0442\u043E \u0432 \u043B\u043E\u0433\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u0430.`] : [],
10051
+ "\u0411\u043E\u043B\u044C\u0448\u0435 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u0432 \u041D\u0415 \u0437\u043E\u0432\u0438 \u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u0430\u043B\u044C\u0448\u0435 \u041D\u0415 \u0434\u0435\u043B\u0430\u0439 \u2014 \u043F\u0440\u043E\u0441\u0442\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438 \u0441\u0435\u0441\u0441\u0438\u044E."
10052
+ ].join("\n")
10053
+ }
10054
+ ]
10055
+ };
10056
+ }
10057
+ )
10058
+ );
9858
10059
  server.tool(
9859
10060
  "rest",
9860
10061
  "\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0413\u0435\u0440\u043E\u044F-\u043B\u0435\u0439\u0442\u0435\u043D\u0430\u043D\u0442\u0430. \u0417\u0430\u043C\u0435\u0440\u0435\u0442\u044C \u0434\u043E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u043E\u0442\u0440\u044F\u0434\u0430: \u0440\u044B\u0432\u043E\u043A \u0441\u0434\u0435\u043B\u0430\u043D (\u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0438 \u0441\u043E\u0437\u0434\u0430\u043D\u044B / \u0434\u043E\u043A\u043B\u0430\u0434 \u0434\u0430\u043D / \u043E\u0442\u0432\u0435\u0442\u044B \u0440\u043E\u0437\u0434\u0430\u043D\u044B), \u0437\u0432\u0430\u0442\u044C \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0447\u0435\u0433\u043E. \u041F\u0440\u043E\u0433\u043E\u043D \u2192 paused, \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044F; \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442 \u043E\u0432\u0435\u0440\u043C\u0430\u0439\u043D\u0434 \u043F\u043E \u0441\u043E\u0431\u044B\u0442\u0438\u044E \u043E\u0442\u0440\u044F\u0434\u0430 \u0438\u043B\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0432\u0440\u0443\u0447\u043D\u0443\u044E. \u041E\u0431\u044B\u0447\u043D\u044B\u0439 \u0434\u0440\u043E\u043D \u0441\u0434\u0430\u0451\u0442 \u0440\u0430\u0431\u043E\u0442\u0443 \u0447\u0435\u0440\u0435\u0437 done, \u043D\u0435 rest.",
@@ -9914,9 +10115,7 @@ ${notFoundText}` : notFoundText }],
9914
10115
  isError: true
9915
10116
  };
9916
10117
  }
9917
- const reviewerBySpawn = opts.reviewsRunId != null || opts.runKind === "reviewer";
9918
- const iAmReviewer = run?.reviews_run_id != null || run?.kind === "reviewer" || reviewerBySpawn;
9919
- if (iAmReviewer && run?.reviews_run_id == null) {
10118
+ if (iAmReviewer(run) && run?.reviews_run_id == null) {
9920
10119
  return {
9921
10120
  content: [
9922
10121
  {
@@ -10911,6 +11110,15 @@ function makeExclusiveReader(opts) {
10911
11110
  ...opts.now !== void 0 ? { now: opts.now } : {}
10912
11111
  });
10913
11112
  }
11113
+ function makeAutoReviewReader(opts) {
11114
+ return makeRepoValueReader({
11115
+ fetch: opts.fetch,
11116
+ fallback: opts.fallback,
11117
+ valid: (v) => typeof v === "boolean",
11118
+ ...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {},
11119
+ ...opts.now !== void 0 ? { now: opts.now } : {}
11120
+ });
11121
+ }
10914
11122
  function makeDroneCapReader(opts) {
10915
11123
  const read = makeRepoValueReader({
10916
11124
  fetch: opts.fetch,
@@ -10926,6 +11134,39 @@ function makeDroneCapReader(opts) {
10926
11134
  };
10927
11135
  }
10928
11136
 
11137
+ // src/mergeGate.ts
11138
+ init_src();
11139
+ function prNumberOf(ref) {
11140
+ const s = (ref || "").trim();
11141
+ if (!s) return null;
11142
+ const url = s.match(/\/pull\/(\d+)/);
11143
+ if (url) return Number(url[1]);
11144
+ const bare = s.match(/^#?(\d+)$/);
11145
+ return bare ? Number(bare[1]) : null;
11146
+ }
11147
+ function findRunByPr(runs, ref) {
11148
+ const num = prNumberOf(ref);
11149
+ if (num == null) return null;
11150
+ let best = null;
11151
+ for (const r of runs) {
11152
+ if (!r.pr_url || prNumberOf(r.pr_url) !== num) continue;
11153
+ if (!best || r.id > best.id) best = r;
11154
+ }
11155
+ return best;
11156
+ }
11157
+ function makeMergeGateResolver(deps) {
11158
+ return async (ref) => {
11159
+ const run = findRunByPr(deps.listRuns(), ref);
11160
+ if (!run) return { ...computeMergeGate({ hasRun: false, autoReview: false, reviewVerdict: null }), run: null };
11161
+ const autoReview = deps.autoReview ? await deps.autoReview(run.repo).catch(() => deps.fallbackAutoReview) : deps.fallbackAutoReview;
11162
+ const decision = computeMergeGate({ hasRun: true, autoReview, reviewVerdict: run.review_verdict });
11163
+ return {
11164
+ ...decision,
11165
+ run: { id: run.id, task_slug: run.task_slug, name: run.name, pr_url: run.pr_url, review_verdict: run.review_verdict }
11166
+ };
11167
+ };
11168
+ }
11169
+
10929
11170
  // src/envGate.ts
10930
11171
  import { existsSync as existsSync13 } from "fs";
10931
11172
  import { join as join24 } from "path";
@@ -11364,7 +11605,7 @@ async function changedPaths(worktree, opts = {}) {
11364
11605
  // package.json
11365
11606
  var package_default = {
11366
11607
  name: "@taskless-app/swarm",
11367
- version: "0.6.7",
11608
+ version: "0.6.9",
11368
11609
  type: "module",
11369
11610
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
11370
11611
  keywords: [
@@ -11414,7 +11655,7 @@ var package_default = {
11414
11655
  execa: "^10.0.0",
11415
11656
  zod: "3.25.76"
11416
11657
  },
11417
- gitHead: "08ee2a1a4bdb53ba1c25c92066104f0de3a23433"
11658
+ gitHead: "cefe25ffbc0e9791234055c8ba552ff3f8d07a3f"
11418
11659
  };
11419
11660
 
11420
11661
  // src/version.ts
@@ -12837,6 +13078,12 @@ function makeHandler(deps) {
12837
13078
  hostId: deps.selfHost?.hostId ?? null
12838
13079
  });
12839
13080
  }
13081
+ if (p === "/merge-gate") {
13082
+ const ref = url.searchParams.get("pr") ?? "";
13083
+ 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 });
13084
+ const resolve4 = deps.mergeGate ?? makeMergeGateResolver({ listRuns: () => deps.db.listRuns(), fallbackAutoReview: SWARM_AUTO_REVIEW });
13085
+ return Response.json(await resolve4(ref));
13086
+ }
12840
13087
  if (p === "/resources") return Response.json(hostResourceSnapshot());
12841
13088
  if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
12842
13089
  const diffMatch = p.match(/^\/run\/(\d+)\/diff$/);
@@ -14289,7 +14536,12 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
14289
14536
  // вторую ветку мимо PR. Пишем ту, что реально записана на прогоне; пусто (первый спавн —
14290
14537
  // ветку заводит spawnDrone из слага) — прежняя формулировка.
14291
14538
  `\u0422\u044B \u2014 \u0434\u0440\u043E\u043D \u0440\u043E\u044F. \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C \u0432 \u044D\u0442\u043E\u043C git-worktree, \u0432\u0435\u0442\u043A\u0430 \`${run.branch || run.task_slug}\`${run.branch && run.branch !== run.task_slug ? " (\u043E\u043D\u0430 \u041D\u0415 \u0440\u0430\u0432\u043D\u0430 \u0441\u043B\u0430\u0433\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u044D\u0442\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u0435 \u0443\u0436\u0435 \u043E\u0442\u043A\u0440\u044B\u0442\u043E\u0433\u043E PR, \u043F\u0443\u0448 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u043D\u0435\u0451)" : " \u0441\u043E\u0437\u0434\u0430\u043D\u0430 \u043F\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438"}.`,
14292
- "\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; read_task() \u2014 \u0441\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 (\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435+\u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438+\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u044B+\u0434\u043E\u043A\u0438+\u0438\u0441\u0445\u043E\u0434\u044B \u0441\u043E\u0441\u0435\u0434\u043D\u0438\u0445 \u0437\u0430\u0434\u0430\u0447 \u043F\u043E \u0437\u043E\u043D\u0435 \u043A\u043E\u0434\u0430) \u0431\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432, \u0435\u0441\u043B\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043C\u043E\u0433\u043B\u0438 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435 \u0441\u0442\u0430\u0440\u0442\u0430; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); report_acceptance_check(id, state, evidence) \u2014 \u0437\u0430\u043A\u0440\u043E\u0439 \u041A\u0410\u0416\u0414\u042B\u0419 \u043F\u0443\u043D\u043A\u0442 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0435\u0432 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 \u0437\u0430\u0434\u0430\u0447\u0438 (id \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0438\u0437 \u0431\u043B\u043E\u043A\u0430 \xAB\u041A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438\xBB, state = passed|failed|pending \u043F\u043E \u0444\u0430\u043A\u0442\u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430, evidence \u2014 \u0432\u044B\u0432\u043E\u0434 \u043A\u043E\u043C\u0430\u043D\u0434\u044B/\u0441\u0441\u044B\u043B\u043A\u0430/\u0447\u0442\u043E \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u043B); \u043F\u0440\u043E\u0439\u0434\u0438\u0441\u044C \u043F\u043E \u043D\u0438\u043C \u041F\u0415\u0420\u0415\u0414 done(), \u0438\u043D\u0430\u0447\u0435 \u0433\u0430\u043B\u043E\u0447\u043A\u0438 \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442 \u0440\u0443\u043A\u0430\u043C\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440; done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR; note(text) \u2014 \u0437\u0430\u043C\u0435\u0442\u043A\u0430 \xAB\u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438\xBB \u0411\u0415\u0417 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0438: \u043D\u0430\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u043D\u0430 \u043F\u0440\u043E\u0431\u0435\u043B \u0432 \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435, \u043F\u0440\u043E\u0442\u0443\u0445\u0448\u0443\u044E \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u044E, \u0431\u0430\u0433 \u0440\u044F\u0434\u043E\u043C \u0441\u043E \u0441\u0432\u043E\u0438\u043C \u0434\u0438\u0444\u0444\u043E\u043C \u2014 \u0441\u043A\u0430\u0436\u0438 \u0438 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u0434\u0430\u043B\u044C\u0448\u0435, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0443\u0432\u0438\u0434\u0438\u0442 \u0438 \u0441\u0430\u043C \u0440\u0435\u0448\u0438\u0442, \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u044C \u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043D\u0435 \u043F\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0439 \u0442\u0430\u043A\u043E\u0435 \u0432 \u0434\u043E\u0440\u043E\u0433\u043E\u0439 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 ask \u0438 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u0438 \u0432 \u043E\u0442\u0447\u0451\u0442\u0435 \u043E \u0441\u0434\u0430\u0447\u0435).",
14539
+ "\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; read_task() \u2014 \u0441\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 (\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435+\u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438+\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u044B+\u0434\u043E\u043A\u0438+\u0438\u0441\u0445\u043E\u0434\u044B \u0441\u043E\u0441\u0435\u0434\u043D\u0438\u0445 \u0437\u0430\u0434\u0430\u0447 \u043F\u043E \u0437\u043E\u043D\u0435 \u043A\u043E\u0434\u0430) \u0431\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432, \u0435\u0441\u043B\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043C\u043E\u0433\u043B\u0438 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435 \u0441\u0442\u0430\u0440\u0442\u0430; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); report_acceptance_check(id, state, evidence) \u2014 \u0437\u0430\u043A\u0440\u043E\u0439 \u041A\u0410\u0416\u0414\u042B\u0419 \u043F\u0443\u043D\u043A\u0442 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0435\u0432 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 \u0437\u0430\u0434\u0430\u0447\u0438 (id \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0438\u0437 \u0431\u043B\u043E\u043A\u0430 \xAB\u041A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438\xBB, state = passed|failed|pending \u043F\u043E \u0444\u0430\u043A\u0442\u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430, evidence \u2014 \u0432\u044B\u0432\u043E\u0434 \u043A\u043E\u043C\u0430\u043D\u0434\u044B/\u0441\u0441\u044B\u043B\u043A\u0430/\u0447\u0442\u043E \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u043B); \u043F\u0440\u043E\u0439\u0434\u0438\u0441\u044C \u043F\u043E \u043D\u0438\u043C \u041F\u0415\u0420\u0415\u0414 done(), \u0438\u043D\u0430\u0447\u0435 \u0433\u0430\u043B\u043E\u0447\u043A\u0438 \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442 \u0440\u0443\u043A\u0430\u043C\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440; done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR; return_task(missing, readings, assumed, questions) \u2014 \u0432\u0435\u0440\u043D\u0443\u0442\u044C \u0421\u042B\u0420\u0423\u042E \u0437\u0430\u0434\u0430\u0447\u0443 \u0430\u0432\u0442\u043E\u0440\u0443 (\u0441\u043C. \u043F\u0440\u0430\u0432\u0438\u043B\u043E \u043D\u0438\u0436\u0435); note(text) \u2014 \u0437\u0430\u043C\u0435\u0442\u043A\u0430 \xAB\u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438\xBB \u0411\u0415\u0417 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0438: \u043D\u0430\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u043D\u0430 \u043F\u0440\u043E\u0431\u0435\u043B \u0432 \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435, \u043F\u0440\u043E\u0442\u0443\u0445\u0448\u0443\u044E \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u044E, \u0431\u0430\u0433 \u0440\u044F\u0434\u043E\u043C \u0441\u043E \u0441\u0432\u043E\u0438\u043C \u0434\u0438\u0444\u0444\u043E\u043C \u2014 \u0441\u043A\u0430\u0436\u0438 \u0438 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u0434\u0430\u043B\u044C\u0448\u0435, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0443\u0432\u0438\u0434\u0438\u0442 \u0438 \u0441\u0430\u043C \u0440\u0435\u0448\u0438\u0442, \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u044C \u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043D\u0435 \u043F\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0439 \u0442\u0430\u043A\u043E\u0435 \u0432 \u0434\u043E\u0440\u043E\u0433\u043E\u0439 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 ask \u0438 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u0438 \u0432 \u043E\u0442\u0447\u0451\u0442\u0435 \u043E \u0441\u0434\u0430\u0447\u0435).",
14540
+ // TASWM-412: третий выход из непонятной задачи. Раньше их было два, и оба плохие —
14541
+ // угадать постановку (сделает не то) или ask (слот занят, а ответить может только автор
14542
+ // задачи, не оператор роя). Граница здесь ОБЯЗАТЕЛЬНА: дрон, легко возвращающий задачи,
14543
+ // становится отписочником, а это хуже, чем дрон, который иногда угадывает неверно.
14544
+ "\u0421\u042B\u0420\u0423\u042E \u0417\u0410\u0414\u0410\u0427\u0423 \u041D\u0415 \u0412\u042B\u0414\u0423\u041C\u042B\u0412\u0410\u0419 \u2014 \u0412\u0415\u0420\u041D\u0418 \u0410\u0412\u0422\u041E\u0420\u0423. \u041F\u0440\u043E\u0447\u0438\u0442\u0430\u0432 \u0437\u0430\u0434\u0430\u0447\u0443, \u043E\u0442\u0432\u0435\u0442\u044C \u0441\u0435\u0431\u0435 \u043D\u0430 \u0434\u0432\u0430 \u0432\u043E\u043F\u0440\u043E\u0441\u0430: \u043F\u043E\u043D\u044F\u0442\u043D\u043E \u043B\u0438, \u0427\u0422\u041E \u043D\u0443\u0436\u043D\u043E \u0441\u0434\u0435\u043B\u0430\u0442\u044C, \u0438 \u0435\u0441\u0442\u044C \u043B\u0438 \u0442\u043E, \u041F\u041E \u0427\u0415\u041C\u0423 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442. \u041E\u0431\u0430 \xAB\u043D\u0435\u0442\xBB \u2192 return_task(missing, readings, assumed, questions): \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0443\u0435\u0434\u0443\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0430\u0432\u0442\u043E\u0440\u0443, \u0437\u0430\u0434\u0430\u0447\u0430 \u0443\u0439\u0434\u0451\u0442 \u043D\u0430 \u043D\u0435\u0433\u043E \u0438 \u0432 \u0440\u0430\u0431\u043E\u0442\u0443, \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u043E\u0435\u0442\u0441\u044F \u043A\u0430\u043A \xAB\u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u0430 \u0430\u0432\u0442\u043E\u0440\u0443\xBB (\u043D\u0435 \xAB\u043F\u0440\u043E\u0432\u0430\u043B\u0435\u043D\u0430\xBB), \u0441\u043B\u043E\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u0441\u044F. \u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0442\u044C \u041C\u041E\u0416\u041D\u041E, \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435\u043B\u044C\u0437\u044F \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u043D\u0438 \u0432 \u043E\u0434\u043D\u043E\u043C \u0440\u0430\u0437\u0443\u043C\u043D\u043E\u043C \u0442\u043E\u043B\u043A\u043E\u0432\u0430\u043D\u0438\u0438 \u0438\u043B\u0438 \u043D\u0435\u0447\u0435\u043C \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442. \u0422\u043E\u043B\u043A\u043E\u0432\u0430\u043D\u0438\u0439 \u0434\u0432\u0430 \u0438 \u043E\u0431\u0430 \u043E\u0441\u043C\u044B\u0441\u043B\u0435\u043D\u043D\u044B \u2014 \u0432\u044B\u0431\u0435\u0440\u0438 \u043E\u0447\u0435\u0432\u0438\u0434\u043D\u043E\u0435, \u0441\u0434\u0435\u043B\u0430\u0439 \u0438 \u043D\u0430\u0437\u043E\u0432\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0443 \u0432 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0438 PR. \u0417\u0430\u0434\u0430\u0447\u0430 \u043F\u043E\u043D\u044F\u0442\u043D\u0430, \u043D\u043E \u0432 \u043E\u0434\u043D\u043E\u043C \u043F\u0443\u043D\u043A\u0442\u0435 \u043F\u0440\u043E\u0442\u0438\u0432\u043E\u0440\u0435\u0447\u0438\u0442 \u043F\u0440\u0430\u0432\u0438\u043B\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u044D\u0442\u043E ask, \u0430 \u043D\u0435 \u0432\u043E\u0437\u0432\u0440\u0430\u0442.",
14293
14545
  "\u041F\u0440\u0430\u0432\u0438\u043B\u0430: \u0442\u0440\u043E\u0433\u0430\u0435\u0448\u044C UI \u2014 \u0421\u041D\u0410\u0427\u0410\u041B\u0410 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438, \u0435\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0441\u043A\u0438\u043B\u043B \u0441 UI-\u043F\u0440\u0430\u0432\u0438\u043B\u0430\u043C\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430, \u0438 \u0441\u043B\u0435\u0434\u0443\u0439 \u0435\u0433\u043E \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435 (\u043D\u0435\u0442 \u043D\u0443\u0436\u043D\u043E\u0433\u043E \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0430 \u2014 \u0437\u0430\u0432\u0435\u0434\u0438 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443, \u043D\u0435 \u0445\u0430\u0440\u0434\u043A\u043E\u0434\u044C \u043E\u0434\u043D\u043E\u0440\u0430\u0437\u043E\u0432\u043E\u0435); \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0441\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0438; \u043F\u043E\u0441\u043B\u0435 UI-\u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u0441\u043A\u0440\u0438\u043D\u044B mobile+desktop \u041A \u0417\u0410\u0414\u0410\u0427\u0415 (\u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 screenshots \u0443 done); \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0447\u0435\u0440\u0435\u0437 gh \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url).",
14294
14546
  // TASWM-377: у сессии есть ПОТОЛОК ТРАТ, и он режет её без предупреждения, прямо посреди
14295
14547
  // работы. Дрон об этом знать обязан: цена сессии растёт от ЧИСЛА ХОДОВ (каждый ход заново
@@ -16720,6 +16972,7 @@ async function runDaemon(opts) {
16720
16972
  let envGate;
16721
16973
  let exclusiveProject;
16722
16974
  let projectDroneCap;
16975
+ let autoReviewProject;
16723
16976
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
16724
16977
  const client = db;
16725
16978
  verifyModeReader = makeVerifyModeReader({
@@ -16766,6 +17019,14 @@ async function runDaemon(opts) {
16766
17019
  },
16767
17020
  ttlMs: 6e4
16768
17021
  });
17022
+ autoReviewProject = makeAutoReviewReader({
17023
+ fetch: async (repo) => {
17024
+ const repoFullName = await remoteRepoFullName(repo).catch(() => null);
17025
+ return repoFullName ? client.repoAutoReview(repoFullName) : null;
17026
+ },
17027
+ fallback: SWARM_AUTO_REVIEW,
17028
+ ttlMs: 6e4
17029
+ });
16769
17030
  }
16770
17031
  let spawnReviewerFn;
16771
17032
  let verdictCommentFn;
@@ -17334,6 +17595,15 @@ async function runDaemon(opts) {
17334
17595
  // печатает `swarm status`. Без него человек снова гадал бы, какие репозитории машина
17335
17596
  // считает своими и в каком дереве работает при двух копиях одного клона.
17336
17597
  repoMap: () => repoMapRows(repoMapState),
17598
+ // Гейт мержа (TASWM-355): ручка /merge-gate отвечает по ЖИВОЙ настройке autoReview того
17599
+ // проекта, которому принадлежит PR (ридер выше), а не по глобальному env машины. Именно
17600
+ // этот ответ и превращается в запрет: PreToolUse-хук на `gh pr merge` спрашивает его
17601
+ // через `swarm merge-gate` и отказывает в команде, пока вердикта ревьюера нет.
17602
+ mergeGate: makeMergeGateResolver({
17603
+ listRuns: () => db.listRuns(),
17604
+ ...autoReviewProject ? { autoReview: autoReviewProject } : {},
17605
+ fallbackAutoReview: SWARM_AUTO_REVIEW
17606
+ }),
17337
17607
  // Оператор-пауза + «Оживить» (TASWM-214 Фаза 2): тот же shared-флаг, что читает тик;
17338
17608
  // narrate — операторские действия видны в ленте роя; revive — сброс залипшего шторма.
17339
17609
  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.9",
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": "cefe25ffbc0e9791234055c8ba552ff3f8d07a3f",
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",