@taskless-app/swarm 0.6.3 → 0.6.4

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 +122 -26
  2. package/dist/overmind.js +255 -74
  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.4",
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: "12ecebcf1e60baab07e68cae3e78a29d78cb7fde"
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
  /**
@@ -7642,8 +7738,8 @@ var GK = ['claw','spore','eye','hex'];
7642
7738
  var ST = ${jsonForScript(statusTuples())};
7643
7739
  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
7740
  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' };
7741
+ 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' };
7742
+ 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
7743
 
7648
7744
  function E(s){ return String(s==null?"":s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]}); }
7649
7745
  function color(st){ return 'var('+(ST[st]||ST.queued)[2]+')'; }
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();
@@ -4868,18 +4914,40 @@ CREATE TABLE IF NOT EXISTS imports (
4868
4914
  return rows.map((r) => r.area);
4869
4915
  }
4870
4916
  /**
4871
- * Из depends_on прогона — слаги зависимостей, ещё НЕ закрытых (нет прогона со
4872
- * статусом done). Пусто → все выполнены, дрона можно спавнить. Явная зависимость
4873
- * (spawn --after) отдельна от area-lock: area освобождается на review (до мержа),
4874
- * а тут ждём именно done (смерж), чтобы следующий дрон стартовал от кода предыдущего.
4875
- * Несуществующий/зациклённый слаг просто никогда не станет done → прогон вечно ждёт,
4876
- * но очередь не виснет (гейт — пропуск в цикле, не блокировка).
4917
+ * Из depends_on прогона — слаги зависимостей, ещё НЕ исчерпанных. Пусто → все выполнены,
4918
+ * дрона можно спавнить. Явная зависимость (spawn --after) отдельна от area-lock: area
4919
+ * освобождается на review (до мержа), а тут ждём финала блокера, чтобы следующий дрон
4920
+ * стартовал от его кода.
4921
+ *
4922
+ * TASWM-361: критерий — «живых прогонов по слагу не осталось», а не «есть done». Раньше
4923
+ * зависимость снимал ТОЛЬКО успех, и остановленный оператором блокер держал ждущих вечно
4924
+ * (VESPA и TYPHON — сутки очереди при пустом рое). Само правило и его обоснование —
4925
+ * `judgeDep` в swarm-core/deps.ts (его же читают зеркало прода и прод-снапшот).
4926
+ *
4927
+ * Несуществующий/зациклённый слаг прогонов не имеет вовсе → `pending` → прогон ждёт, как и
4928
+ * раньше, но очередь не виснет (гейт — пропуск в цикле, не блокировка).
4877
4929
  */
4878
4930
  unmetDeps(depends_on) {
4931
+ return this.depVerdicts(depends_on).filter((d) => !d.met).map((d) => d.slug);
4932
+ }
4933
+ /**
4934
+ * TASWM-361: вердикт по каждой зависимости — не только «держит ли», но и КАК закрылась
4935
+ * (успехом блокера или его неуспешным финалом). Разница нужна overmind: снятие по
4936
+ * неуспеху оставляет след в ленге ждавшего прогона, штатное — нет.
4937
+ */
4938
+ depVerdicts(depends_on) {
4879
4939
  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));
4940
+ const slugs = [...new Set(depends_on)];
4941
+ const rows = this.db.prepare(
4942
+ `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`
4943
+ ).all(...slugs);
4944
+ const bySlug = /* @__PURE__ */ new Map();
4945
+ for (const r of rows) {
4946
+ const list = bySlug.get(r.task_slug) ?? [];
4947
+ list.push({ status: r.status, name: r.name, stopReason: r.stop_reason });
4948
+ bySlug.set(r.task_slug, list);
4949
+ }
4950
+ return depends_on.map((slug) => ({ slug, ...judgeDep(bySlug.get(slug) ?? []) }));
4883
4951
  }
4884
4952
  /** Все репо, засветившиеся в прогонах — whitelist для валидации spawn из веба. */
