@taskless-app/swarm 0.6.3 → 0.6.5

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.
Files changed (3) hide show
  1. package/dist/cli.js +154 -28
  2. package/dist/overmind.js +352 -83
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -104,7 +104,13 @@ var init_types = __esm({
104
104
  // делать нечего». Находки вроде «в дизайн-системе нет иконки микрофона» раньше либо
105
105
  // превращались в такой дорогой вопрос, либо тонули в отчёте о сдаче (дрон LAMPYRIS сдал
106
106
  // заглушку «Мик» текстом на кнопке — уехало в прод). note уезжает оператору, дрон работает дальше.
107
- "note"
107
+ "note",
108
+ // Зависимость прогона снята НЕуспешным финалом блокера (TASWM-361): тот остановлен/упал/
109
+ // переигран, успешного финала не будет — ждать больше нечего. Пишется в ленту ЖДАВШЕГО
110
+ // прогона перед самым спавном: «поехал молча» означало бы, что причина запуска исчезла из
111
+ // истории, и следующий разбор «почему он стартовал поверх незакрытого блокера» упёрся бы в
112
+ // пустоту. Успешное снятие (блокер смержен) события не порождает — это штатный ход.
113
+ "dep_cleared"
108
114
  ];
109
115
  MILESTONE_KINDS = [
110
116
  "spawned",
@@ -125,8 +131,10 @@ var init_types = __esm({
125
131
  "storm_dead",
126
132
  "anomaly",
127
133
  // надзор overmind (TASWM-194) — та же нотификация, что улетела в инбокс, вехой в Истории
128
- "note"
134
+ "note",
129
135
  // заметка дрона «замечено по пути» (TASWM-279) — веха, иначе находка тонет в ленте step'ов
136
+ "dep_cleared"
137
+ // зависимость снята неуспешным финалом блокера (TASWM-361) — решение роя, веха как conflict
130
138
  ];
131
139
  MILESTONE_KIND_SET = new Set(MILESTONE_KINDS);
132
140
  }
@@ -355,6 +363,32 @@ var init_status = __esm({
355
363
  }
356
364
  });
357
365
 
366
+ // ../swarm-core/src/deps.ts
367
+ function judgeDep(runs) {
368
+ const real = runs.filter((r) => !r.service);
369
+ if (real.length === 0) return { ...PENDING };
370
+ if (real.some((r) => !isTerminalStatus(r.status))) return { ...PENDING };
371
+ if (real.some((r) => r.status === "done")) {
372
+ return { outcome: "done", met: true, status: "done", name: null, stopReason: null };
373
+ }
374
+ const last = real[real.length - 1];
375
+ return {
376
+ outcome: "exhausted",
377
+ met: true,
378
+ status: last.status,
379
+ name: last.name ?? null,
380
+ stopReason: last.stopReason ?? null
381
+ };
382
+ }
383
+ var PENDING;
384
+ var init_deps = __esm({
385
+ "../swarm-core/src/deps.ts"() {
386
+ "use strict";
387
+ init_status();
388
+ PENDING = { outcome: "pending", met: false, status: null, name: null, stopReason: null };
389
+ }
390
+ });
391
+
358
392
  // ../swarm-core/src/time.ts
359
393
  function stamp(d = /* @__PURE__ */ new Date()) {
360
394
  return d.toISOString().slice(0, 19).replace("T", " ");
@@ -1061,6 +1095,7 @@ var init_src = __esm({
1061
1095
  init_timeline();
1062
1096
  init_reviewPhase();
1063
1097
  init_status();
1098
+ init_deps();
1064
1099
  init_time();
1065
1100
  init_windows();
1066
1101
  init_slug();
@@ -1849,7 +1884,7 @@ var init_package = __esm({
1849
1884
  "package.json"() {
1850
1885
  package_default = {
1851
1886
  name: "@taskless-app/swarm",
1852
- version: "0.6.3",
1887
+ version: "0.6.5",
1853
1888
  type: "module",
1854
1889
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
1855
1890
  keywords: [
@@ -1899,7 +1934,7 @@ var init_package = __esm({
1899
1934
  execa: "^10.0.0",
1900
1935
  zod: "3.25.76"
1901
1936
  },
1902
- gitHead: "298573d4b361b8fc7ea4d6ea9d1e8784906bbd7e"
1937
+ gitHead: "76ff5edd816a2b9dd7f10db62c8fde89eec7771f"
1903
1938
  };
1904
1939
  }
1905
1940
  });
@@ -2688,18 +2723,40 @@ CREATE TABLE IF NOT EXISTS imports (
2688
2723
  return rows.map((r) => r.area);
2689
2724
  }
2690
2725
  /**
2691
- * Из depends_on прогона — слаги зависимостей, ещё НЕ закрытых (нет прогона со
2692
- * статусом done). Пусто → все выполнены, дрона можно спавнить. Явная зависимость
2693
- * (spawn --after) отдельна от area-lock: area освобождается на review (до мержа),
2694
- * а тут ждём именно done (смерж), чтобы следующий дрон стартовал от кода предыдущего.
2695
- * Несуществующий/зациклённый слаг просто никогда не станет done → прогон вечно ждёт,
2696
- * но очередь не виснет (гейт — пропуск в цикле, не блокировка).
2726
+ * Из depends_on прогона — слаги зависимостей, ещё НЕ исчерпанных. Пусто → все выполнены,
2727
+ * дрона можно спавнить. Явная зависимость (spawn --after) отдельна от area-lock: area
2728
+ * освобождается на review (до мержа), а тут ждём финала блокера, чтобы следующий дрон
2729
+ * стартовал от его кода.
2730
+ *
2731
+ * TASWM-361: критерий — «живых прогонов по слагу не осталось», а не «есть done». Раньше
2732
+ * зависимость снимал ТОЛЬКО успех, и остановленный оператором блокер держал ждущих вечно
2733
+ * (VESPA и TYPHON — сутки очереди при пустом рое). Само правило и его обоснование —
2734
+ * `judgeDep` в swarm-core/deps.ts (его же читают зеркало прода и прод-снапшот).
2735
+ *
2736
+ * Несуществующий/зациклённый слаг прогонов не имеет вовсе → `pending` → прогон ждёт, как и
2737
+ * раньше, но очередь не виснет (гейт — пропуск в цикле, не блокировка).
2697
2738
  */
2698
2739
  unmetDeps(depends_on) {
2740
+ return this.depVerdicts(depends_on).filter((d) => !d.met).map((d) => d.slug);
2741
+ }
2742
+ /**
2743
+ * TASWM-361: вердикт по каждой зависимости — не только «держит ли», но и КАК закрылась
2744
+ * (успехом блокера или его неуспешным финалом). Разница нужна overmind: снятие по
2745
+ * неуспеху оставляет след в ленге ждавшего прогона, штатное — нет.
2746
+ */
2747
+ depVerdicts(depends_on) {
2699
2748
  if (!depends_on || depends_on.length === 0) return [];
2700
- const rows = this.db.prepare("SELECT DISTINCT task_slug FROM runs WHERE status = 'done' AND resolves_run_id IS NULL").all();
2701
- const done = new Set(rows.map((r) => r.task_slug));
2702
- return depends_on.filter((slug) => !done.has(slug));
2749
+ const slugs = [...new Set(depends_on)];
2750
+ const rows = this.db.prepare(
2751
+ `SELECT task_slug, status, name, stop_reason FROM runs WHERE task_slug IN (${slugs.map(() => "?").join(",")}) AND resolves_run_id IS NULL AND reviews_run_id IS NULL ORDER BY id`
2752
+ ).all(...slugs);
2753
+ const bySlug = /* @__PURE__ */ new Map();
2754
+ for (const r of rows) {
2755
+ const list = bySlug.get(r.task_slug) ?? [];
2756
+ list.push({ status: r.status, name: r.name, stopReason: r.stop_reason });
2757
+ bySlug.set(r.task_slug, list);
2758
+ }
2759
+ return depends_on.map((slug) => ({ slug, ...judgeDep(bySlug.get(slug) ?? []) }));
2703
2760
  }
2704
2761
  /** Все репо, засветившиеся в прогонах — whitelist для валидации spawn из веба. */
2705
2762
  distinctRepos() {
@@ -4184,10 +4241,13 @@ var SwarmClient = class _SwarmClient {
4184
4241
  // редко — без этого лог был бы простынёй из одной и той же строки, и в ней потонуло бы
4185
4242
  // всё остальное. Меняется набор (эпик закрыли / блокер прошли) → печатаем снова.
4186
4243
  lastClaimSkipLog = null;
4187
- // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
4188
- // Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
4189
- // переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
4190
- depsDoneInProd = /* @__PURE__ */ new Set();
4244
+ // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → вердикт по ВСЕМ его прогонам.
4245
+ // `done` монотонен (не откатывается) → положительный ответ не протухает и больше не
4246
+ // спрашивается; прочие вердикты переспрашиваются по TTL — «исчерпана неуспешно»
4247
+ // (TASWM-361) монотонностью НЕ обладает: оператор вправе переигрть остановленный блокер
4248
+ // новым прогоном того же слага, и тогда зависимость снова становится ожиданием.
4249
+ // Живёт в памяти: рестарт демона просто переспросит прод.
4250
+ depsProd = /* @__PURE__ */ new Map();
4191
4251
  depsAskedAt = /* @__PURE__ */ new Map();
4192
4252
  static DEPS_TTL_MS = 6e4;
4193
4253
  // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
@@ -4614,12 +4674,36 @@ var SwarmClient = class _SwarmClient {
4614
4674
  return [...seen];
4615
4675
  }
4616
4676
  unmetDeps(depends_on) {
4677
+ return this.depVerdicts(depends_on).filter((d) => !d.met).map((d) => d.slug);
4678
+ }
4679
+ /**
4680
+ * TASWM-361: вердикт по каждой зависимости — см. докблок SwarmStore.depVerdicts.
4681
+ *
4682
+ * АСИММЕТРИЯ ЗЕРКАЛА И ПРОДА, и почему она здесь намеренная. Зеркало — ЧАСТИЧНАЯ выборка
4683
+ * (только прогоны этой машины, только окно гидрации), и по частичной выборке два вывода
4684
+ * стоят разного:
4685
+ * • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он не
4686
+ * портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
4687
+ * • «живых прогонов слага не осталось» — вывод от ОТСУТСТВИЯ: невидимый зеркалу прогон
4688
+ * на соседней машине как раз и может быть живым. Поэтому «исчерпана неуспешно»
4689
+ * принимаем ТОЛЬКО из ответа прода (refreshDeps) — он видит всех.
4690
+ * Прод недоступен → вердикта нет → прогон честно ждёт следующего окна: лишняя минута
4691
+ * очереди дешевле дрона, поднятого поверх работающего блокера.
4692
+ */
4693
+ depVerdicts(depends_on) {
4617
4694
  if (!depends_on || depends_on.length === 0) return [];
4618
- const done = new Set(this.depsDoneInProd);
4695
+ const doneInMirror = /* @__PURE__ */ new Set();
4619
4696
  for (const r of this.runs.values()) {
4620
- if (r.status === "done" && r.resolves_run_id == null) done.add(r.task_slug);
4697
+ if (r.status === "done" && r.resolves_run_id == null && r.reviews_run_id == null) doneInMirror.add(r.task_slug);
4621
4698
  }
4622
- return depends_on.filter((slug) => !done.has(slug));
4699
+ return depends_on.map((slug) => {
4700
+ if (doneInMirror.has(slug)) {
4701
+ return { slug, outcome: "done", met: true, status: "done", name: null, stopReason: null };
4702
+ }
4703
+ const fromProd = this.depsProd.get(slug);
4704
+ if (fromProd) return { slug, ...fromProd };
4705
+ return { slug, outcome: "pending", met: false, status: null, name: null, stopReason: null };
4706
+ });
4623
4707
  }
4624
4708
  /**
4625
4709
  * TASWM-300: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
@@ -4630,14 +4714,15 @@ var SwarmClient = class _SwarmClient {
4630
4714
  * просто позавчера, в зеркало не попадала — и `--after` держал прогон в очереди вечно
4631
4715
  * (04.08: снимали руками).
4632
4716
  *
4633
- * Кеш. «Выполнено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
4634
- * положительный ответ помним навсегда и больше не спрашиваем. Отрицательный переспрашиваем
4717
+ * Кеш. «Смержено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
4718
+ * положительный ответ помним навсегда и больше не спрашиваем. Остальные вердикты (включая
4719
+ * «исчерпана неуспешно», TASWM-361 — блокер могут переиграть новым прогоном) переспрашиваем
4635
4720
  * не чаще DEPS_TTL_MS: тик идёт раз в ~4с, а ждущих зависимость прогонов может быть
4636
4721
  * много — без TTL это был бы поход в прод на каждый слаг каждый тик.
4637
4722
  */
4638
4723
  async refreshDeps(slugs, nowMs = Date.now()) {
4639
4724
  for (const slug of new Set(slugs)) {
4640
- if (!slug || this.depsDoneInProd.has(slug)) continue;
4725
+ if (!slug || this.depsProd.get(slug)?.outcome === "done") continue;
4641
4726
  const asked = this.depsAskedAt.get(slug);
4642
4727
  if (asked !== void 0 && nowMs - asked < _SwarmClient.DEPS_TTL_MS) continue;
4643
4728
  this.depsAskedAt.set(slug, nowMs);
@@ -4650,7 +4735,18 @@ var SwarmClient = class _SwarmClient {
4650
4735
  }
4651
4736
  if (res?.isError) continue;
4652
4737
  const runs = res.structuredContent?.runs ?? [];
4653
- if (runs.some((r) => r.status === "done" && !r.resolvesRunId)) this.depsDoneInProd.add(slug);
4738
+ const facts = runs.slice().sort((a, b) => String(a.createdAt ?? "").localeCompare(String(b.createdAt ?? ""))).map((r) => ({
4739
+ status: String(r.status ?? ""),
4740
+ // Служебный сабран под тем же слагом (резолвер конфликта / ревьюер): его финал —
4741
+ // не финал задачи. Резолвер вычёркивался и до TASWM-361, ревьюер добавлен вместе
4742
+ // с расширением критерия — иначе остановленный ревьюер закрывал бы зависимость.
4743
+ service: Boolean(r.resolvesRunId) || Boolean(r.reviewsRunId),
4744
+ // В проде имя дрона — колонка droneName (см. hydrateRun); `name` терпим как
4745
+ // запасной вариант ради фикстур и старых ответов.
4746
+ name: r.droneName ?? r.name ?? null,
4747
+ stopReason: r.stopReason ?? null
4748
+ }));
4749
+ this.depsProd.set(slug, judgeDep(facts));
4654
4750
  }
4655
4751
  }
4656
4752
  /**
@@ -5195,6 +5291,19 @@ var SwarmClient = class _SwarmClient {
5195
5291
  * → null: ридер (makeMaxDronesReader) берёт env-фолбэк.
5196
5292
  */
5197
5293
  async ownerMaxDrones() {
5294
+ return (await this.ownerSwarmSettings())?.maxDrones ?? null;
5295
+ }
5296
+ /**
5297
+ * TASWM-364: настройки роя ЦЕЛИКОМ из того же who_am_i — живой кап дронов И модели, на
5298
+ * которых спавнятся дроны и ревьюеры (раздел «Рой» в настройках). Одним вызовом, а не
5299
+ * двумя: читатель у них один (ридер maxDrones раз в TTL), и второй сетевой поход за
5300
+ * соседним полем того же ответа был бы платой ни за что.
5301
+ *
5302
+ * Прод недоступен / ответ без валидного капа → null: вызывающий берёт env-фолбэки
5303
+ * (SWARM_MAX_IN_FLIGHT для капа, SWARM_DRONE_MODEL для модели). Модель без капа отдельно
5304
+ * не отдаём СОЗНАТЕЛЬНО — «ответ прода не разобрался» это одно событие, а не два.
5305
+ */
5306
+ async ownerSwarmSettings() {
5198
5307
  let res;
5199
5308
  try {
5200
5309
  res = await this.call("who_am_i", {});
@@ -5204,7 +5313,14 @@ var SwarmClient = class _SwarmClient {
5204
5313
  if (res?.isError) return null;
5205
5314
  const obj = jsonFromTextContent(res);
5206
5315
  const n = obj?.maxDrones;
5207
- return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
5316
+ if (!(typeof n === "number" && Number.isFinite(n) && n > 0)) return null;
5317
+ const str = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
5318
+ return {
5319
+ maxDrones: Math.trunc(n),
5320
+ // Старый прод (до TASWM-364) поля swarmSettings не отдаёт вовсе — обе модели null,
5321
+ // то есть прежнее поведение: env машины и дефолт харнесса.
5322
+ models: { drone: str(obj?.swarmSettings?.droneModel), reviewer: str(obj?.swarmSettings?.reviewerModel) }
5323
+ };
5208
5324
  }
5209
5325
  /**
5210
5326
  * Live verifyMode проекта из Taskless (TASWM-171): читает swarm_repo_config по
@@ -7642,8 +7758,8 @@ var GK = ['claw','spore','eye','hex'];
7642
7758
  var ST = ${jsonForScript(statusTuples())};
7643
7759
  var ORDER = { running:0, blocked:1, paused:2, storm_dead:3, review:4, stalled:5, queued:6, stopped:7, done:8, failed:9, superseded:10 };
7644
7760
  var TERM = ${jsonForScript(Object.fromEntries(TERMINAL_STATUSES.map((s) => [s, 1])))};
7645
- var EVL = { spawned:'\u0434\u0440\u043E\u043D \u0437\u0430\u043F\u0443\u0449\u0435\u043D', step:'', blocked:'\u0432\u043E\u043F\u0440\u043E\u0441 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443', answered:'\u043F\u043E\u043B\u0443\u0447\u0435\u043D \u043E\u0442\u0432\u0435\u0442', pr_opened:'PR \u043E\u0442\u043A\u0440\u044B\u0442', merged:'PR \u0441\u043C\u0435\u0440\u0436\u0451\u043D, \u0437\u0430\u0434\u0430\u0447\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u0430', done:'\u0433\u043E\u0442\u043E\u0432\u043E', failed:'\u0434\u0440\u043E\u043D \u0443\u043F\u0430\u043B', stalled:'\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441/\u043F\u043E\u0433\u0438\u0431', respawned:'\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435', paused:'\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D', resumed:'overmind \u0432\u0435\u0440\u043D\u0443\u043B\u0441\u044F \u2014 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B', rescued:'worktree \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u2014 \u0437\u0430\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438', conflict:'\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 main (merge-main-in)', died:'\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440', storm_start:'\u26A1 \u043F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 \u2014 Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442', storm_end:'\u0441\u0432\u044F\u0437\u044C \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u2014 \u0440\u043E\u0439 \u043F\u0440\u043E\u0431\u0443\u0436\u0434\u0430\u0435\u0442\u0441\u044F', storm_dead:'\u0434\u0440\u043E\u043D \u0432 \u043A\u043E\u043A\u043E\u043D\u0435 (\u0448\u0442\u043E\u0440\u043C)', stopped:'\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D', superseded:'\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D', anomaly:'\u26A0\uFE0F \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u044F \u0440\u043E\u044F' };
7646
- var MDOT = { merged:'--success', done:'--success', pr_opened:'--info', spawned:'--muted-foreground', stalled:'--destructive', failed:'--destructive', respawned:'--warning', paused:'--warning', resumed:'--success', rescued:'--destructive', conflict:'--warning', died:'--destructive', storm_start:'--destructive', storm_end:'--success', storm_dead:'--warning', stopped:'--muted-foreground', superseded:'--muted-foreground', anomaly:'--warning' };
7761
+ var EVL = { spawned:'\u0434\u0440\u043E\u043D \u0437\u0430\u043F\u0443\u0449\u0435\u043D', step:'', blocked:'\u0432\u043E\u043F\u0440\u043E\u0441 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443', answered:'\u043F\u043E\u043B\u0443\u0447\u0435\u043D \u043E\u0442\u0432\u0435\u0442', pr_opened:'PR \u043E\u0442\u043A\u0440\u044B\u0442', merged:'PR \u0441\u043C\u0435\u0440\u0436\u0451\u043D, \u0437\u0430\u0434\u0430\u0447\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u0430', done:'\u0433\u043E\u0442\u043E\u0432\u043E', failed:'\u0434\u0440\u043E\u043D \u0443\u043F\u0430\u043B', stalled:'\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441/\u043F\u043E\u0433\u0438\u0431', respawned:'\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435', paused:'\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D', resumed:'overmind \u0432\u0435\u0440\u043D\u0443\u043B\u0441\u044F \u2014 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B', rescued:'worktree \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u2014 \u0437\u0430\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438', conflict:'\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 main (merge-main-in)', died:'\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440', storm_start:'\u26A1 \u043F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 \u2014 Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442', storm_end:'\u0441\u0432\u044F\u0437\u044C \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u2014 \u0440\u043E\u0439 \u043F\u0440\u043E\u0431\u0443\u0436\u0434\u0430\u0435\u0442\u0441\u044F', storm_dead:'\u0434\u0440\u043E\u043D \u0432 \u043A\u043E\u043A\u043E\u043D\u0435 (\u0448\u0442\u043E\u0440\u043C)', stopped:'\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D', superseded:'\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D', anomaly:'\u26A0\uFE0F \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u044F \u0440\u043E\u044F', dep_cleared:'\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C \u0441\u043D\u044F\u0442\u0430 \u2014 \u0431\u043B\u043E\u043A\u0435\u0440 \u043D\u0435 \u0434\u043E\u0435\u0434\u0435\u0442' };
7762
+ var MDOT = { merged:'--success', done:'--success', pr_opened:'--info', spawned:'--muted-foreground', stalled:'--destructive', failed:'--destructive', respawned:'--warning', paused:'--warning', resumed:'--success', rescued:'--destructive', conflict:'--warning', died:'--destructive', storm_start:'--destructive', storm_end:'--success', storm_dead:'--warning', stopped:'--muted-foreground', superseded:'--muted-foreground', anomaly:'--warning', dep_cleared:'--warning' };
7647
7763
 
7648
7764
  function E(s){ return String(s==null?"":s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]}); }
7649
7765
  function color(st){ return 'var('+(ST[st]||ST.queued)[2]+')'; }
@@ -8613,6 +8729,16 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
8613
8729
  };
8614
8730
  }
8615
8731
 
8732
+ // src/swarmModels.ts
8733
+ function resolveRunModel(run, swarm, env = process.env) {
8734
+ const pick = (v) => {
8735
+ const s = typeof v === "string" ? v.trim() : "";
8736
+ return s.length > 0 ? s : null;
8737
+ };
8738
+ const fromSwarm = run.kind === "reviewer" ? pick(swarm?.reviewer) ?? pick(swarm?.drone) : pick(swarm?.drone);
8739
+ return pick(run.model) ?? fromSwarm ?? pick(env.SWARM_DRONE_MODEL) ?? null;
8740
+ }
8741
+
8616
8742
  // src/launcher.ts
8617
8743
  init_executors();
8618
8744
  var requireFromHere = createRequire(import.meta.url);
@@ -8750,7 +8876,7 @@ function makeLauncher(cfg) {
8750
8876
  ...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
8751
8877
  }
8752
8878
  };
8753
- const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8879
+ const model = resolveRunModel(run, cfg.swarmModels?.() ?? null);
8754
8880
  const realHome = cfg.droneIsolation?.realHome ?? homedir4();
8755
8881
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
8756
8882
  if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync7(join12(cfg.droneIsolation.swarmHome, "token"))) {
package/dist/overmind.js CHANGED
@@ -69,7 +69,13 @@ var init_types = __esm({
69
69
  // делать нечего». Находки вроде «в дизайн-системе нет иконки микрофона» раньше либо
70
70
  // превращались в такой дорогой вопрос, либо тонули в отчёте о сдаче (дрон LAMPYRIS сдал
71
71
  // заглушку «Мик» текстом на кнопке — уехало в прод). note уезжает оператору, дрон работает дальше.
72
- "note"
72
+ "note",
73
+ // Зависимость прогона снята НЕуспешным финалом блокера (TASWM-361): тот остановлен/упал/
74
+ // переигран, успешного финала не будет — ждать больше нечего. Пишется в ленту ЖДАВШЕГО
75
+ // прогона перед самым спавном: «поехал молча» означало бы, что причина запуска исчезла из
76
+ // истории, и следующий разбор «почему он стартовал поверх незакрытого блокера» упёрся бы в
77
+ // пустоту. Успешное снятие (блокер смержен) события не порождает — это штатный ход.
78
+ "dep_cleared"
73
79
  ];
74
80
  MILESTONE_KINDS = [
75
81
  "spawned",
@@ -90,8 +96,10 @@ var init_types = __esm({
90
96
  "storm_dead",
91
97
  "anomaly",
92
98
  // надзор overmind (TASWM-194) — та же нотификация, что улетела в инбокс, вехой в Истории
93
- "note"
99
+ "note",
94
100
  // заметка дрона «замечено по пути» (TASWM-279) — веха, иначе находка тонет в ленте step'ов
101
+ "dep_cleared"
102
+ // зависимость снята неуспешным финалом блокера (TASWM-361) — решение роя, веха как conflict
95
103
  ];
96
104
  MILESTONE_KIND_SET = new Set(MILESTONE_KINDS);
97
105
  }
@@ -356,6 +364,43 @@ var init_status = __esm({
356
364
  }
357
365
  });
358
366
 
367
+ // ../swarm-core/src/deps.ts
368
+ function judgeDep(runs) {
369
+ const real = runs.filter((r) => !r.service);
370
+ if (real.length === 0) return { ...PENDING };
371
+ if (real.some((r) => !isTerminalStatus(r.status))) return { ...PENDING };
372
+ if (real.some((r) => r.status === "done")) {
373
+ return { outcome: "done", met: true, status: "done", name: null, stopReason: null };
374
+ }
375
+ const last = real[real.length - 1];
376
+ return {
377
+ outcome: "exhausted",
378
+ met: true,
379
+ status: last.status,
380
+ name: last.name ?? null,
381
+ stopReason: last.stopReason ?? null
382
+ };
383
+ }
384
+ function depClearedText(slug, v) {
385
+ const who = v.name ? `${slug} (${v.name})` : slug;
386
+ const how = DEP_END_LABEL[v.status ?? ""] ?? `\u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D (${v.status ?? "?"})`;
387
+ const why = v.stopReason ? ` \u2014 ${v.stopReason}` : "";
388
+ return `\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C \u0441\u043D\u044F\u0442\u0430: \u0436\u0434\u0430\u043B ${who}, \u0442\u043E\u0442 ${how}${why}. \u0423\u0441\u043F\u0435\u0448\u043D\u043E\u0433\u043E \u0444\u0438\u043D\u0430\u043B\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u2014 \u0435\u0434\u0443.`;
389
+ }
390
+ var PENDING, DEP_END_LABEL;
391
+ var init_deps = __esm({
392
+ "../swarm-core/src/deps.ts"() {
393
+ "use strict";
394
+ init_status();
395
+ PENDING = { outcome: "pending", met: false, status: null, name: null, stopReason: null };
396
+ DEP_END_LABEL = {
397
+ stopped: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C",
398
+ failed: "\u0443\u043F\u0430\u043B",
399
+ superseded: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D"
400
+ };
401
+ }
402
+ });
403
+
359
404
  // ../swarm-core/src/time.ts
360
405
  function stamp(d = /* @__PURE__ */ new Date()) {
361
406
  return d.toISOString().slice(0, 19).replace("T", " ");
@@ -1230,6 +1275,7 @@ var init_src = __esm({
1230
1275
  init_timeline();
1231
1276
  init_reviewPhase();
1232
1277
  init_status();
1278
+ init_deps();
1233
1279
  init_time();
1234
1280
  init_windows();
1235
1281
  init_slug();
@@ -4059,6 +4105,16 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
4059
4105
  const text = res?.content?.find((c) => c.type === "text")?.text;
4060
4106
  return typeof text === "string" && text.trim() ? text : null;
4061
4107
  }
4108
+ async function reportAcceptanceCheck(input) {
4109
+ const res = await safeCall("report_acceptance_check", {
4110
+ id: input.id,
4111
+ state: input.state,
4112
+ ...input.evidence ? { evidence: input.evidence } : {}
4113
+ });
4114
+ if (!res) return null;
4115
+ const text = res.content?.find((c) => c.type === "text")?.text;
4116
+ return typeof text === "string" && text.trim() ? text : null;
4117
+ }
4062
4118
  async function resolveId(idOrSlug) {
4063
4119
  const cached = idCache.get(idOrSlug);
4064
4120
  if (cached) return cached;
@@ -4127,6 +4183,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
4127
4183
  backlogStatusId,
4128
4184
  getDocument,
4129
4185
  getTaskContext,
4186
+ reportAcceptanceCheck,
4130
4187
  createTask: async (input) => {
4131
4188
  if (!input.projectId) {
4132
4189
  throw new Error("createTask: projectId \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u2014 \u0430\u0432\u0442\u043E-\u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0440\u043E\u0435\u043C \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u043E (TASWM-204)");
@@ -4375,7 +4432,7 @@ var init_taskless = __esm({
4375
4432
  init_agentToken();
4376
4433
  init_workspace();
4377
4434
  SLUG_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
4378
- WS_FREE_TOOLS = /* @__PURE__ */ new Set(["list_workspaces", "who_am_i", "finalize_upload"]);
4435
+ WS_FREE_TOOLS = /* @__PURE__ */ new Set(["list_workspaces", "who_am_i", "finalize_upload", "report_acceptance_check"]);
4379
4436
  TaskBodyUnavailableError = class extends Error {
4380
4437
  constructor(slug, workspaceId) {
4381
4438
  super(
@@ -4868,18 +4925,40 @@ CREATE TABLE IF NOT EXISTS imports (
4868
4925
  return rows.map((r) => r.area);
4869
4926
  }
4870
4927
  /**
4871
- * Из depends_on прогона — слаги зависимостей, ещё НЕ закрытых (нет прогона со
4872
- * статусом done). Пусто → все выполнены, дрона можно спавнить. Явная зависимость
4873
- * (spawn --after) отдельна от area-lock: area освобождается на review (до мержа),
4874
- * а тут ждём именно done (смерж), чтобы следующий дрон стартовал от кода предыдущего.
4875
- * Несуществующий/зациклённый слаг просто никогда не станет done → прогон вечно ждёт,
4876
- * но очередь не виснет (гейт — пропуск в цикле, не блокировка).
4928
+ * Из depends_on прогона — слаги зависимостей, ещё НЕ исчерпанных. Пусто → все выполнены,
4929
+ * дрона можно спавнить. Явная зависимость (spawn --after) отдельна от area-lock: area
4930
+ * освобождается на review (до мержа), а тут ждём финала блокера, чтобы следующий дрон
4931
+ * стартовал от его кода.
4932
+ *
4933
+ * TASWM-361: критерий — «живых прогонов по слагу не осталось», а не «есть done». Раньше
4934
+ * зависимость снимал ТОЛЬКО успех, и остановленный оператором блокер держал ждущих вечно
4935
+ * (VESPA и TYPHON — сутки очереди при пустом рое). Само правило и его обоснование —
4936
+ * `judgeDep` в swarm-core/deps.ts (его же читают зеркало прода и прод-снапшот).
4937
+ *
4938
+ * Несуществующий/зациклённый слаг прогонов не имеет вовсе → `pending` → прогон ждёт, как и
4939
+ * раньше, но очередь не виснет (гейт — пропуск в цикле, не блокировка).
4877
4940
  */
4878
4941
  unmetDeps(depends_on) {
4942
+ return this.depVerdicts(depends_on).filter((d) => !d.met).map((d) => d.slug);
4943
+ }
4944
+ /**
4945
+ * TASWM-361: вердикт по каждой зависимости — не только «держит ли», но и КАК закрылась
4946
+ * (успехом блокера или его неуспешным финалом). Разница нужна overmind: снятие по
4947
+ * неуспеху оставляет след в ленге ждавшего прогона, штатное — нет.
4948
+ */
4949
+ depVerdicts(depends_on) {
4879
4950
  if (!depends_on || depends_on.length === 0) return [];
4880
- const rows = this.db.prepare("SELECT DISTINCT task_slug FROM runs WHERE status = 'done' AND resolves_run_id IS NULL").all();
4881
- const done = new Set(rows.map((r) => r.task_slug));
4882
- return depends_on.filter((slug) => !done.has(slug));
4951
+ const slugs = [...new Set(depends_on)];
4952
+ const rows = this.db.prepare(
4953
+ `SELECT task_slug, status, name, stop_reason FROM runs WHERE task_slug IN (${slugs.map(() => "?").join(",")}) AND resolves_run_id IS NULL AND reviews_run_id IS NULL ORDER BY id`
4954
+ ).all(...slugs);
4955
+ const bySlug = /* @__PURE__ */ new Map();
4956
+ for (const r of rows) {
4957
+ const list = bySlug.get(r.task_slug) ?? [];
4958
+ list.push({ status: r.status, name: r.name, stopReason: r.stop_reason });
4959
+ bySlug.set(r.task_slug, list);
4960
+ }
4961
+ return depends_on.map((slug) => ({ slug, ...judgeDep(bySlug.get(slug) ?? []) }));
4883
4962
  }
4884
4963
  /** Все репо, засветившиеся в прогонах — whitelist для валидации spawn из веба. */
4885
4964
  distinctRepos() {
@@ -5568,10 +5647,13 @@ var SwarmClient = class _SwarmClient {
5568
5647
  // редко — без этого лог был бы простынёй из одной и той же строки, и в ней потонуло бы
5569
5648
  // всё остальное. Меняется набор (эпик закрыли / блокер прошли) → печатаем снова.
5570
5649
  lastClaimSkipLog = null;
5571
- // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
5572
- // Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
5573
- // переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
5574
- depsDoneInProd = /* @__PURE__ */ new Set();
5650
+ // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → вердикт по ВСЕМ его прогонам.
5651
+ // `done` монотонен (не откатывается) → положительный ответ не протухает и больше не
5652
+ // спрашивается; прочие вердикты переспрашиваются по TTL — «исчерпана неуспешно»
5653
+ // (TASWM-361) монотонностью НЕ обладает: оператор вправе переигрть остановленный блокер
5654
+ // новым прогоном того же слага, и тогда зависимость снова становится ожиданием.
5655
+ // Живёт в памяти: рестарт демона просто переспросит прод.
5656
+ depsProd = /* @__PURE__ */ new Map();
5575
5657
  depsAskedAt = /* @__PURE__ */ new Map();
5576
5658
  static DEPS_TTL_MS = 6e4;
5577
5659
  // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
@@ -5998,12 +6080,36 @@ var SwarmClient = class _SwarmClient {
5998
6080
  return [...seen];
5999
6081
  }
6000
6082
  unmetDeps(depends_on) {
6083
+ return this.depVerdicts(depends_on).filter((d) => !d.met).map((d) => d.slug);
6084
+ }
6085
+ /**
6086
+ * TASWM-361: вердикт по каждой зависимости — см. докблок SwarmStore.depVerdicts.
6087
+ *
6088
+ * АСИММЕТРИЯ ЗЕРКАЛА И ПРОДА, и почему она здесь намеренная. Зеркало — ЧАСТИЧНАЯ выборка
6089
+ * (только прогоны этой машины, только окно гидрации), и по частичной выборке два вывода
6090
+ * стоят разного:
6091
+ * • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он не
6092
+ * портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
6093
+ * • «живых прогонов слага не осталось» — вывод от ОТСУТСТВИЯ: невидимый зеркалу прогон
6094
+ * на соседней машине как раз и может быть живым. Поэтому «исчерпана неуспешно»
6095
+ * принимаем ТОЛЬКО из ответа прода (refreshDeps) — он видит всех.
6096
+ * Прод недоступен → вердикта нет → прогон честно ждёт следующего окна: лишняя минута
6097
+ * очереди дешевле дрона, поднятого поверх работающего блокера.
6098
+ */
6099
+ depVerdicts(depends_on) {
6001
6100
  if (!depends_on || depends_on.length === 0) return [];
6002
- const done = new Set(this.depsDoneInProd);
6101
+ const doneInMirror = /* @__PURE__ */ new Set();
6003
6102
  for (const r of this.runs.values()) {
6004
- if (r.status === "done" && r.resolves_run_id == null) done.add(r.task_slug);
6103
+ if (r.status === "done" && r.resolves_run_id == null && r.reviews_run_id == null) doneInMirror.add(r.task_slug);
6005
6104
  }
6006
- return depends_on.filter((slug) => !done.has(slug));
6105
+ return depends_on.map((slug) => {
6106
+ if (doneInMirror.has(slug)) {
6107
+ return { slug, outcome: "done", met: true, status: "done", name: null, stopReason: null };
6108
+ }
6109
+ const fromProd = this.depsProd.get(slug);
6110
+ if (fromProd) return { slug, ...fromProd };
6111
+ return { slug, outcome: "pending", met: false, status: null, name: null, stopReason: null };
6112
+ });
6007
6113
  }
6008
6114
  /**
6009
6115
  * TASWM-300: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
@@ -6014,14 +6120,15 @@ var SwarmClient = class _SwarmClient {
6014
6120
  * просто позавчера, в зеркало не попадала — и `--after` держал прогон в очереди вечно
6015
6121
  * (04.08: снимали руками).
6016
6122
  *
6017
- * Кеш. «Выполнено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
6018
- * положительный ответ помним навсегда и больше не спрашиваем. Отрицательный переспрашиваем
6123
+ * Кеш. «Смержено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
6124
+ * положительный ответ помним навсегда и больше не спрашиваем. Остальные вердикты (включая
6125
+ * «исчерпана неуспешно», TASWM-361 — блокер могут переиграть новым прогоном) переспрашиваем
6019
6126
  * не чаще DEPS_TTL_MS: тик идёт раз в ~4с, а ждущих зависимость прогонов может быть
6020
6127
  * много — без TTL это был бы поход в прод на каждый слаг каждый тик.
6021
6128
  */
6022
6129
  async refreshDeps(slugs, nowMs = Date.now()) {
6023
6130
  for (const slug of new Set(slugs)) {
6024
- if (!slug || this.depsDoneInProd.has(slug)) continue;
6131
+ if (!slug || this.depsProd.get(slug)?.outcome === "done") continue;
6025
6132
  const asked = this.depsAskedAt.get(slug);
6026
6133
  if (asked !== void 0 && nowMs - asked < _SwarmClient.DEPS_TTL_MS) continue;
6027
6134
  this.depsAskedAt.set(slug, nowMs);
@@ -6034,7 +6141,18 @@ var SwarmClient = class _SwarmClient {
6034
6141
  }
6035
6142
  if (res?.isError) continue;
6036
6143
  const runs = res.structuredContent?.runs ?? [];
6037
- if (runs.some((r) => r.status === "done" && !r.resolvesRunId)) this.depsDoneInProd.add(slug);
6144
+ const facts = runs.slice().sort((a, b) => String(a.createdAt ?? "").localeCompare(String(b.createdAt ?? ""))).map((r) => ({
6145
+ status: String(r.status ?? ""),
6146
+ // Служебный сабран под тем же слагом (резолвер конфликта / ревьюер): его финал —
6147
+ // не финал задачи. Резолвер вычёркивался и до TASWM-361, ревьюер добавлен вместе
6148
+ // с расширением критерия — иначе остановленный ревьюер закрывал бы зависимость.
6149
+ service: Boolean(r.resolvesRunId) || Boolean(r.reviewsRunId),
6150
+ // В проде имя дрона — колонка droneName (см. hydrateRun); `name` терпим как
6151
+ // запасной вариант ради фикстур и старых ответов.
6152
+ name: r.droneName ?? r.name ?? null,
6153
+ stopReason: r.stopReason ?? null
6154
+ }));
6155
+ this.depsProd.set(slug, judgeDep(facts));
6038
6156
  }
6039
6157
  }
6040
6158
  /**
@@ -6579,6 +6697,19 @@ var SwarmClient = class _SwarmClient {
6579
6697
  * → null: ридер (makeMaxDronesReader) берёт env-фолбэк.
6580
6698
  */
6581
6699
  async ownerMaxDrones() {
6700
+ return (await this.ownerSwarmSettings())?.maxDrones ?? null;
6701
+ }
6702
+ /**
6703
+ * TASWM-364: настройки роя ЦЕЛИКОМ из того же who_am_i — живой кап дронов И модели, на
6704
+ * которых спавнятся дроны и ревьюеры (раздел «Рой» в настройках). Одним вызовом, а не
6705
+ * двумя: читатель у них один (ридер maxDrones раз в TTL), и второй сетевой поход за
6706
+ * соседним полем того же ответа был бы платой ни за что.
6707
+ *
6708
+ * Прод недоступен / ответ без валидного капа → null: вызывающий берёт env-фолбэки
6709
+ * (SWARM_MAX_IN_FLIGHT для капа, SWARM_DRONE_MODEL для модели). Модель без капа отдельно
6710
+ * не отдаём СОЗНАТЕЛЬНО — «ответ прода не разобрался» это одно событие, а не два.
6711
+ */
6712
+ async ownerSwarmSettings() {
6582
6713
  let res;
6583
6714
  try {
6584
6715
  res = await this.call("who_am_i", {});
@@ -6588,7 +6719,14 @@ var SwarmClient = class _SwarmClient {
6588
6719
  if (res?.isError) return null;
6589
6720
  const obj = jsonFromTextContent(res);
6590
6721
  const n = obj?.maxDrones;
6591
- return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
6722
+ if (!(typeof n === "number" && Number.isFinite(n) && n > 0)) return null;
6723
+ const str2 = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
6724
+ return {
6725
+ maxDrones: Math.trunc(n),
6726
+ // Старый прод (до TASWM-364) поля swarmSettings не отдаёт вовсе — обе модели null,
6727
+ // то есть прежнее поведение: env машины и дефолт харнесса.
6728
+ models: { drone: str2(obj?.swarmSettings?.droneModel), reviewer: str2(obj?.swarmSettings?.reviewerModel) }
6729
+ };
6592
6730
  }
6593
6731
  /**
6594
6732
  * Live verifyMode проекта из Taskless (TASWM-171): читает swarm_repo_config по
@@ -8079,6 +8217,16 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
8079
8217
  };
8080
8218
  }
8081
8219
 
8220
+ // src/swarmModels.ts
8221
+ function resolveRunModel(run, swarm, env = process.env) {
8222
+ const pick2 = (v) => {
8223
+ const s = typeof v === "string" ? v.trim() : "";
8224
+ return s.length > 0 ? s : null;
8225
+ };
8226
+ const fromSwarm = run.kind === "reviewer" ? pick2(swarm?.reviewer) ?? pick2(swarm?.drone) : pick2(swarm?.drone);
8227
+ return pick2(run.model) ?? fromSwarm ?? pick2(env.SWARM_DRONE_MODEL) ?? null;
8228
+ }
8229
+
8082
8230
  // src/launcher.ts
8083
8231
  init_executors();
8084
8232
  var requireFromHere = createRequire(import.meta.url);
@@ -8216,7 +8364,7 @@ function makeLauncher(cfg) {
8216
8364
  ...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
8217
8365
  }
8218
8366
  };
8219
- const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8367
+ const model = resolveRunModel(run, cfg.swarmModels?.() ?? null);
8220
8368
  const realHome = cfg.droneIsolation?.realHome ?? homedir4();
8221
8369
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
8222
8370
  if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join12(cfg.droneIsolation.swarmHome, "token"))) {
@@ -8284,7 +8432,7 @@ function accountRunCost(deps, run) {
8284
8432
  const dir = join13(deps.runLogsDir, String(run.id));
8285
8433
  const files = unprocessedLogFiles(dir);
8286
8434
  if (files.length === 0) return;
8287
- const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8435
+ const model = resolveRunModel(run, deps.swarmModels?.() ?? null);
8288
8436
  const executor = deps.executor ?? resolveExecutor();
8289
8437
  let added = 0;
8290
8438
  let tokensIn = 0;
@@ -9388,6 +9536,41 @@ ${notFoundText}` : notFoundText }],
9388
9536
  return { content: [{ type: "text", text: lines.join("\n") }] };
9389
9537
  }
9390
9538
  );
9539
+ server.tool(
9540
+ "report_acceptance_check",
9541
+ "\u0417\u0430\u043A\u0440\u044B\u0442\u044C \u043E\u0434\u0438\u043D \u043F\u0443\u043D\u043A\u0442 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0435\u0432 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 \u0442\u0432\u043E\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438: passed \u2014 \u043F\u0440\u043E\u0448\u043B\u0430, failed \u2014 \u0443\u043F\u0430\u043B\u0430, pending \u2014 \u0441\u043D\u043E\u0432\u0430 \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u043B\u0430\u0441\u044C. id \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0431\u0435\u0440\u0438 \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 (\u0441\u0442\u0440\u043E\u043A\u0430 \xAB(id: \u2026)\xBB) \u0432 \u043F\u0440\u043E\u043C\u043F\u0442\u0435 \u0438\u043B\u0438 \u0432 read_task. evidence \u2014 \u0414\u041E\u041A\u0410\u0417\u0410\u0422\u0415\u041B\u042C\u0421\u0422\u0412\u041E: \u0441\u0442\u0440\u043E\u043A\u0430 \u0432\u044B\u0432\u043E\u0434\u0430 \u043A\u043E\u043C\u0430\u043D\u0434\u044B, \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 PR/\u0441\u043A\u0440\u0438\u043D, \u0447\u0442\u043E \u0438\u043C\u0435\u043D\u043D\u043E \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u043B. \u0421\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u0441\u0442\u0430\u0432\u044C \u043F\u043E \u0424\u0410\u041A\u0422\u0423 \u043F\u0440\u043E\u0433\u043E\u043D\u0430, \u0430 \u043D\u0435 \u043F\u043E \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044E: \xAB\u0443\u043F\u0430\u043B\u0430\xBB \u0438 \xAB\u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u043B\u0430\u0441\u044C\xBB \u2014 \u0440\u0430\u0437\u043D\u043E\u0435. \u041F\u0440\u043E\u0439\u0434\u0438\u0441\u044C \u043F\u043E \u0432\u0441\u0435\u043C \u043F\u0443\u043D\u043A\u0442\u0430\u043C \u041F\u0415\u0420\u0415\u0414 done(): \u043D\u0435\u043F\u0440\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043D\u044B\u0435 \u0433\u0430\u043B\u043E\u0447\u043A\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442 \u0440\u0443\u043A\u0430\u043C\u0438 \u043F\u043E \u0442\u0432\u043E\u0435\u043C\u0443 \u043E\u0442\u0447\u0451\u0442\u0443 \u2014 \u0440\u043E\u0432\u043D\u043E \u0442\u0430 \u0440\u0430\u0431\u043E\u0442\u0430, \u043A\u043E\u0442\u043E\u0440\u0443\u044E \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0441\u043D\u0438\u043C\u0430\u044E\u0442. \u0421\u0442\u0430\u0432\u0438\u0442\u044C \u0441\u0430\u043C\u0438 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u043D\u0435\u0447\u0435\u043C: \u0438\u0445 \u0437\u0430\u0434\u0430\u0451\u0442 \u043F\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0449\u0438\u043A.",
9542
+ {
9543
+ id: z.string().describe("id \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \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"),
9544
+ state: z.enum(["passed", "failed", "pending"]).describe("passed | failed | pending \u2014 \u043F\u043E \u0444\u0430\u043A\u0442\u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430"),
9545
+ evidence: z.string().max(4e3).optional().describe("\u0447\u0435\u043C \u0434\u043E\u043A\u0430\u0437\u0430\u043D\u043E: \u0432\u044B\u0432\u043E\u0434 \u043A\u043E\u043C\u0430\u043D\u0434\u044B, \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 PR/\u0441\u043A\u0440\u0438\u043D, \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0435\u043C\u043E\u0435 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435")
9546
+ },
9547
+ async ({ id, state, evidence }) => {
9548
+ if (!opts.taskless) {
9549
+ return {
9550
+ content: [
9551
+ {
9552
+ type: "text",
9553
+ text: "Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u0433\u0430\u043B\u043E\u0447\u043A\u0443 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u043C. \u041E\u0442\u0447\u0438\u0442\u0430\u0439\u0441\u044F \u043F\u043E \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u044E \u0442\u0435\u043A\u0441\u0442\u043E\u043C \u0432 summary \u043F\u0440\u0438 done()."
9554
+ }
9555
+ ],
9556
+ isError: true
9557
+ };
9558
+ }
9559
+ const text = await opts.taskless.reportAcceptanceCheck({ id, state, evidence });
9560
+ if (!text) {
9561
+ return {
9562
+ content: [
9563
+ {
9564
+ type: "text",
9565
+ text: `\u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 ${id} \u043D\u0435 \u043E\u0442\u043C\u0435\u0447\u0435\u043D\u0430 (\u0441\u043C. \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430): id \u043C\u043E\u0433 \u0431\u044B\u0442\u044C \u0447\u0443\u0436\u0438\u043C \u0438\u043B\u0438 \u0443\u0441\u0442\u0430\u0440\u0435\u0442\u044C \u2014 \u043F\u0435\u0440\u0435\u0447\u0438\u0442\u0430\u0439 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 read_task. \u041D\u0435 \u043C\u043E\u043B\u0447\u0438 \u043E\u0431 \u044D\u0442\u043E\u043C \u0432 summary \u043F\u0440\u0438 done().`
9566
+ }
9567
+ ],
9568
+ isError: true
9569
+ };
9570
+ }
9571
+ return { content: [{ type: "text", text }] };
9572
+ }
9573
+ );
9391
9574
  server.tool(
9392
9575
  "ask",
9393
9576
  "\u0417\u0430\u0434\u0430\u0442\u044C \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u0438 \u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F \u043E\u0442\u0432\u0435\u0442\u0430. \u0415\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F \u2014 \u0434\u0430\u0439 options (2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430, \u0441\u0442\u0430\u043D\u0443\u0442 \u043A\u043D\u043E\u043F\u043A\u0430\u043C\u0438); \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0432\u044B\u0431\u0435\u0440\u0435\u0442 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 \u0438\u043B\u0438 \u043D\u0430\u043F\u0438\u0448\u0435\u0442 \u0441\u0432\u043E\u0451. \u041E\u043F\u0443\u0441\u0442\u0438 options \u0434\u043B\u044F \u043E\u0442\u043A\u0440\u044B\u0442\u043E\u0433\u043E \u0432\u043E\u043F\u0440\u043E\u0441\u0430. \u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043E\u0442\u0432\u0435\u0442 \u0441\u0442\u0440\u043E\u043A\u043E\u0439. \u0417\u043E\u0432\u0438, \u043A\u043E\u0433\u0434\u0430 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u2014 \u043D\u0435 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439.",
@@ -9480,13 +9663,31 @@ ${notFoundText}` : notFoundText }],
9480
9663
  isError: true
9481
9664
  };
9482
9665
  }
9666
+ const reviewerBySpawn = opts.reviewsRunId != null || opts.runKind === "reviewer";
9667
+ const iAmReviewer = run?.reviews_run_id != null || run?.kind === "reviewer" || reviewerBySpawn;
9668
+ if (iAmReviewer && run?.reviews_run_id == null) {
9669
+ return {
9670
+ content: [
9671
+ {
9672
+ type: "text",
9673
+ text: '\u0422\u044B \u0434\u0440\u043E\u043D-\u0440\u0435\u0432\u044C\u044E\u0435\u0440, \u043D\u043E \u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u0441\u0432\u044F\u0437\u044C \u0441 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u043C \u2014 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u043A\u0443\u0434\u0430, \u0438 \u0437\u0430\u043A\u0440\u044B\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u043B\u044C\u0437\u044F (TASWM-375). \u041E\u0441\u0442\u0430\u0432\u044C \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 PR (`gh pr comment <pr> --body "**APPROVE**|**REQUEST CHANGES** \u2026"`) \u2014 \u0440\u043E\u0439 \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u0438\u0442 \u0435\u0433\u043E \u043E\u0442\u0442\u0443\u0434\u0430 \u2014 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438 done() \u0447\u0435\u0440\u0435\u0437 \u043C\u0438\u043D\u0443\u0442\u0443.'
9674
+ }
9675
+ ],
9676
+ isError: true
9677
+ };
9678
+ }
9483
9679
  if (run?.reviews_run_id != null) {
9484
9680
  if (isTerminalStatus(run.status)) {
9485
9681
  return { content: [{ type: "text", text: "\u043F\u0440\u043E\u0433\u043E\u043D \u0443\u0436\u0435 \u0437\u0430\u043A\u0440\u044B\u0442 (PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D" }] };
9486
9682
  }
9487
9683
  if (verdict !== "approve" && verdict !== "changes") {
9488
9684
  return {
9489
- content: [{ type: "text", text: '\u0422\u044B \u0434\u0440\u043E\u043D-\u0440\u0435\u0432\u044C\u044E\u0435\u0440 (\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E, TASWM-148) \u2014 done() \u0442\u0440\u0435\u0431\u0443\u0435\u0442 verdict: "approve" | "changes" (\u0438\u0442\u043E\u0433 \u043F\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u043E\u043C\u0443 PR).' }],
9685
+ content: [
9686
+ {
9687
+ type: "text",
9688
+ text: '\u0422\u044B \u0434\u0440\u043E\u043D-\u0440\u0435\u0432\u044C\u044E\u0435\u0440 (\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E, TASWM-148) \u2014 done() \u0442\u0440\u0435\u0431\u0443\u0435\u0442 verdict: "approve" | "changes" (\u0438\u0442\u043E\u0433 \u043F\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u043E\u043C\u0443 PR). \u0422\u0440\u0435\u0442\u044C\u0435\u0433\u043E \u0438\u0441\u0445\u043E\u0434\u0430 \u043D\u0435\u0442, \u0438 \u0443\u0441\u043F\u0435\u0445\u043E\u043C \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043F\u0440\u043E\u0433\u043E\u043D \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u0442\u0441\u044F (TASWM-375): \u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u043D\u0430 \u043D\u0435\u0433\u043E \u0431\u0443\u0434\u0435\u0442 \u043D\u0435\u0447\u0435\u043C, \u0430 \u043F\u0440\u0430\u0432\u0438\u043B\u043E \xAB\u043D\u0435 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u0434\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430\xBB \u0441\u043E\u0431\u043B\u044E\u0434\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u043C. \u0420\u0435\u0448\u0438\u0442\u044C \u043D\u0435 \u043C\u043E\u0436\u0435\u0448\u044C \u2014 done() \u043D\u0435 \u0437\u043E\u0432\u0438 \u0432\u043E\u0432\u0441\u0435: \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u043E\u0435\u0442\u0441\u044F \u043A\u0430\u043A failed \u0441 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439 \u0438 \u0443\u0439\u0434\u0451\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443, \u044D\u0442\u043E \u0447\u0435\u0441\u0442\u043D\u0435\u0435 \u0442\u0438\u0445\u043E\u0433\u043E \u0443\u0441\u043F\u0435\u0445\u0430.'
9689
+ }
9690
+ ],
9490
9691
  isError: true
9491
9692
  };
9492
9693
  }
@@ -9496,28 +9697,39 @@ ${notFoundText}` : notFoundText }],
9496
9697
  await store.addEvent(run.reviews_run_id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E \u043E\u0442 ${run.name}: ${verdictText}${summary ? ` \u2014 ${summary}` : ""}`);
9497
9698
  const reviewedSlug = reportSlug(reviewed?.task_slug);
9498
9699
  let shots = { attachments: [], warning: "" };
9700
+ let commentWarning = "";
9499
9701
  if (opts.taskless && reviewedSlug) {
9500
9702
  shots = await uploadShots();
9501
- await opts.taskless.addComment(
9502
- reviewedSlug,
9503
- `${(summary ?? "").trim()}${shots.warning}`,
9504
- {
9505
- swarmKind: "review",
9506
- swarmDroneName: run.name,
9507
- swarmVerdict: verdict,
9508
- // TASWM-304: круг — ПРОВЕРЯЕМОГО прогона (у ревьюер-прогона свой
9509
- // review_cycle всегда 0), иначе вердикты всех кругов схлопнулись
9510
- // бы в одну плашку.
9511
- swarmReviewCycle: reviewed?.review_cycle ?? 0
9512
- },
9513
- shots.attachments
9514
- ).catch(() => {
9515
- });
9703
+ try {
9704
+ await opts.taskless.addComment(
9705
+ reviewedSlug,
9706
+ `${(summary ?? "").trim()}${shots.warning}`,
9707
+ {
9708
+ swarmKind: "review",
9709
+ swarmDroneName: run.name,
9710
+ swarmVerdict: verdict,
9711
+ // TASWM-304: круг — ПРОВЕРЯЕМОГО прогона (у ревьюер-прогона свой
9712
+ // review_cycle всегда 0), иначе вердикты всех кругов схлопнулись
9713
+ // бы в одну плашку.
9714
+ swarmReviewCycle: reviewed?.review_cycle ?? 0
9715
+ },
9716
+ shots.attachments
9717
+ );
9718
+ } catch (e) {
9719
+ commentWarning = e instanceof Error ? e.message : String(e);
9720
+ await store.addEvent(run.reviews_run_id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442-\u043A\u043E\u043C\u043C\u0435\u043D\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 ${reviewedSlug} \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${commentWarning}`);
9721
+ }
9722
+ } else {
9723
+ commentWarning = opts.taskless ? "\u0437\u0430\u0434\u0430\u0447\u0430 \u0440\u0435\u0432\u044C\u044E \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430" : "\u043A\u043B\u0438\u0435\u043D\u0442 Taskless \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D";
9724
+ await store.addEvent(run.reviews_run_id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442-\u043A\u043E\u043C\u043C\u0435\u043D\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${commentWarning}`);
9516
9725
  }
9517
9726
  const trustedPrUrl = reviewed?.pr_url ?? pr_url;
9518
9727
  await store.updateRun(null, { status: "review", pr_url: trustedPrUrl });
9519
9728
  await store.addEvent(null, "pr_opened", trustedPrUrl);
9520
- return { content: [{ type: "text", text: `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0437\u0430\u043F\u0438\u0441\u0430\u043D, \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E${shotsNote(shots)}` }] };
9729
+ const commentNote = commentWarning ? `
9730
+
9731
+ \u26A0\uFE0F \u0412\u0435\u0440\u0434\u0438\u043A\u0442-\u043A\u043E\u043C\u043C\u0435\u043D\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 \u041D\u0415 \u0443\u0435\u0445\u0430\u043B (${commentWarning}) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0435\u0441\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u043D\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0435. \u041F\u0440\u043E\u0434\u0443\u0431\u043B\u0438\u0440\u0443\u0439 \u0435\u0433\u043E \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 PR: \`gh pr comment ${trustedPrUrl} --body "${verdictText} \u2026"\`.` : "";
9732
+ return { content: [{ type: "text", text: `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0437\u0430\u043F\u0438\u0441\u0430\u043D, \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E${shotsNote(shots)}${commentNote}` }] };
9521
9733
  }
9522
9734
  if (isGithubPrUrl(pr_url)) {
9523
9735
  const report = (summary ?? "").trim();
@@ -9739,7 +9951,14 @@ async function runDroneServer(runId) {
9739
9951
  );
9740
9952
  const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
9741
9953
  const expectedKind = toRunKind(process.env.SWARM_RUN_KIND);
9742
- const common = { taskless, runKind: expectedKind, prHeadBranch, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
9954
+ const spawnReviewsRunId = Number(process.env.SWARM_REVIEWS_RUN_ID);
9955
+ const common = {
9956
+ taskless,
9957
+ runKind: expectedKind,
9958
+ prHeadBranch,
9959
+ ...expectedTaskSlug ? { expectedTaskSlug } : {},
9960
+ ...Number.isInteger(spawnReviewsRunId) && spawnReviewsRunId > 0 ? { reviewsRunId: spawnReviewsRunId } : {}
9961
+ };
9743
9962
  const channelUrl = process.env.SWARM_DRONE_CHANNEL_URL;
9744
9963
  const channelToken = process.env.SWARM_DRONE_CHANNEL_TOKEN;
9745
9964
  if (channelUrl && channelToken) {
@@ -10894,7 +11113,7 @@ async function changedPaths(worktree, opts = {}) {
10894
11113
  // package.json
10895
11114
  var package_default = {
10896
11115
  name: "@taskless-app/swarm",
10897
- version: "0.6.3",
11116
+ version: "0.6.5",
10898
11117
  type: "module",
10899
11118
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
10900
11119
  keywords: [
@@ -10944,7 +11163,7 @@ var package_default = {
10944
11163
  execa: "^10.0.0",
10945
11164
  zod: "3.25.76"
10946
11165
  },
10947
- gitHead: "298573d4b361b8fc7ea4d6ea9d1e8784906bbd7e"
11166
+ gitHead: "76ff5edd816a2b9dd7f10db62c8fde89eec7771f"
10948
11167
  };
10949
11168
 
10950
11169
  // src/version.ts
@@ -11671,8 +11890,8 @@ var GK = ['claw','spore','eye','hex'];
11671
11890
  var ST = ${jsonForScript(statusTuples())};
11672
11891
  var ORDER = { running:0, blocked:1, paused:2, storm_dead:3, review:4, stalled:5, queued:6, stopped:7, done:8, failed:9, superseded:10 };
11673
11892
  var TERM = ${jsonForScript(Object.fromEntries(TERMINAL_STATUSES.map((s) => [s, 1])))};
11674
- var EVL = { spawned:'\u0434\u0440\u043E\u043D \u0437\u0430\u043F\u0443\u0449\u0435\u043D', step:'', blocked:'\u0432\u043E\u043F\u0440\u043E\u0441 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443', answered:'\u043F\u043E\u043B\u0443\u0447\u0435\u043D \u043E\u0442\u0432\u0435\u0442', pr_opened:'PR \u043E\u0442\u043A\u0440\u044B\u0442', merged:'PR \u0441\u043C\u0435\u0440\u0436\u0451\u043D, \u0437\u0430\u0434\u0430\u0447\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u0430', done:'\u0433\u043E\u0442\u043E\u0432\u043E', failed:'\u0434\u0440\u043E\u043D \u0443\u043F\u0430\u043B', stalled:'\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441/\u043F\u043E\u0433\u0438\u0431', respawned:'\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435', paused:'\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D', resumed:'overmind \u0432\u0435\u0440\u043D\u0443\u043B\u0441\u044F \u2014 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B', rescued:'worktree \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u2014 \u0437\u0430\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438', conflict:'\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 main (merge-main-in)', died:'\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440', storm_start:'\u26A1 \u043F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 \u2014 Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442', storm_end:'\u0441\u0432\u044F\u0437\u044C \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u2014 \u0440\u043E\u0439 \u043F\u0440\u043E\u0431\u0443\u0436\u0434\u0430\u0435\u0442\u0441\u044F', storm_dead:'\u0434\u0440\u043E\u043D \u0432 \u043A\u043E\u043A\u043E\u043D\u0435 (\u0448\u0442\u043E\u0440\u043C)', stopped:'\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D', superseded:'\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D', anomaly:'\u26A0\uFE0F \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u044F \u0440\u043E\u044F' };
11675
- var MDOT = { merged:'--success', done:'--success', pr_opened:'--info', spawned:'--muted-foreground', stalled:'--destructive', failed:'--destructive', respawned:'--warning', paused:'--warning', resumed:'--success', rescued:'--destructive', conflict:'--warning', died:'--destructive', storm_start:'--destructive', storm_end:'--success', storm_dead:'--warning', stopped:'--muted-foreground', superseded:'--muted-foreground', anomaly:'--warning' };
11893
+ var EVL = { spawned:'\u0434\u0440\u043E\u043D \u0437\u0430\u043F\u0443\u0449\u0435\u043D', step:'', blocked:'\u0432\u043E\u043F\u0440\u043E\u0441 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443', answered:'\u043F\u043E\u043B\u0443\u0447\u0435\u043D \u043E\u0442\u0432\u0435\u0442', pr_opened:'PR \u043E\u0442\u043A\u0440\u044B\u0442', merged:'PR \u0441\u043C\u0435\u0440\u0436\u0451\u043D, \u0437\u0430\u0434\u0430\u0447\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u0430', done:'\u0433\u043E\u0442\u043E\u0432\u043E', failed:'\u0434\u0440\u043E\u043D \u0443\u043F\u0430\u043B', stalled:'\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441/\u043F\u043E\u0433\u0438\u0431', respawned:'\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435', paused:'\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D', resumed:'overmind \u0432\u0435\u0440\u043D\u0443\u043B\u0441\u044F \u2014 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B', rescued:'worktree \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u2014 \u0437\u0430\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438', conflict:'\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 main (merge-main-in)', died:'\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440', storm_start:'\u26A1 \u043F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 \u2014 Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442', storm_end:'\u0441\u0432\u044F\u0437\u044C \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u2014 \u0440\u043E\u0439 \u043F\u0440\u043E\u0431\u0443\u0436\u0434\u0430\u0435\u0442\u0441\u044F', storm_dead:'\u0434\u0440\u043E\u043D \u0432 \u043A\u043E\u043A\u043E\u043D\u0435 (\u0448\u0442\u043E\u0440\u043C)', stopped:'\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D', superseded:'\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D', anomaly:'\u26A0\uFE0F \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u044F \u0440\u043E\u044F', dep_cleared:'\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C \u0441\u043D\u044F\u0442\u0430 \u2014 \u0431\u043B\u043E\u043A\u0435\u0440 \u043D\u0435 \u0434\u043E\u0435\u0434\u0435\u0442' };
11894
+ var MDOT = { merged:'--success', done:'--success', pr_opened:'--info', spawned:'--muted-foreground', stalled:'--destructive', failed:'--destructive', respawned:'--warning', paused:'--warning', resumed:'--success', rescued:'--destructive', conflict:'--warning', died:'--destructive', storm_start:'--destructive', storm_end:'--success', storm_dead:'--warning', stopped:'--muted-foreground', superseded:'--muted-foreground', anomaly:'--warning', dep_cleared:'--warning' };
11676
11895
 
11677
11896
  function E(s){ return String(s==null?"":s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]}); }
11678
11897
  function color(st){ return 'var('+(ST[st]||ST.queued)[2]+')'; }
@@ -13434,6 +13653,7 @@ var NEEDS_HUMAN_MARKS = [
13434
13653
  CHANGES_WAKE_ESCALATION_MARK
13435
13654
  ];
13436
13655
  var SELF_REFERENCE_MARK = "PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430";
13656
+ var REVIEW_NO_VERDICT_REASON = "\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u043E\u0441\u044C \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430: \u043D\u0438 approve, \u043D\u0438 changes \u2014 \u0437\u0430\u0441\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E, \u043D\u0443\u0436\u0435\u043D \u043D\u043E\u0432\u044B\u0439 \u0437\u0430\u0445\u043E\u0434 (TASWM-375)";
13437
13657
  function verdictCommentMark(createdAt) {
13438
13658
  return `\u043A\u043E\u043C\u043C\u0435\u043D\u0442-\u0432\u0435\u0440\u0434\u0438\u043A\u0442@${createdAt}`;
13439
13659
  }
@@ -13764,7 +13984,7 @@ function buildCiReadyNudge(prUrl, over) {
13764
13984
  head,
13765
13985
  "",
13766
13986
  "\u0422\u044B \u2014 \u0442\u043E\u0442 \u0436\u0435 \u0434\u0440\u043E\u043D-\u0420\u0415\u0412\u042C\u042E\u0415\u0420, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u0423 \u0416\u0415 \u0440\u0430\u0431\u043E\u0442\u0443: \u0444\u0430\u0439\u043B\u044B \u043D\u0435 \u043F\u0440\u0430\u0432\u0438\u0448\u044C, \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u043D\u0435 \u043F\u0443\u0448\u0438\u0448\u044C. \u0427\u0435\u043A-\u043B\u0438\u0441\u0442 \u0438 \u0434\u0438\u0444\u0444 \u0443\u0436\u0435 \u0432 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0435 \u043F\u0440\u043E\u0448\u043B\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u2014 \u043D\u0435 \u043D\u0430\u0447\u0438\u043D\u0430\u0439 \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043D\u043E\u0432\u043E, \u0434\u043E\u0432\u0435\u0434\u0438 \u043D\u0430\u0447\u0430\u0442\u043E\u0435.",
13767
- `\u0417\u0430\u043A\u043E\u043D\u0447\u0438\u043B \u2014 \u043E\u0441\u0442\u0430\u0432\u044C \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C (\`gh pr comment ${prUrl} --body "**APPROVE**|**REQUEST CHANGES** \u2026"\`) \u0438 \u041E\u0411\u042F\u0417\u0410\u0422\u0415\u041B\u042C\u041D\u041E \u0432\u044B\u0437\u043E\u0432\u0438 done(${prUrl}, verdict: "approve"|"changes", summary: "\u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u0447\u0442\u043E \u0440\u0435\u0448\u0438\u043B"). \u0411\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0442\u0432\u043E\u0439 \u0432\u044B\u0445\u043E\u0434 \u0440\u043E\u0439 \u043F\u0440\u0438\u043C\u0435\u0442 \u0437\u0430 \u0441\u043C\u0435\u0440\u0442\u044C, \u0438 \u0440\u0435\u0432\u044C\u044E \u0432\u0441\u0442\u0430\u043D\u0435\u0442.`,
13987
+ `\u0417\u0430\u043A\u043E\u043D\u0447\u0438\u043B \u2014 \u043E\u0441\u0442\u0430\u0432\u044C \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C (\`gh pr comment ${prUrl} --body "**APPROVE**|**REQUEST CHANGES** \u2026"\`) \u0438 \u041E\u0411\u042F\u0417\u0410\u0422\u0415\u041B\u042C\u041D\u041E \u0432\u044B\u0437\u043E\u0432\u0438 done(${prUrl}, verdict: "approve"|"changes", summary: "\u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u0447\u0442\u043E \u0440\u0435\u0448\u0438\u043B"). \u0411\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043F\u0440\u043E\u0433\u043E\u043D \u0443\u0441\u043F\u0435\u0445\u043E\u043C \u043D\u0435 \u0437\u0430\u043A\u0440\u043E\u0435\u0442\u0441\u044F: \u0440\u043E\u0439 \u043F\u043E\u043C\u0435\u0442\u0438\u0442 \u0435\u0433\u043E \xAB\u0443\u043F\u0430\u043B\xBB \u0438 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442 \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043D\u043E\u0432\u043E (TASWM-375) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442, \u0437\u0430 \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0443\u0436\u0435 \u0437\u0430\u043F\u043B\u0430\u0447\u0435\u043D\u043E, \u043F\u0440\u043E\u043F\u0430\u0434\u0451\u0442.`,
13768
13988
  "\u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 MCP-\u0442\u0443\u043B ask."
13769
13989
  ].join("\n");
13770
13990
  }
@@ -13810,7 +14030,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
13810
14030
  // вторую ветку мимо PR. Пишем ту, что реально записана на прогоне; пусто (первый спавн —
13811
14031
  // ветку заводит spawnDrone из слага) — прежняя формулировка.
13812
14032
  `\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"}.`,
13813
- "\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); 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).",
14033
+ "\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).",
13814
14034
  "\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).",
13815
14035
  // TASWM-293: живой случай SCE-5 — ревьюер заблокировал PR за отсутствие CI (у проекта
13816
14036
  // его нет и не предполагается), автора разбудили «закрывать замечания», замечаний по коду
@@ -13989,7 +14209,8 @@ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachments
13989
14209
  " \u0415\u0441\u043B\u0438 \u0445\u043E\u0442\u044C \u043E\u0434\u0438\u043D \u0447\u0435\u043A \u0435\u0449\u0451 PENDING \u2014 \u043D\u0430 \u044D\u0442\u043E\u043C \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0441\u044C: \u0448\u0430\u0433\u0438 2 \u0438 3 \u043D\u0438\u0436\u0435 \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438 \u0446\u0435\u043B\u0438\u043A\u043E\u043C, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0435 \u0432\u044B\u043D\u043E\u0441\u0438, \u043F\u0440\u043E\u0441\u0442\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438 \u0441\u0435\u0441\u0441\u0438\u044E (\u0441\u043C. \u0447\u0435\u043A-\u043B\u0438\u0441\u0442 \u0432\u044B\u0448\u0435, \u043F\u043E\u0447\u0435\u043C\u0443 \u044D\u0442\u043E \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E \u0438 \u0447\u0442\u043E \u043F\u0440\u043E\u0438\u0437\u043E\u0439\u0434\u0451\u0442 \u0434\u0430\u043B\u044C\u0448\u0435).",
13990
14210
  `2. \u0427\u0435\u043A\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0438\u0441\u044C (\u0437\u0435\u043B\u0451\u043D\u044B\u0435 \u0438\u043B\u0438 \u0441 \u043A\u0440\u0430\u0441\u043D\u044B\u043C) \u0438\u043B\u0438 \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043C\u0438\u0442\u0435 \u0438\u0445 \u0432\u043E\u0432\u0441\u0435 \u043D\u0435\u0442 \u2014 \u043E\u0441\u0442\u0430\u0432\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043D\u0430 \u0441\u0430\u043C PR: \`gh pr comment ${prUrl} --body "..."\` (\u041D\u0415 \`gh pr review\` \u2014 GitHub \u0435\u0433\u043E \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442 \u043D\u0430 \u0441\u0432\u043E\u0451\u043C PR). \u041D\u0430\u0447\u043D\u0438 \u0441 **APPROVE** \u0438\u043B\u0438 **REQUEST CHANGES** \u0437\u0430\u0433\u043B\u0430\u0432\u043D\u044B\u043C\u0438, \u0434\u0430\u043B\u044C\u0448\u0435 \u2014 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u0438\u043A\u0430: \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B, \u0447\u0442\u043E \u043D\u0430\u0448\u0451\u043B; \u0434\u043B\u044F REQUEST CHANGES \u2014 \u0442\u043E\u0447\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B/\u0441\u0442\u0440\u043E\u043A\u0438 \u0438 \u0447\u0442\u043E \u0447\u0438\u043D\u0438\u0442\u044C.`
13991
14211
  ],
13992
- `3. \u0412\u044B\u0437\u043E\u0432\u0438 done(${prUrl}, verdict: "approve"|"changes", summary: "\u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u0447\u0442\u043E \u0440\u0435\u0448\u0438\u043B") \u2014 \u0440\u043E\u0439 \u0441\u0430\u043C \u0440\u0430\u0437\u043D\u0435\u0441\u0451\u0442 summary \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A, \u0430 verdict \u0437\u0430\u043F\u0438\u0448\u0435\u0442 \u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D (\u0431\u0435\u0439\u0434\u0436 \u0432 \u0432\u0435\u0431\u0435). \u0422\u044B \u0441\u0430\u043C \u0440\u0435\u0432\u044C\u044E \u041D\u0415 \u043F\u043E\u0434\u043B\u0435\u0436\u0438\u0448\u044C \u2014 \u0440\u0435\u043A\u0443\u0440\u0441\u0438\u0438 \u043D\u0435\u0442.`
14212
+ `3. \u0412\u044B\u0437\u043E\u0432\u0438 done(${prUrl}, verdict: "approve"|"changes", summary: "\u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u0447\u0442\u043E \u0440\u0435\u0448\u0438\u043B") \u2014 \u0440\u043E\u0439 \u0441\u0430\u043C \u0440\u0430\u0437\u043D\u0435\u0441\u0451\u0442 summary \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A, \u0430 verdict \u0437\u0430\u043F\u0438\u0448\u0435\u0442 \u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D (\u0431\u0435\u0439\u0434\u0436 \u0432 \u0432\u0435\u0431\u0435). \u0422\u044B \u0441\u0430\u043C \u0440\u0435\u0432\u044C\u044E \u041D\u0415 \u043F\u043E\u0434\u043B\u0435\u0436\u0438\u0448\u044C \u2014 \u0440\u0435\u043A\u0443\u0440\u0441\u0438\u0438 \u043D\u0435\u0442.`,
14213
+ '\u0412\u0435\u0440\u0434\u0438\u043A\u0442 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D: \u0442\u0440\u0435\u0442\u044C\u0435\u0433\u043E \u0438\u0441\u0445\u043E\u0434\u0430 \u043D\u0435\u0442, \u0438 \u0443\u0441\u043F\u0435\u0445\u043E\u043C \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u0442\u0441\u044F (TASWM-375) \u2014 done() \u0431\u0435\u0437 verdict \u043E\u0442\u043A\u0430\u0436\u0435\u0442, \u0430 \u043F\u0440\u043E\u0433\u043E\u043D, \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u0432\u0448\u0438\u0439\u0441\u044F \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430, \u0440\u043E\u0439 \u043F\u043E\u043C\u0435\u0442\u0438\u0442 "\u0443\u043F\u0430\u043B" \u0438 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442 \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043D\u043E\u0432\u043E. \u041E\u0442\u043A\u0430\u0437\u0430\u043B done() \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0441\u0451 \u0440\u0430\u0432\u043D\u043E \u043E\u0441\u0442\u0430\u0432\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 PR (\u0448\u0430\u0433 2): \u0434\u0435\u043C\u043E\u043D \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u0438\u0442 \u0435\u0433\u043E \u043E\u0442\u0442\u0443\u0434\u0430.'
13993
14214
  ].join("\n");
13994
14215
  }
13995
14216
  function droneLabel(run) {
@@ -14548,6 +14769,19 @@ async function fillSuggestedArea(deps, run) {
14548
14769
  `\u0437\u043E\u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0440\u0435\u043F\u043E: ${guess.zone} (${Math.round(guess.confidence * 100)}% \u2014 ${guess.why}) \u2014 TASWM-348`
14549
14770
  );
14550
14771
  }
14772
+ function noteExhaustedDeps(deps, run, verdicts) {
14773
+ const exhausted = verdicts.filter((d) => d.outcome === "exhausted");
14774
+ if (exhausted.length === 0) return;
14775
+ const already = new Set(
14776
+ deps.db.events(run.id).filter((e) => e.kind === "dep_cleared").map((e) => e.text ?? "")
14777
+ );
14778
+ for (const d of exhausted) {
14779
+ const text = depClearedText(d.slug, d);
14780
+ if (already.has(text)) continue;
14781
+ deps.db.addEvent(run.id, "dep_cleared", text);
14782
+ narrate(deps, `${droneLabel(run)} \u2014 ${text}`);
14783
+ }
14784
+ }
14551
14785
  async function trySpawnQueuedRun(deps, run) {
14552
14786
  if (deps.control?.paused) return "skipped";
14553
14787
  const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
@@ -14558,11 +14792,13 @@ async function trySpawnQueuedRun(deps, run) {
14558
14792
  narrate(deps, `${droneLabel(run)} \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u0430 \u0434\u0440\u0443\u0433\u0438\u043C \u0440\u0430\u043D\u043E\u043C, \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E.`);
14559
14793
  return "closed";
14560
14794
  }
14561
- if (deps.db.unmetDeps(run.depends_on).length > 0) return "skipped";
14795
+ const depVerdicts = deps.db.depVerdicts(run.depends_on);
14796
+ if (depVerdicts.some((d) => !d.met)) return "skipped";
14562
14797
  if (!await envGateAllows(deps, run)) return "skipped";
14563
14798
  if (!await projectQuotaGateAllows(deps, run)) return "skipped";
14564
14799
  if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
14565
14800
  if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
14801
+ noteExhaustedDeps(deps, run, depVerdicts);
14566
14802
  await fillSuggestedArea(deps, run);
14567
14803
  try {
14568
14804
  await deps.spawn(run);
@@ -14927,6 +15163,28 @@ async function postScrapedVerdictReport(deps, runId, taskSlug, verdict, droneNam
14927
15163
  deps.db.addEvent(runId, "step", `\u0434\u0435\u043C\u043E\u043D-\u043E\u0442\u0447\u0451\u0442 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 ${taskSlug} \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${e instanceof Error ? e.message : String(e)}`);
14928
15164
  });
14929
15165
  }
15166
+ async function salvageVerdictFromPrComment(deps, rv, reviewsRunId, since, ctx) {
15167
+ if (!deps.verdictComment || !rv.pr_url) return false;
15168
+ const found = await deps.verdictComment(rv.pr_url, since).catch(() => null);
15169
+ if (!found) return false;
15170
+ const verdictText = verdictLabel(found.verdict);
15171
+ deps.db.updateRun(reviewsRunId, { review_verdict: found.verdict });
15172
+ deps.db.addEvent(reviewsRunId, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E \u043F\u043E\u0434\u0445\u0432\u0430\u0447\u0435\u043D \u0438\u0437 PR-\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430 (${ctx.how}): ${verdictText}`);
15173
+ await closeReviewerRun(
15174
+ deps,
15175
+ rv,
15176
+ `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u044B\u043D\u0435\u0441\u0435\u043D \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C (${verdictText}), ${ctx.how} \u2014 \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043A\u0440\u044B\u0442\u043E \u0431\u0435\u0437 \u0441\u043F\u0430\u0432\u043D\u0430 \u0432\u0442\u043E\u0440\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 (TASWM-229)`
15177
+ );
15178
+ narrate(deps, `${droneLabel(rv)} \u043E\u0441\u0442\u0430\u0432\u0438\u043B \u0432\u0435\u0440\u0434\u0438\u043A\u0442 (${verdictText}) \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u2014 \u0437\u0430\u0441\u0447\u0438\u0442\u0430\u043B, \u0432\u0442\u043E\u0440\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E.`);
15179
+ const reviewed = deps.db.getRun(reviewsRunId);
15180
+ if (reviewed) {
15181
+ await postScrapedVerdictReport(deps, rv.id, reviewed.task_slug, found.verdict, rv.name || "(\u0438\u043C\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)", reviewed.workspace_id, reviewed.review_cycle ?? 0);
15182
+ if (found.verdict === "changes" && !ctx.stormActive && !deps.db.activeResolverFor(reviewed.id)) {
15183
+ await wakeAuthorOnChanges(deps, reviewed, ctx.now, ctx.droneCap);
15184
+ }
15185
+ }
15186
+ return true;
15187
+ }
14930
15188
  function buildEscalationCandidates(db) {
14931
15189
  const candidates = [];
14932
15190
  for (const run of db.listRuns("review")) {
@@ -15086,6 +15344,24 @@ async function tickOnce(deps) {
15086
15344
  continue;
15087
15345
  }
15088
15346
  if (run.reviews_run_id != null) {
15347
+ const reviewed = deps.db.getRun(run.reviews_run_id);
15348
+ const sameCycle = reviewed != null && (reviewed.review_cycle ?? 0) === (run.review_cycle ?? 0);
15349
+ if (reviewed && sameCycle && reviewed.review_verdict == null) {
15350
+ const salvaged = await salvageVerdictFromPrComment(deps, run, run.reviews_run_id, run.created_at, {
15351
+ how: "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u0437\u0432\u0430\u043B done(), \u043D\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u0441\u044F",
15352
+ now,
15353
+ droneCap,
15354
+ stormActive
15355
+ });
15356
+ if (salvaged) continue;
15357
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
15358
+ });
15359
+ deps.db.updateRun(run.id, { status: "failed", stop_reason: REVIEW_NO_VERDICT_REASON });
15360
+ deps.db.addEvent(run.id, "failed", REVIEW_NO_VERDICT_REASON);
15361
+ deps.db.addEvent(run.reviews_run_id, "step", `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${run.name} (#${run.id}) \u0437\u0430\u043A\u0440\u044B\u043B\u0441\u044F \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043B\u043E\u0441\u044C, \u043D\u0443\u0436\u0435\u043D \u043D\u043E\u0432\u044B\u0439 \u0437\u0430\u0445\u043E\u0434`);
15362
+ narrate(deps, `${droneLabel(run)} \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B, \u043D\u0435 \u043E\u0441\u0442\u0430\u0432\u0438\u0432 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430, \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D \u043A\u0430\u043A \u0443\u043F\u0430\u0432\u0448\u0438\u0439: \u0437\u0430\u0441\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E.`);
15363
+ continue;
15364
+ }
15089
15365
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
15090
15366
  });
15091
15367
  deps.db.updateRun(run.id, { status: "done" });
@@ -15359,33 +15635,13 @@ async function tickOnce(deps) {
15359
15635
  }
15360
15636
  }
15361
15637
  if (!alive && run.status === "running" && run.reviews_run_id != null && run.pr_url && deps.verdictComment) {
15362
- const verdict = await deps.verdictComment(run.pr_url, run.created_at).catch(() => null);
15363
- if (verdict) {
15364
- const verdictText = verdictLabel(verdict.verdict);
15365
- deps.db.updateRun(run.reviews_run_id, { review_verdict: verdict.verdict });
15366
- deps.db.addEvent(
15367
- run.reviews_run_id,
15368
- "step",
15369
- `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E \u043F\u043E\u0434\u0445\u0432\u0430\u0447\u0435\u043D \u0438\u0437 PR-\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430 (\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0432\u044B\u0448\u0435\u043B, \u043D\u0435 \u043F\u043E\u0437\u0432\u0430\u0432 done()): ${verdictText}`
15370
- );
15371
- if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
15372
- });
15373
- deps.db.updateRun(run.id, { status: "done" });
15374
- deps.db.addEvent(
15375
- run.id,
15376
- "done",
15377
- `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u044B\u043D\u0435\u0441\u0435\u043D \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C (${verdictText}), \u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u2014 \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043A\u0440\u044B\u0442\u043E \u0431\u0435\u0437 \u0441\u043F\u0430\u0432\u043D\u0430 \u0432\u0442\u043E\u0440\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 (TASWM-229)`
15378
- );
15379
- narrate(deps, `${droneLabel(run)} \u043E\u0441\u0442\u0430\u0432\u0438\u043B \u0432\u0435\u0440\u0434\u0438\u043A\u0442 (${verdictText}) \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0438 \u0432\u044B\u0448\u0435\u043B \u2014 \u0437\u0430\u0441\u0447\u0438\u0442\u0430\u043B, \u0432\u0442\u043E\u0440\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E.`);
15380
- const reviewed = deps.db.getRun(run.reviews_run_id);
15381
- if (reviewed) {
15382
- await postScrapedVerdictReport(deps, run.id, reviewed.task_slug, verdict.verdict, run.name || "(\u0438\u043C\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)", reviewed.workspace_id, reviewed.review_cycle ?? 0);
15383
- if (verdict.verdict === "changes" && !stormActive && !deps.db.activeResolverFor(reviewed.id)) {
15384
- await wakeAuthorOnChanges(deps, reviewed, now, droneCap);
15385
- }
15386
- }
15387
- continue;
15388
- }
15638
+ const salvaged = await salvageVerdictFromPrComment(deps, run, run.reviews_run_id, run.created_at, {
15639
+ how: "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0432\u044B\u0448\u0435\u043B, \u043D\u0435 \u043F\u043E\u0437\u0432\u0430\u0432 done()",
15640
+ now,
15641
+ droneCap,
15642
+ stormActive
15643
+ });
15644
+ if (salvaged) continue;
15389
15645
  }
15390
15646
  if (!alive && run.status === "running" && run.reviews_run_id != null && run.pr_url && deps.ci) {
15391
15647
  const parent = deps.db.getRun(run.reviews_run_id);
@@ -15937,6 +16193,7 @@ async function runDaemon(opts) {
15937
16193
  }
15938
16194
  } : void 0;
15939
16195
  let lastDroneCap = null;
16196
+ let lastSwarmModels = null;
15940
16197
  let lastOwnCapacity = null;
15941
16198
  const hostCommandState = hostCommandStatePath(SWARM_HOME);
15942
16199
  let hostMaxDrones = loadMaxDronesOverride(hostCommandState);
@@ -15949,7 +16206,8 @@ async function runDaemon(opts) {
15949
16206
  executor: EXECUTOR,
15950
16207
  droneIsolation: DRONE_ISOLATION,
15951
16208
  channel: droneChannelLaunch,
15952
- droneCap: () => lastDroneCap
16209
+ droneCap: () => lastDroneCap,
16210
+ swarmModels: () => lastSwarmModels
15953
16211
  });
15954
16212
  let prepareReader;
15955
16213
  const zoneModels = new ZoneModels({ manualAreas: () => new Map(db.listRuns().map((r) => [r.task_slug, r.area])) });
@@ -16085,12 +16343,20 @@ async function runDaemon(opts) {
16085
16343
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
16086
16344
  const client = db;
16087
16345
  const readMaxDrones = makeMaxDronesReader({
16088
- fetch: () => client.ownerMaxDrones(),
16346
+ // TASWM-364: одним чтением берём и кап, и модели роя — это соседние поля ОДНОГО
16347
+ // ответа who_am_i. Модели складываем в снимок для спавна тем же побочным ходом,
16348
+ // что и кап ниже: второго сетевого канала за ними заводить незачем.
16349
+ fetch: async () => {
16350
+ const settings = await client.ownerSwarmSettings();
16351
+ if (settings) lastSwarmModels = settings.models;
16352
+ return settings?.maxDrones ?? null;
16353
+ },
16089
16354
  fallback: SWARM_MAX_IN_FLIGHT > 0 ? SWARM_MAX_IN_FLIGHT : 3,
16090
16355
  ttlMs: 1e4
16091
16356
  });
16092
16357
  maxDrones = async () => {
16093
- const own = lastOwnCapacity = hostMaxDrones ?? await readMaxDrones();
16358
+ const live = await readMaxDrones();
16359
+ const own = lastOwnCapacity = hostMaxDrones ?? live;
16094
16360
  return lastDroneCap = operatorMaxDrones != null ? Math.min(own, operatorMaxDrones) : own;
16095
16361
  };
16096
16362
  }
@@ -16287,7 +16553,9 @@ async function runDaemon(opts) {
16287
16553
  repoHasCi: (run) => repoHasCiWorkflows(run.worktree_path, run.repo || opts.repo),
16288
16554
  isAlive: pidAlive,
16289
16555
  // Учёт cost/usage (TASWM-188): реальный аккаунтант поверх RUN_LOGS_DIR + db.
16290
- accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR }),
16556
+ // TASWM-364: swarmModels — ТОТ ЖЕ снимок, что уехал в launcher: цена токенов считается
16557
+ // по модели, на которой прогон реально работал.
16558
+ accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR, swarmModels: () => lastSwarmModels }),
16291
16559
  // Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
16292
16560
  // (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
16293
16561
  exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
@@ -16824,6 +17092,7 @@ export {
16824
17092
  REVIEW_CYCLE_ESCALATION_MARK,
16825
17093
  REVIEW_CYCLE_LIMIT,
16826
17094
  REVIEW_ESCALATION_MARK,
17095
+ REVIEW_NO_VERDICT_REASON,
16827
17096
  REVIEW_PR_UNKNOWN_MARK,
16828
17097
  REVIEW_SPAWN_LIMIT,
16829
17098
  REVIEW_UNRECOVERABLE_MARK,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
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": "298573d4b361b8fc7ea4d6ea9d1e8784906bbd7e",
41
+ "gitHead": "76ff5edd816a2b9dd7f10db62c8fde89eec7771f",
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",