4885
4953
  distinctRepos() {
@@ -5568,10 +5636,13 @@ var SwarmClient = class _SwarmClient {
5568
5636
  // редко — без этого лог был бы простынёй из одной и той же строки, и в ней потонуло бы
5569
5637
  // всё остальное. Меняется набор (эпик закрыли / блокер прошли) → печатаем снова.
5570
5638
  lastClaimSkipLog = null;
5571
- // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
5572
- // Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
5573
- // переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
5574
- depsDoneInProd = /* @__PURE__ */ new Set();
5639
+ // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → вердикт по ВСЕМ его прогонам.
5640
+ // `done` монотонен (не откатывается) → положительный ответ не протухает и больше не
5641
+ // спрашивается; прочие вердикты переспрашиваются по TTL — «исчерпана неуспешно»
5642
+ // (TASWM-361) монотонностью НЕ обладает: оператор вправе переигрть остановленный блокер
5643
+ // новым прогоном того же слага, и тогда зависимость снова становится ожиданием.
5644
+ // Живёт в памяти: рестарт демона просто переспросит прод.
5645
+ depsProd = /* @__PURE__ */ new Map();
5575
5646
  depsAskedAt = /* @__PURE__ */ new Map();
5576
5647
  static DEPS_TTL_MS = 6e4;
5577
5648
  // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
@@ -5998,12 +6069,36 @@ var SwarmClient = class _SwarmClient {
5998
6069
  return [...seen];
5999
6070
  }
6000
6071
  unmetDeps(depends_on) {
6072
+ return this.depVerdicts(depends_on).filter((d) => !d.met).map((d) => d.slug);
6073
+ }
6074
+ /**
6075
+ * TASWM-361: вердикт по каждой зависимости — см. докблок SwarmStore.depVerdicts.
6076
+ *
6077
+ * АСИММЕТРИЯ ЗЕРКАЛА И ПРОДА, и почему она здесь намеренная. Зеркало — ЧАСТИЧНАЯ выборка
6078
+ * (только прогоны этой машины, только окно гидрации), и по частичной выборке два вывода
6079
+ * стоят разного:
6080
+ * • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он не
6081
+ * портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
6082
+ * • «живых прогонов слага не осталось» — вывод от ОТСУТСТВИЯ: невидимый зеркалу прогон
6083
+ * на соседней машине как раз и может быть живым. Поэтому «исчерпана неуспешно»
6084
+ * принимаем ТОЛЬКО из ответа прода (refreshDeps) — он видит всех.
6085
+ * Прод недоступен → вердикта нет → прогон честно ждёт следующего окна: лишняя минута
6086
+ * очереди дешевле дрона, поднятого поверх работающего блокера.
6087
+ */
6088
+ depVerdicts(depends_on) {
6001
6089
  if (!depends_on || depends_on.length === 0) return [];
6002
- const done = new Set(this.depsDoneInProd);
6090
+ const doneInMirror = /* @__PURE__ */ new Set();
6003
6091
  for (const r of this.runs.values()) {
6004
- if (r.status === "done" && r.resolves_run_id == null) done.add(r.task_slug);
6092
+ if (r.status === "done" && r.resolves_run_id == null && r.reviews_run_id == null) doneInMirror.add(r.task_slug);
6005
6093
  }
6006
- return depends_on.filter((slug) => !done.has(slug));
6094
+ return depends_on.map((slug) => {
6095
+ if (doneInMirror.has(slug)) {
6096
+ return { slug, outcome: "done", met: true, status: "done", name: null, stopReason: null };
6097
+ }
6098
+ const fromProd = this.depsProd.get(slug);
6099
+ if (fromProd) return { slug, ...fromProd };
6100
+ return { slug, outcome: "pending", met: false, status: null, name: null, stopReason: null };
6101
+ });
6007
6102
  }
6008
6103
  /**
6009
6104
  * TASWM-300: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
@@ -6014,14 +6109,15 @@ var SwarmClient = class _SwarmClient {
6014
6109
  * просто позавчера, в зеркало не попадала — и `--after` держал прогон в очереди вечно
6015
6110
  * (04.08: снимали руками).
6016
6111
  *
6017
- * Кеш. «Выполнено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
6018
- * положительный ответ помним навсегда и больше не спрашиваем. Отрицательный переспрашиваем
6112
+ * Кеш. «Смержено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
6113
+ * положительный ответ помним навсегда и больше не спрашиваем. Остальные вердикты (включая
6114
+ * «исчерпана неуспешно», TASWM-361 — блокер могут переиграть новым прогоном) переспрашиваем
6019
6115
  * не чаще DEPS_TTL_MS: тик идёт раз в ~4с, а ждущих зависимость прогонов может быть
6020
6116
  * много — без TTL это был бы поход в прод на каждый слаг каждый тик.
6021
6117
  */
6022
6118
  async refreshDeps(slugs, nowMs = Date.now()) {
6023
6119
  for (const slug of new Set(slugs)) {
6024
- if (!slug || this.depsDoneInProd.has(slug)) continue;
6120
+ if (!slug || this.depsProd.get(slug)?.outcome === "done") continue;
6025
6121
  const asked = this.depsAskedAt.get(slug);
6026
6122
  if (asked !== void 0 && nowMs - asked < _SwarmClient.DEPS_TTL_MS) continue;
6027
6123
  this.depsAskedAt.set(slug, nowMs);
@@ -6034,7 +6130,18 @@ var SwarmClient = class _SwarmClient {
6034
6130
  }
6035
6131
  if (res?.isError) continue;
6036
6132
  const runs = res.structuredContent?.runs ?? [];
6037
- if (runs.some((r) => r.status === "done" && !r.resolvesRunId)) this.depsDoneInProd.add(slug);
6133
+ const facts = runs.slice().sort((a, b) => String(a.createdAt ?? "").localeCompare(String(b.createdAt ?? ""))).map((r) => ({
6134
+ status: String(r.status ?? ""),
6135
+ // Служебный сабран под тем же слагом (резолвер конфликта / ревьюер): его финал —
6136
+ // не финал задачи. Резолвер вычёркивался и до TASWM-361, ревьюер добавлен вместе
6137
+ // с расширением критерия — иначе остановленный ревьюер закрывал бы зависимость.
6138
+ service: Boolean(r.resolvesRunId) || Boolean(r.reviewsRunId),
6139
+ // В проде имя дрона — колонка droneName (см. hydrateRun); `name` терпим как
6140
+ // запасной вариант ради фикстур и старых ответов.
6141
+ name: r.droneName ?? r.name ?? null,
6142
+ stopReason: r.stopReason ?? null
6143
+ }));
6144
+ this.depsProd.set(slug, judgeDep(facts));
6038
6145
  }
6039
6146
  }
6040
6147
  /**
@@ -9480,13 +9587,31 @@ ${notFoundText}` : notFoundText }],
9480
9587
  isError: true
9481
9588
  };
9482
9589
  }
9590
+ const reviewerBySpawn = opts.reviewsRunId != null || opts.runKind === "reviewer";
9591
+ const iAmReviewer = run?.reviews_run_id != null || run?.kind === "reviewer" || reviewerBySpawn;
9592
+ if (iAmReviewer && run?.reviews_run_id == null) {
9593
+ return {
9594
+ content: [
9595
+ {
9596
+ type: "text",
9597
+ 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.'
9598
+ }
9599
+ ],
9600
+ isError: true
9601
+ };
9602
+ }
9483
9603
  if (run?.reviews_run_id != null) {
9484
9604
  if (isTerminalStatus(run.status)) {
9485
9605
  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
9606
  }
9487
9607
  if (verdict !== "approve" && verdict !== "changes") {
9488
9608
  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).' }],
9609
+ content: [
9610
+ {
9611
+ type: "text",
9612
+ 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.'
9613
+ }
9614
+ ],
9490
9615
  isError: true
9491
9616
  };
9492
9617
  }
@@ -9496,28 +9621,39 @@ ${notFoundText}` : notFoundText }],
9496
9621
  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
9622
  const reviewedSlug = reportSlug(reviewed?.task_slug);
9498
9623
  let shots = { attachments: [], warning: "" };
9624
+ let commentWarning = "";
9499
9625
  if (opts.taskless && reviewedSlug) {
9500
9626
  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
- });
9627
+ try {
9628
+ await opts.taskless.addComment(
9629
+ reviewedSlug,
9630
+ `${(summary ?? "").trim()}${shots.warning}`,
9631
+ {
9632
+ swarmKind: "review",
9633
+ swarmDroneName: run.name,
9634
+ swarmVerdict: verdict,
9635
+ // TASWM-304: круг — ПРОВЕРЯЕМОГО прогона (у ревьюер-прогона свой
9636
+ // review_cycle всегда 0), иначе вердикты всех кругов схлопнулись
9637
+ // бы в одну плашку.
9638
+ swarmReviewCycle: reviewed?.review_cycle ?? 0
9639
+ },
9640
+ shots.attachments
9641
+ );
9642
+ } catch (e) {
9643
+ commentWarning = e instanceof Error ? e.message : String(e);
9644
+ 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}`);
9645
+ }
9646
+ } else {
9647
+ 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";
9648
+ 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
9649
  }
9517
9650
  const trustedPrUrl = reviewed?.pr_url ?? pr_url;
9518
9651
  await store.updateRun(null, { status: "review", pr_url: trustedPrUrl });
9519
9652
  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)}` }] };
9653
+ const commentNote = commentWarning ? `
9654
+
9655
+ \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"\`.` : "";
9656
+ 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
9657
  }
9522
9658
  if (isGithubPrUrl(pr_url)) {
9523
9659
  const report = (summary ?? "").trim();
@@ -9739,7 +9875,14 @@ async function runDroneServer(runId) {
9739
9875
  );
9740
9876
  const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
9741
9877
  const expectedKind = toRunKind(process.env.SWARM_RUN_KIND);
9742
- const common = { taskless, runKind: expectedKind, prHeadBranch, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
9878
+ const spawnReviewsRunId = Number(process.env.SWARM_REVIEWS_RUN_ID);
9879
+ const common = {
9880
+ taskless,
9881
+ runKind: expectedKind,
9882
+ prHeadBranch,
9883
+ ...expectedTaskSlug ? { expectedTaskSlug } : {},
9884
+ ...Number.isInteger(spawnReviewsRunId) && spawnReviewsRunId > 0 ? { reviewsRunId: spawnReviewsRunId } : {}
9885
+ };
9743
9886
  const channelUrl = process.env.SWARM_DRONE_CHANNEL_URL;
9744
9887
  const channelToken = process.env.SWARM_DRONE_CHANNEL_TOKEN;
9745
9888
  if (channelUrl && channelToken) {
@@ -10894,7 +11037,7 @@ async function changedPaths(worktree, opts = {}) {
10894
11037
  // package.json
10895
11038
  var package_default = {
10896
11039
  name: "@taskless-app/swarm",
10897
- version: "0.6.3",
11040
+ version: "0.6.4",
10898
11041
  type: "module",
10899
11042
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
10900
11043
  keywords: [
@@ -10944,7 +11087,7 @@ var package_default = {
10944
11087
  execa: "^10.0.0",
10945
11088
  zod: "3.25.76"
10946
11089
  },
10947
- gitHead: "298573d4b361b8fc7ea4d6ea9d1e8784906bbd7e"
11090
+ gitHead: "12ecebcf1e60baab07e68cae3e78a29d78cb7fde"
10948
11091
  };
10949
11092
 
10950
11093
  // src/version.ts
@@ -11671,8 +11814,8 @@ var GK = ['claw','spore','eye','hex'];
11671
11814
  var ST = ${jsonForScript(statusTuples())};
11672
11815
  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
11816
  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' };
11817
+ 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' };
11818
+ 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
11819
 
11677
11820
  function E(s){ return String(s==null?"":s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]}); }
11678
11821
  function color(st){ return 'var('+(ST[st]||ST.queued)[2]+')'; }
@@ -13434,6 +13577,7 @@ var NEEDS_HUMAN_MARKS = [
13434
13577
  CHANGES_WAKE_ESCALATION_MARK
13435
13578
  ];
13436
13579
  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";
13580
+ 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
13581
  function verdictCommentMark(createdAt) {
13438
13582
  return `\u043A\u043E\u043C\u043C\u0435\u043D\u0442-\u0432\u0435\u0440\u0434\u0438\u043A\u0442@${createdAt}`;
13439
13583
  }
@@ -13764,7 +13908,7 @@ function buildCiReadyNudge(prUrl, over) {
13764
13908
  head,
13765
13909
  "",
13766
13910
  "\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.`,
13911
+ `\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
13912
  "\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
13913
  ].join("\n");
13770
13914
  }
@@ -13989,7 +14133,8 @@ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachments
13989
14133
  " \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
14134
  `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
14135
  ],
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.`
14136
+ `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.`,
14137
+ '\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
14138
  ].join("\n");
13994
14139
  }
13995
14140
  function droneLabel(run) {
@@ -14548,6 +14693,19 @@ async function fillSuggestedArea(deps, run) {
14548
14693
  `\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
14694
  );
14550
14695
  }
14696
+ function noteExhaustedDeps(deps, run, verdicts) {
14697
+ const exhausted = verdicts.filter((d) => d.outcome === "exhausted");
14698
+ if (exhausted.length === 0) return;
14699
+ const already = new Set(
14700
+ deps.db.events(run.id).filter((e) => e.kind === "dep_cleared").map((e) => e.text ?? "")
14701
+ );
14702
+ for (const d of exhausted) {
14703
+ const text = depClearedText(d.slug, d);
14704
+ if (already.has(text)) continue;
14705
+ deps.db.addEvent(run.id, "dep_cleared", text);
14706
+ narrate(deps, `${droneLabel(run)} \u2014 ${text}`);
14707
+ }
14708
+ }
14551
14709
  async function trySpawnQueuedRun(deps, run) {
14552
14710
  if (deps.control?.paused) return "skipped";
14553
14711
  const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
@@ -14558,11 +14716,13 @@ async function trySpawnQueuedRun(deps, run) {
14558
14716
  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
14717
  return "closed";
14560
14718
  }
14561
- if (deps.db.unmetDeps(run.depends_on).length > 0) return "skipped";
14719
+ const depVerdicts = deps.db.depVerdicts(run.depends_on);
14720
+ if (depVerdicts.some((d) => !d.met)) return "skipped";
14562
14721
  if (!await envGateAllows(deps, run)) return "skipped";
14563
14722
  if (!await projectQuotaGateAllows(deps, run)) return "skipped";
14564
14723
  if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
14565
14724
  if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
14725
+ noteExhaustedDeps(deps, run, depVerdicts);
14566
14726
  await fillSuggestedArea(deps, run);
14567
14727
  try {
14568
14728
  await deps.spawn(run);
@@ -14927,6 +15087,28 @@ async function postScrapedVerdictReport(deps, runId, taskSlug, verdict, droneNam
14927
15087
  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
15088
  });
14929
15089
  }
15090
+ async function salvageVerdictFromPrComment(deps, rv, reviewsRunId, since, ctx) {
15091
+ if (!deps.verdictComment || !rv.pr_url) return false;
15092
+ const found = await deps.verdictComment(rv.pr_url, since).catch(() => null);
15093
+ if (!found) return false;
15094
+ const verdictText = verdictLabel(found.verdict);
15095
+ deps.db.updateRun(reviewsRunId, { review_verdict: found.verdict });
15096
+ 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}`);
15097
+ await closeReviewerRun(
15098
+ deps,
15099
+ rv,
15100
+ `\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)`
15101
+ );
15102
+ 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.`);
15103
+ const reviewed = deps.db.getRun(reviewsRunId);
15104
+ if (reviewed) {
15105
+ 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);
15106
+ if (found.verdict === "changes" && !ctx.stormActive && !deps.db.activeResolverFor(reviewed.id)) {
15107
+ await wakeAuthorOnChanges(deps, reviewed, ctx.now, ctx.droneCap);
15108
+ }
15109
+ }
15110
+ return true;
15111
+ }
14930
15112
  function buildEscalationCandidates(db) {
14931
15113
  const candidates = [];
14932
15114
  for (const run of db.listRuns("review")) {
@@ -15086,6 +15268,24 @@ async function tickOnce(deps) {
15086
15268
  continue;
15087
15269
  }
15088
15270
  if (run.reviews_run_id != null) {
15271
+ const reviewed = deps.db.getRun(run.reviews_run_id);
15272
+ const sameCycle = reviewed != null && (reviewed.review_cycle ?? 0) === (run.review_cycle ?? 0);
15273
+ if (reviewed && sameCycle && reviewed.review_verdict == null) {
15274
+ const salvaged = await salvageVerdictFromPrComment(deps, run, run.reviews_run_id, run.created_at, {
15275
+ 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",
15276
+ now,
15277
+ droneCap,
15278
+ stormActive
15279
+ });
15280
+ if (salvaged) continue;
15281
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
15282
+ });
15283
+ deps.db.updateRun(run.id, { status: "failed", stop_reason: REVIEW_NO_VERDICT_REASON });
15284
+ deps.db.addEvent(run.id, "failed", REVIEW_NO_VERDICT_REASON);
15285
+ 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`);
15286
+ 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.`);
15287
+ continue;
15288
+ }
15089
15289
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
15090
15290
  });
15091
15291
  deps.db.updateRun(run.id, { status: "done" });
@@ -15359,33 +15559,13 @@ async function tickOnce(deps) {
15359
15559
  }
15360
15560
  }
15361
15561
  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
- }
15562
+ const salvaged = await salvageVerdictFromPrComment(deps, run, run.reviews_run_id, run.created_at, {
15563
+ how: "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0432\u044B\u0448\u0435\u043B, \u043D\u0435 \u043F\u043E\u0437\u0432\u0430\u0432 done()",
15564
+ now,
15565
+ droneCap,
15566
+ stormActive
15567
+ });
15568
+ if (salvaged) continue;
15389
15569
  }
15390
15570
  if (!alive && run.status === "running" && run.reviews_run_id != null && run.pr_url && deps.ci) {
15391
15571
  const parent = deps.db.getRun(run.reviews_run_id);
@@ -16824,6 +17004,7 @@ export {
16824
17004
  REVIEW_CYCLE_ESCALATION_MARK,
16825
17005
  REVIEW_CYCLE_LIMIT,
16826
17006
  REVIEW_ESCALATION_MARK,
17007
+ REVIEW_NO_VERDICT_REASON,
16827
17008
  REVIEW_PR_UNKNOWN_MARK,
16828
17009
  REVIEW_SPAWN_LIMIT,
16829
17010
  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.4",
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": "12ecebcf1e60baab07e68cae3e78a29d78cb7fde",
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",