@taskless-app/swarm 0.6.2 → 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.
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();
@@ -1325,6 +1371,54 @@ var init_claudeBin = __esm({
1325
1371
  }
1326
1372
  });
1327
1373
 
1374
+ // src/swarmFiles.ts
1375
+ function sq(s) {
1376
+ return `'${s.replace(/'/g, `'\\''`)}'`;
1377
+ }
1378
+ function guardHookScript(hook, delegate) {
1379
+ const paths = SWARM_WORKTREE_ARTIFACTS.map(sq).join(" ");
1380
+ const lines = [
1381
+ "#!/usr/bin/env sh",
1382
+ `# \u0421\u0442\u0440\u0430\u0445\u043E\u0432\u043A\u0430 \u0440\u043E\u044F (TASWM-372): \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B \u0440\u043E\u044F \u043D\u0435 \u0443\u0435\u0437\u0436\u0430\u044E\u0442 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.`,
1383
+ "# \u0425\u0443\u043A \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u041F\u0415\u0420-WORKTREE (git config --worktree core.hooksPath) \u043F\u0440\u0438 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0435",
1384
+ "# worktree \u0434\u0440\u043E\u043D\u0430; \u0445\u0443\u043A\u0438 \u0441\u0430\u043C\u043E\u0433\u043E \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u043D\u0435 \u0442\u0440\u043E\u043D\u0443\u0442\u044B \u2014 \u043F\u0440\u0435\u0436\u043D\u0438\u0439 \u0432\u044B\u0437\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u043D\u0438\u0436\u0435.",
1385
+ `swarm_found=$(git ls-files -- ${paths} 2>/dev/null)`,
1386
+ `if [ -n "$swarm_found" ]; then`,
1387
+ ` echo "\u2716 \u0421\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B \u0420\u041E\u042F \u0432 \u0438\u043D\u0434\u0435\u043A\u0441\u0435 git \u2014 ${ACTION[hook]} (TASWM-372):" >&2`,
1388
+ ` printf '%s\\n' "$swarm_found" | sed 's/^/ /' >&2`,
1389
+ ` echo " \u042D\u0442\u043E \u0444\u0430\u0439\u043B\u044B \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0440\u043E\u044F, \u0430 \u043D\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430: \u0432 ${SWARM_MCP_CONFIG_FILE} \u043B\u0435\u0436\u0438\u0442 \u0442\u043E\u043A\u0435\u043D," >&2`,
1390
+ ` echo " \u043A\u043E\u0442\u043E\u0440\u043E\u043C\u0443 \u043D\u0435\u043B\u044C\u0437\u044F \u043E\u043A\u0430\u0437\u0430\u0442\u044C\u0441\u044F \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 (\u0438\u043D\u0446\u0438\u0434\u0435\u043D\u0442 11.08)." >&2`,
1391
+ ` echo " \u0423\u0431\u0435\u0440\u0438 \u0438\u0445 \u0438\u0437 \u0438\u043D\u0434\u0435\u043A\u0441\u0430 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438:" >&2`,
1392
+ ` echo " git rm --cached -r --ignore-unmatch ${SWARM_WORKTREE_ARTIFACTS.join(" ")}" >&2`,
1393
+ ` echo " \u0424\u0430\u0439\u043B\u044B \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 \u043E\u0441\u0442\u0430\u043D\u0443\u0442\u0441\u044F \u2014 \u043E\u043D\u0438 \u043D\u0443\u0436\u043D\u044B \u043F\u0440\u043E\u0433\u043E\u043D\u0443." >&2`,
1394
+ " exit 1",
1395
+ "fi"
1396
+ ];
1397
+ if (delegate) {
1398
+ lines.push(
1399
+ "# \u041F\u0440\u0435\u0436\u043D\u0438\u0439 \u0445\u0443\u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (husky \u0438 \u0442.\u043F.) \u2014 \u043E\u043D \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u043F\u0440\u043E\u043F\u0430\u0441\u0442\u044C \u0438\u0437-\u0437\u0430 \u043D\u0430\u0448\u0435\u0439 \u0441\u0442\u0440\u0430\u0445\u043E\u0432\u043A\u0438.",
1400
+ `if [ -x ${sq(delegate)} ]; then exec ${sq(delegate)} "$@"; fi`
1401
+ );
1402
+ }
1403
+ lines.push("exit 0", "");
1404
+ return lines.join("\n");
1405
+ }
1406
+ var SWARM_MCP_CONFIG_FILE, SWARM_TASK_DIR, SWARM_WORKTREE_ARTIFACTS, SWARM_EXCLUDE_PATTERNS, GUARD_HOOKS, ACTION;
1407
+ var init_swarmFiles = __esm({
1408
+ "src/swarmFiles.ts"() {
1409
+ "use strict";
1410
+ SWARM_MCP_CONFIG_FILE = ".swarm-mcp.json";
1411
+ SWARM_TASK_DIR = ".swarm-task";
1412
+ SWARM_WORKTREE_ARTIFACTS = [SWARM_MCP_CONFIG_FILE, SWARM_TASK_DIR];
1413
+ SWARM_EXCLUDE_PATTERNS = [`/${SWARM_MCP_CONFIG_FILE}`, `/${SWARM_TASK_DIR}/`];
1414
+ GUARD_HOOKS = ["pre-commit", "pre-push"];
1415
+ ACTION = {
1416
+ "pre-commit": "\u043A\u043E\u043C\u043C\u0438\u0442 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D",
1417
+ "pre-push": "\u043F\u0443\u0448 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D"
1418
+ };
1419
+ }
1420
+ });
1421
+
1328
1422
  // src/pricing.ts
1329
1423
  function costFromUsage(usage, model, table = MODEL_PRICING) {
1330
1424
  const pricing = model ? table[model] : void 0;
@@ -1463,9 +1557,10 @@ var init_claudeCode = __esm({
1463
1557
  "src/executors/claudeCode.ts"() {
1464
1558
  "use strict";
1465
1559
  init_claudeBin();
1560
+ init_swarmFiles();
1466
1561
  init_claudeResult();
1467
1562
  ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
1468
- MCP_CONFIG_FILE = ".swarm-mcp.json";
1563
+ MCP_CONFIG_FILE = SWARM_MCP_CONFIG_FILE;
1469
1564
  claudeCodeExecutor = {
1470
1565
  id: "claude-code",
1471
1566
  title: "Claude Code (claude)",
@@ -2647,8 +2742,8 @@ var init_procCleanup = __esm({
2647
2742
 
2648
2743
  // src/git.ts
2649
2744
  import { existsSync as existsSync9, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
2650
- import { appendFile, mkdir, readFile, rm } from "fs/promises";
2651
- import { isAbsolute as isAbsolute2, join as join15 } from "path";
2745
+ import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
2746
+ import { isAbsolute as isAbsolute2, join as join15, sep as sep2 } from "path";
2652
2747
  import { $ as $2 } from "execa";
2653
2748
  function isUnrecoverableRefError(e) {
2654
2749
  if (e instanceof MissingRefError) return true;
@@ -2720,6 +2815,11 @@ async function worktreeForBranch(repo, branch) {
2720
2815
  return null;
2721
2816
  }
2722
2817
  async function addWorktree(repo, path, branch, opts = {}) {
2818
+ const res = await checkoutWorktree(repo, path, branch, opts);
2819
+ await ensureWorktreeHygiene(path);
2820
+ return res;
2821
+ }
2822
+ async function checkoutWorktree(repo, path, branch, opts) {
2723
2823
  await ensureLongPaths(repo);
2724
2824
  const localSha = await revParse(repo, `refs/heads/${branch}`);
2725
2825
  const originSha = await revParse(repo, `refs/remotes/origin/${branch}`);
@@ -2763,10 +2863,8 @@ async function setWorktreeIdentity(worktree, name) {
2763
2863
  }
2764
2864
  async function ensureGitExcluded(worktree, pattern) {
2765
2865
  try {
2766
- const out = await $2({ reject: false })`git -C ${worktree} rev-parse --git-common-dir`;
2767
- const raw = out.stdout.trim();
2768
- if (out.exitCode !== 0 || !raw) return false;
2769
- const commonDir = isAbsolute2(raw) ? raw : join15(worktree, raw);
2866
+ const commonDir = await gitPath(worktree, "--git-common-dir");
2867
+ if (!commonDir) return false;
2770
2868
  const infoDir = join15(commonDir, "info");
2771
2869
  const excludeFile = join15(infoDir, "exclude");
2772
2870
  let current = "";
@@ -2784,6 +2882,46 @@ async function ensureGitExcluded(worktree, pattern) {
2784
2882
  return false;
2785
2883
  }
2786
2884
  }
2885
+ async function gitPath(worktree, flag) {
2886
+ const out = await $2({ reject: false })`git -C ${worktree} rev-parse ${flag}`;
2887
+ const raw = out.stdout.trim();
2888
+ if (out.exitCode !== 0 || !raw) return null;
2889
+ return isAbsolute2(raw) ? raw : join15(worktree, raw);
2890
+ }
2891
+ async function ensureWorktreeHygiene(worktree) {
2892
+ for (const pattern of SWARM_EXCLUDE_PATTERNS) await ensureGitExcluded(worktree, pattern);
2893
+ await installGuardHooks(worktree);
2894
+ }
2895
+ async function installGuardHooks(worktree) {
2896
+ try {
2897
+ const gitDir = await gitPath(worktree, "--git-dir");
2898
+ if (!gitDir) return false;
2899
+ const hooksDir = join15(gitDir, "swarm-hooks");
2900
+ const prev = await previousHooksPath(worktree);
2901
+ await mkdir(hooksDir, { recursive: true });
2902
+ for (const hook of GUARD_HOOKS) {
2903
+ await writeFile(join15(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
2904
+ await chmod(join15(hooksDir, hook), 493);
2905
+ }
2906
+ await $2`git -C ${worktree} config extensions.worktreeConfig true`;
2907
+ await $2`git -C ${worktree} config --worktree core.hooksPath ${posix(hooksDir)}`;
2908
+ return true;
2909
+ } catch {
2910
+ return false;
2911
+ }
2912
+ }
2913
+ function posix(p) {
2914
+ return (sep2 === "\\" ? p.replace(/\\/g, "/") : p).replace(/\/+$/, "");
2915
+ }
2916
+ async function previousHooksPath(worktree) {
2917
+ for (const scope of ["--local", "--global", "--system"]) {
2918
+ const out = await $2({ reject: false })`git -C ${worktree} config ${scope} --get core.hooksPath`;
2919
+ const value = out.stdout.trim();
2920
+ if (out.exitCode === 0 && value) return value;
2921
+ }
2922
+ const commonDir = await gitPath(worktree, "--git-common-dir");
2923
+ return commonDir ? join15(commonDir, "hooks") : null;
2924
+ }
2787
2925
  async function removeWorktree(repo, path) {
2788
2926
  const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
2789
2927
  await cleanupWorktreeProcesses(path);
@@ -2848,6 +2986,7 @@ async function prepareDetachedWorktree(repo, branch, worktree) {
2848
2986
  const ref = `origin/${branch}`;
2849
2987
  if (!await revParse(repo, `${ref}^{commit}`)) throw new MissingRefError(ref, repo);
2850
2988
  await $2`git -C ${repo} worktree add --detach --force ${worktree} ${ref}`;
2989
+ await ensureWorktreeHygiene(worktree);
2851
2990
  }
2852
2991
  async function mergeRef(worktree, ref) {
2853
2992
  const before = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
@@ -2885,6 +3024,7 @@ var init_git = __esm({
2885
3024
  "src/git.ts"() {
2886
3025
  "use strict";
2887
3026
  init_procCleanup();
3027
+ init_swarmFiles();
2888
3028
  WorktreeBusyError = class extends Error {
2889
3029
  constructor(branch, worktreePath) {
2890
3030
  super(`worktree \u0437\u0430\u043D\u044F\u0442: ${worktreePath} (\u0432\u0435\u0442\u043A\u0430 ${branch}) \u2014 \u043F\u0440\u0438\u0431\u0435\u0440\u0438 worktree \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438 \u043F\u0440\u043E\u0433\u043E\u043D (swarm cancel ${branch})`);
@@ -3266,7 +3406,7 @@ var init_droneChannel = __esm({
3266
3406
  });
3267
3407
 
3268
3408
  // src/attachments.ts
3269
- import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
3409
+ import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3270
3410
  import { join as join18 } from "path";
3271
3411
  function attachmentsFromDoc(doc, origin) {
3272
3412
  const out = [];
@@ -3354,7 +3494,7 @@ async function materializeAttachments(opts) {
3354
3494
  const io = opts.io ?? {
3355
3495
  removeDir: async (dir2) => rm2(dir2, { recursive: true, force: true }),
3356
3496
  mkdir: async (dir2) => void await mkdir2(dir2, { recursive: true }),
3357
- writeFile: async (path, bytes) => writeFile(path, bytes)
3497
+ writeFile: async (path, bytes) => writeFile2(path, bytes)
3358
3498
  };
3359
3499
  const result = { files: [], skipped: [] };
3360
3500
  if (opts.refs.length === 0) return result;
@@ -3486,13 +3626,13 @@ function mb(bytes) {
3486
3626
  function errText(e) {
3487
3627
  return e instanceof Error ? e.message : String(e);
3488
3628
  }
3489
- var ATTACHMENTS_DIR, SWARM_TASK_DIR, ATTACH_MAX_FILES, ATTACH_MAX_TOTAL_BYTES, ATTACH_MAX_FILE_BYTES, IMAGE_EXT, DOC_EXT, TEXT_EXT, KNOWN_EXT, TRANSLIT, EXT_BY_MIME;
3629
+ var ATTACHMENTS_DIR, ATTACH_MAX_FILES, ATTACH_MAX_TOTAL_BYTES, ATTACH_MAX_FILE_BYTES, IMAGE_EXT, DOC_EXT, TEXT_EXT, KNOWN_EXT, TRANSLIT, EXT_BY_MIME;
3490
3630
  var init_attachments = __esm({
3491
3631
  "src/attachments.ts"() {
3492
3632
  "use strict";
3493
3633
  init_git();
3494
- ATTACHMENTS_DIR = ".swarm-task/attachments";
3495
- SWARM_TASK_DIR = ".swarm-task";
3634
+ init_swarmFiles();
3635
+ ATTACHMENTS_DIR = `${SWARM_TASK_DIR}/attachments`;
3496
3636
  ATTACH_MAX_FILES = 12;
3497
3637
  ATTACH_MAX_TOTAL_BYTES = 24 * 1024 * 1024;
3498
3638
  ATTACH_MAX_FILE_BYTES = 8 * 1024 * 1024;
@@ -4774,18 +4914,40 @@ CREATE TABLE IF NOT EXISTS imports (
4774
4914
  return rows.map((r) => r.area);
4775
4915
  }
4776
4916
  /**
4777
- * Из depends_on прогона — слаги зависимостей, ещё НЕ закрытых (нет прогона со
4778
- * статусом done). Пусто → все выполнены, дрона можно спавнить. Явная зависимость
4779
- * (spawn --after) отдельна от area-lock: area освобождается на review (до мержа),
4780
- * а тут ждём именно done (смерж), чтобы следующий дрон стартовал от кода предыдущего.
4781
- * Несуществующий/зациклённый слаг просто никогда не станет done → прогон вечно ждёт,
4782
- * но очередь не виснет (гейт — пропуск в цикле, не блокировка).
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
+ * раньше, но очередь не виснет (гейт — пропуск в цикле, не блокировка).
4783
4929
  */
4784
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) {
4785
4939
  if (!depends_on || depends_on.length === 0) return [];
4786
- const rows = this.db.prepare("SELECT DISTINCT task_slug FROM runs WHERE status = 'done' AND resolves_run_id IS NULL").all();
4787
- const done = new Set(rows.map((r) => r.task_slug));
4788
- 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) ?? []) }));
4789
4951
  }
4790
4952
  /** Все репо, засветившиеся в прогонах — whitelist для валидации spawn из веба. */
4791
4953
  distinctRepos() {
@@ -5230,11 +5392,11 @@ function savePauseState(home, state, io = {}) {
5230
5392
  remove(path);
5231
5393
  return;
5232
5394
  }
5233
- const writeFile2 = io.writeFile ?? ((p, data) => {
5395
+ const writeFile3 = io.writeFile ?? ((p, data) => {
5234
5396
  writeFileSync(`${p}.tmp`, data);
5235
5397
  renameSync(`${p}.tmp`, p);
5236
5398
  });
5237
- writeFile2(path, JSON.stringify(state, null, 2));
5399
+ writeFile3(path, JSON.stringify(state, null, 2));
5238
5400
  }
5239
5401
  function makePauseState(source, reason, now = Date.now()) {
5240
5402
  return {
@@ -5474,10 +5636,13 @@ var SwarmClient = class _SwarmClient {
5474
5636
  // редко — без этого лог был бы простынёй из одной и той же строки, и в ней потонуло бы
5475
5637
  // всё остальное. Меняется набор (эпик закрыли / блокер прошли) → печатаем снова.
5476
5638
  lastClaimSkipLog = null;
5477
- // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
5478
- // Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
5479
- // переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
5480
- depsDoneInProd = /* @__PURE__ */ new Set();
5639
+ // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → вердикт по ВСЕМ его прогонам.
5640
+ // `done` монотонен (не откатывается) → положительный ответ не протухает и больше не
5641
+ // спрашивается; прочие вердикты переспрашиваются по TTL — «исчерпана неуспешно»
5642
+ // (TASWM-361) монотонностью НЕ обладает: оператор вправе переигрть остановленный блокер
5643
+ // новым прогоном того же слага, и тогда зависимость снова становится ожиданием.
5644
+ // Живёт в памяти: рестарт демона просто переспросит прод.
5645
+ depsProd = /* @__PURE__ */ new Map();
5481
5646
  depsAskedAt = /* @__PURE__ */ new Map();
5482
5647
  static DEPS_TTL_MS = 6e4;
5483
5648
  // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
@@ -5904,12 +6069,36 @@ var SwarmClient = class _SwarmClient {
5904
6069
  return [...seen];
5905
6070
  }
5906
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) {
5907
6089
  if (!depends_on || depends_on.length === 0) return [];
5908
- const done = new Set(this.depsDoneInProd);
6090
+ const doneInMirror = /* @__PURE__ */ new Set();
5909
6091
  for (const r of this.runs.values()) {
5910
- 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);
5911
6093
  }
5912
- 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
+ });
5913
6102
  }
5914
6103
  /**
5915
6104
  * TASWM-300: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
@@ -5920,14 +6109,15 @@ var SwarmClient = class _SwarmClient {
5920
6109
  * просто позавчера, в зеркало не попадала — и `--after` держал прогон в очереди вечно
5921
6110
  * (04.08: снимали руками).
5922
6111
  *
5923
- * Кеш. «Выполнено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
5924
- * положительный ответ помним навсегда и больше не спрашиваем. Отрицательный переспрашиваем
6112
+ * Кеш. «Смержено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
6113
+ * положительный ответ помним навсегда и больше не спрашиваем. Остальные вердикты (включая
6114
+ * «исчерпана неуспешно», TASWM-361 — блокер могут переиграть новым прогоном) переспрашиваем
5925
6115
  * не чаще DEPS_TTL_MS: тик идёт раз в ~4с, а ждущих зависимость прогонов может быть
5926
6116
  * много — без TTL это был бы поход в прод на каждый слаг каждый тик.
5927
6117
  */
5928
6118
  async refreshDeps(slugs, nowMs = Date.now()) {
5929
6119
  for (const slug of new Set(slugs)) {
5930
- if (!slug || this.depsDoneInProd.has(slug)) continue;
6120
+ if (!slug || this.depsProd.get(slug)?.outcome === "done") continue;
5931
6121
  const asked = this.depsAskedAt.get(slug);
5932
6122
  if (asked !== void 0 && nowMs - asked < _SwarmClient.DEPS_TTL_MS) continue;
5933
6123
  this.depsAskedAt.set(slug, nowMs);
@@ -5940,7 +6130,18 @@ var SwarmClient = class _SwarmClient {
5940
6130
  }
5941
6131
  if (res?.isError) continue;
5942
6132
  const runs = res.structuredContent?.runs ?? [];
5943
- 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));
5944
6145
  }
5945
6146
  }
5946
6147
  /**
@@ -6595,6 +6796,26 @@ var SwarmClient = class _SwarmClient {
6595
6796
  const v = sc.config?.exclusive;
6596
6797
  return typeof v === "boolean" ? v : null;
6597
6798
  }
6799
+ /**
6800
+ * Live КВОТА «дронов на проект» из Taskless (TASWM-365): тот же swarm_repo_config по
6801
+ * repoFullName — Project.swarmConfig.maxDrones. Сколько прогонов этого проекта могут идти
6802
+ * одновременно; 0 — без ограничения (дефолт проекта). Отдаём ЧИСЛО КАК ЕСТЬ, включая ноль:
6803
+ * превращать его в «без ограничения» — дело ридера (makeDroneCapReader), клиент здесь только
6804
+ * достаёт настройку. null (прод недоступен / repo не смэплен / поля нет / мусор) → ридер
6805
+ * возьмёт «без ограничения», то есть прежнее поведение.
6806
+ */
6807
+ async repoDroneCap(repoFullName) {
6808
+ let res;
6809
+ try {
6810
+ res = await this.call("swarm_repo_config", { repoFullName });
6811
+ } catch {
6812
+ return null;
6813
+ }
6814
+ if (res?.isError) return null;
6815
+ const sc = res.structuredContent ?? {};
6816
+ const v = sc.config?.maxDrones;
6817
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
6818
+ }
6598
6819
  /**
6599
6820
  * TASWM-291: МАШИНЫ роя владельца (swarm_list_hosts) — чтобы `swarm spawn --host
6600
6821
  * <имя>` резолвил человеческое имя в hostId, а `swarm hosts` показывал, какие
@@ -7661,7 +7882,7 @@ function hostIdentityPath(home) {
7661
7882
  function loadHostIdentity(deps) {
7662
7883
  const exists = deps.exists ?? existsSync7;
7663
7884
  const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
7664
- const writeFile2 = deps.writeFile ?? ((p, data) => {
7885
+ const writeFile3 = deps.writeFile ?? ((p, data) => {
7665
7886
  writeFileSync8(`${p}.tmp`, data);
7666
7887
  renameSync6(`${p}.tmp`, p);
7667
7888
  });
@@ -7681,7 +7902,7 @@ function loadHostIdentity(deps) {
7681
7902
  }
7682
7903
  const hostId = newId();
7683
7904
  try {
7684
- writeFile2(path, JSON.stringify({ hostId, hostName, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
7905
+ writeFile3(path, JSON.stringify({ hostId, hostName, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
7685
7906
  } catch {
7686
7907
  console.warn(`[overmind] \u043D\u0435 \u0441\u043C\u043E\u0433 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C ${path} \u2014 hostId \u0436\u0438\u0432\u0451\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430`);
7687
7908
  }
@@ -9366,13 +9587,31 @@ ${notFoundText}` : notFoundText }],
9366
9587
  isError: true
9367
9588
  };
9368
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
+ }
9369
9603
  if (run?.reviews_run_id != null) {
9370
9604
  if (isTerminalStatus(run.status)) {
9371
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" }] };
9372
9606
  }
9373
9607
  if (verdict !== "approve" && verdict !== "changes") {
9374
9608
  return {
9375
- 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
+ ],
9376
9615
  isError: true
9377
9616
  };
9378
9617
  }
@@ -9382,28 +9621,39 @@ ${notFoundText}` : notFoundText }],
9382
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}` : ""}`);
9383
9622
  const reviewedSlug = reportSlug(reviewed?.task_slug);
9384
9623
  let shots = { attachments: [], warning: "" };
9624
+ let commentWarning = "";
9385
9625
  if (opts.taskless && reviewedSlug) {
9386
9626
  shots = await uploadShots();
9387
- await opts.taskless.addComment(
9388
- reviewedSlug,
9389
- `${(summary ?? "").trim()}${shots.warning}`,
9390
- {
9391
- swarmKind: "review",
9392
- swarmDroneName: run.name,
9393
- swarmVerdict: verdict,
9394
- // TASWM-304: круг — ПРОВЕРЯЕМОГО прогона (у ревьюер-прогона свой
9395
- // review_cycle всегда 0), иначе вердикты всех кругов схлопнулись
9396
- // бы в одну плашку.
9397
- swarmReviewCycle: reviewed?.review_cycle ?? 0
9398
- },
9399
- shots.attachments
9400
- ).catch(() => {
9401
- });
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}`);
9402
9649
  }
9403
9650
  const trustedPrUrl = reviewed?.pr_url ?? pr_url;
9404
9651
  await store.updateRun(null, { status: "review", pr_url: trustedPrUrl });
9405
9652
  await store.addEvent(null, "pr_opened", trustedPrUrl);
9406
- 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}` }] };
9407
9657
  }
9408
9658
  if (isGithubPrUrl(pr_url)) {
9409
9659
  const report = (summary ?? "").trim();
@@ -9625,7 +9875,14 @@ async function runDroneServer(runId) {
9625
9875
  );
9626
9876
  const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
9627
9877
  const expectedKind = toRunKind(process.env.SWARM_RUN_KIND);
9628
- 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
+ };
9629
9886
  const channelUrl = process.env.SWARM_DRONE_CHANNEL_URL;
9630
9887
  const channelToken = process.env.SWARM_DRONE_CHANNEL_TOKEN;
9631
9888
  if (channelUrl && channelToken) {
@@ -10327,6 +10584,20 @@ function makeExclusiveReader(opts) {
10327
10584
  ...opts.now !== void 0 ? { now: opts.now } : {}
10328
10585
  });
10329
10586
  }
10587
+ function makeDroneCapReader(opts) {
10588
+ const read = makeRepoValueReader({
10589
+ fetch: opts.fetch,
10590
+ fallback: 0,
10591
+ // «настройки нет» — ниже станет Infinity, как и записанный оператором 0
10592
+ valid: (v) => typeof v === "number" && Number.isFinite(v) && v >= 0,
10593
+ ...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {},
10594
+ ...opts.now !== void 0 ? { now: opts.now } : {}
10595
+ });
10596
+ return async (repo) => {
10597
+ const raw = await read(repo);
10598
+ return raw > 0 ? raw : Number.POSITIVE_INFINITY;
10599
+ };
10600
+ }
10330
10601
 
10331
10602
  // src/envGate.ts
10332
10603
  import { existsSync as existsSync13 } from "fs";
@@ -10391,9 +10662,9 @@ function parseRequirements(lines) {
10391
10662
  out.push({ kind: "docker", raw });
10392
10663
  continue;
10393
10664
  }
10394
- const sep2 = raw.indexOf(":");
10395
- const prefix = sep2 === -1 ? "" : lower.slice(0, sep2);
10396
- const rest = sep2 === -1 ? "" : raw.slice(sep2 + 1).trim();
10665
+ const sep3 = raw.indexOf(":");
10666
+ const prefix = sep3 === -1 ? "" : lower.slice(0, sep3);
10667
+ const rest = sep3 === -1 ? "" : raw.slice(sep3 + 1).trim();
10397
10668
  if (!rest) {
10398
10669
  out.push({ kind: "unknown", raw });
10399
10670
  continue;
@@ -10766,7 +11037,7 @@ async function changedPaths(worktree, opts = {}) {
10766
11037
  // package.json
10767
11038
  var package_default = {
10768
11039
  name: "@taskless-app/swarm",
10769
- version: "0.6.2",
11040
+ version: "0.6.4",
10770
11041
  type: "module",
10771
11042
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
10772
11043
  keywords: [
@@ -10816,7 +11087,7 @@ var package_default = {
10816
11087
  execa: "^10.0.0",
10817
11088
  zod: "3.25.76"
10818
11089
  },
10819
- gitHead: "e812f35a996b00111f3f535b6b0a45b379e7afb2"
11090
+ gitHead: "12ecebcf1e60baab07e68cae3e78a29d78cb7fde"
10820
11091
  };
10821
11092
 
10822
11093
  // src/version.ts
@@ -11543,8 +11814,8 @@ var GK = ['claw','spore','eye','hex'];
11543
11814
  var ST = ${jsonForScript(statusTuples())};
11544
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 };
11545
11816
  var TERM = ${jsonForScript(Object.fromEntries(TERMINAL_STATUSES.map((s) => [s, 1])))};
11546
- 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' };
11547
- 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' };
11548
11819
 
11549
11820
  function E(s){ return String(s==null?"":s).replace(/[&<>"]/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]}); }
11550
11821
  function color(st){ return 'var('+(ST[st]||ST.queued)[2]+')'; }
@@ -12828,11 +13099,11 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
12828
13099
  }
12829
13100
  function saveAnomalyState(home, state, io = {}) {
12830
13101
  const path = anomalyStatePath(home);
12831
- const writeFile2 = io.writeFile ?? ((p, data) => {
13102
+ const writeFile3 = io.writeFile ?? ((p, data) => {
12832
13103
  writeFileSync14(`${p}.tmp`, data);
12833
13104
  renameSync9(`${p}.tmp`, p);
12834
13105
  });
12835
- writeFile2(path, JSON.stringify(state, null, 2));
13106
+ writeFile3(path, JSON.stringify(state, null, 2));
12836
13107
  }
12837
13108
  var AnomalyStateStore = class {
12838
13109
  constructor(home, now = Date.now(), io = {}) {
@@ -13306,6 +13577,7 @@ var NEEDS_HUMAN_MARKS = [
13306
13577
  CHANGES_WAKE_ESCALATION_MARK
13307
13578
  ];
13308
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)";
13309
13581
  function verdictCommentMark(createdAt) {
13310
13582
  return `\u043A\u043E\u043C\u043C\u0435\u043D\u0442-\u0432\u0435\u0440\u0434\u0438\u043A\u0442@${createdAt}`;
13311
13583
  }
@@ -13636,7 +13908,7 @@ function buildCiReadyNudge(prUrl, over) {
13636
13908
  head,
13637
13909
  "",
13638
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.",
13639
- `\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.`,
13640
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."
13641
13913
  ].join("\n");
13642
13914
  }
@@ -13861,7 +14133,8 @@ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachments
13861
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).",
13862
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.`
13863
14135
  ],
13864
- `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.'
13865
14138
  ].join("\n");
13866
14139
  }
13867
14140
  function droneLabel(run) {
@@ -13958,7 +14231,7 @@ async function resurrectStormDead(deps, worktreeExists, hasTranscript, opts) {
13958
14231
  let resurrected = 0;
13959
14232
  for (const run of deps.db.listRuns("storm_dead")) {
13960
14233
  if (countActiveDrones(deps.db) >= cap) break;
13961
- if (!await exclusiveGateAllows(deps, run)) continue;
14234
+ if (!await projectQuotaGateAllows(deps, run)) continue;
13962
14235
  const wt = run.worktree_path;
13963
14236
  const mode = !!wt && worktreeExists(wt) && hasTranscript(wt) ? "continue" : "fresh";
13964
14237
  deps.db.addEvent(run.id, "resumed", mode === "continue" ? STORM_RESUME_CONTINUE_TEXT : STORM_RESUME_FRESH_TEXT);
@@ -14111,7 +14384,7 @@ async function wakeAuthorOnRedCi(deps, run, now, ci, worktreeExists, hasTranscri
14111
14384
  if (run.pid != null && deps.isAlive(run.pid)) return false;
14112
14385
  if (deps.resourcesOk && !deps.resourcesOk()) return false;
14113
14386
  if (countActiveDrones(deps.db) >= droneCap) return false;
14114
- if (!await exclusiveGateAllows(deps, run)) return false;
14387
+ if (!await projectQuotaGateAllows(deps, run)) return false;
14115
14388
  const stats = ciWakeStats(deps.db, run.id);
14116
14389
  if (stats.count >= CI_WAKE_LIMIT) {
14117
14390
  if (!stats.escalated) {
@@ -14242,7 +14515,7 @@ async function wakeAuthorOnChanges(deps, run, now, droneCap) {
14242
14515
  if (deps.db.activeReviewersFor(run.id).some((r) => r.status !== "stalled")) return false;
14243
14516
  if (deps.resourcesOk && !deps.resourcesOk()) return false;
14244
14517
  if (countActiveDrones(deps.db) >= droneCap) return false;
14245
- if (!await exclusiveGateAllows(deps, run)) return false;
14518
+ if (!await projectQuotaGateAllows(deps, run)) return false;
14246
14519
  if (run.review_cycle >= REVIEW_CYCLE_LIMIT) return false;
14247
14520
  const wakes = deps.db.events(run.id).filter((e) => e.kind === "resumed" && (e.text ?? "").includes(CHANGES_WAKE_MARK));
14248
14521
  if (wakes.length >= CHANGES_WAKE_LIMIT) {
@@ -14336,37 +14609,55 @@ function activeDronesOnRepo(deps, repo, exceptRunId) {
14336
14609
  (r) => r.id !== exceptRunId && (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status) && effectiveRepo(deps, r) === repo
14337
14610
  );
14338
14611
  }
14339
- async function exclusiveGateAllows(deps, run) {
14340
- if (!deps.exclusiveProject) return true;
14612
+ async function projectCapFor(deps, repo) {
14613
+ let exclusive = false;
14614
+ if (deps.exclusiveProject) {
14615
+ try {
14616
+ exclusive = await deps.exclusiveProject(repo);
14617
+ } catch (e) {
14618
+ console.error("[overmind] \u0447\u0442\u0435\u043D\u0438\u0435 \u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u043A\u0430\u043A \u0440\u0430\u043D\u044C\u0448\u0435:", e);
14619
+ }
14620
+ }
14621
+ if (exclusive) return { cap: 1, exclusive: true };
14622
+ let quota = Number.POSITIVE_INFINITY;
14623
+ if (deps.projectDroneCap) {
14624
+ try {
14625
+ const v = await deps.projectDroneCap(repo);
14626
+ if (typeof v === "number" && v > 0) quota = v;
14627
+ } catch (e) {
14628
+ console.error("[overmind] \u0447\u0442\u0435\u043D\u0438\u0435 \u043A\u0432\u043E\u0442\u044B \xAB\u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\xBB \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u043A\u0430\u043A \u0440\u0430\u043D\u044C\u0448\u0435:", e);
14629
+ }
14630
+ }
14631
+ return { cap: quota, exclusive: false };
14632
+ }
14633
+ async function projectQuotaGateAllows(deps, run) {
14634
+ if (!deps.exclusiveProject && !deps.projectDroneCap) return true;
14341
14635
  const repo = effectiveRepo(deps, run);
14342
14636
  if (!repo) return true;
14343
- let exclusive;
14344
- try {
14345
- exclusive = await deps.exclusiveProject(repo);
14346
- } catch (e) {
14347
- console.error("[overmind] \u0447\u0442\u0435\u043D\u0438\u0435 \u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u043A\u0430\u043A \u0440\u0430\u043D\u044C\u0448\u0435:", e);
14348
- return true;
14349
- }
14637
+ const { cap, exclusive } = await projectCapFor(deps, repo);
14350
14638
  const logged = deps.exclusiveLogged ??= /* @__PURE__ */ new Map();
14351
- if (exclusive) {
14639
+ if (Number.isFinite(cap)) {
14352
14640
  const busy = activeDronesOnRepo(deps, repo, run.id);
14353
- if (busy.length > 0) {
14641
+ if (busy.length >= cap) {
14354
14642
  const who = busy.map((r) => `${droneLabel(r)} (${r.task_slug})`).join(", ");
14355
- const reason = `\u043F\u0440\u043E\u0435\u043A\u0442 \u0433\u043E\u043D\u044F\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 (\u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B) \u0438 \u0441\u0435\u0439\u0447\u0430\u0441 \u0437\u0430\u043D\u044F\u0442: ${who}`;
14643
+ const reason = exclusive ? `\u043F\u0440\u043E\u0435\u043A\u0442 \u0433\u043E\u043D\u044F\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 (\u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B) \u0438 \u0441\u0435\u0439\u0447\u0430\u0441 \u0437\u0430\u043D\u044F\u0442: ${who}` : `\u043F\u0440\u043E\u0435\u043A\u0442\u0443 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u043E ${cap} \u0434\u0440\u043E\u043D(\u043E\u0432) \u043E\u0434\u043D\u043E\u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E (\xAB\u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\xBB), \u0438 \u0432\u0441\u0435 \u0437\u0430\u043D\u044F\u0442\u044B: ${who}`;
14356
14644
  if (logged.get(run.id) !== reason) {
14357
14645
  logged.set(run.id, reason);
14358
14646
  deps.db.addEvent(
14359
14647
  run.id,
14360
14648
  "step",
14361
- `${reason} \u2014 \u0436\u0434\u0443 \u043D\u0435 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u0443\u044E \u043C\u0430\u0448\u0438\u043D\u0443, \u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430; \u043F\u043E\u0434\u043D\u0438\u043C\u0443 \u0441\u0430\u043C, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u043E\u043D \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u0441\u044F (TASWM-314)`
14649
+ `${reason} \u2014 \u0436\u0434\u0443 \u043D\u0435 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u0443\u044E \u043C\u0430\u0448\u0438\u043D\u0443, \u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430; \u043F\u043E\u0434\u043D\u0438\u043C\u0443 \u0441\u0430\u043C, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u043E\u043D \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u0441\u044F (${exclusive ? "TASWM-314" : "TASWM-365"})`
14650
+ );
14651
+ narrate(
14652
+ deps,
14653
+ exclusive ? `${droneLabel(run)} \u2014 ${reason}. \u041D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E: \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043C\u0435\u0448\u0430\u044E\u0442 \u0434\u0440\u0443\u0433 \u0434\u0440\u0443\u0433\u0443.` : `${droneLabel(run)} \u2014 ${reason}. \u041D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E: \u043F\u0440\u043E\u0435\u043A\u0442 \u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0432\u043E\u044E \u043A\u0432\u043E\u0442\u0443 \u0434\u0440\u043E\u043D\u043E\u0432, \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043F\u043E\u0434\u043E\u0436\u0434\u0451\u0442.`
14362
14654
  );
14363
- narrate(deps, `${droneLabel(run)} \u2014 ${reason}. \u041D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E: \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043C\u0435\u0448\u0430\u044E\u0442 \u0434\u0440\u0443\u0433 \u0434\u0440\u0443\u0433\u0443.`);
14364
14655
  }
14365
14656
  return false;
14366
14657
  }
14367
14658
  }
14368
14659
  if (logged.delete(run.id)) {
14369
- deps.db.addEvent(run.id, "step", "\u043F\u0440\u043E\u0435\u043A\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u043B\u0441\u044F \u2014 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0430 \u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u043E\u0441\u0442\u0438 \u0441\u043D\u044F\u0442\u0430 (TASWM-314)");
14660
+ deps.db.addEvent(run.id, "step", "\u043F\u0440\u043E\u0435\u043A\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u043B\u0441\u044F \u2014 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u043E \u043A\u0432\u043E\u0442\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441\u043D\u044F\u0442\u0430 (TASWM-314/TASWM-365)");
14370
14661
  }
14371
14662
  return true;
14372
14663
  }
@@ -14379,19 +14670,14 @@ function pruneGateLogs(deps) {
14379
14670
  for (const id of [...map.keys()]) if (!open.has(id)) map.delete(id);
14380
14671
  }
14381
14672
  }
14382
- async function exclusivityBlockedRunIds(deps) {
14673
+ async function projectQuotaBlockedRunIds(deps) {
14383
14674
  const blocked = /* @__PURE__ */ new Set();
14384
- if (!deps.exclusiveProject) return blocked;
14675
+ if (!deps.exclusiveProject && !deps.projectDroneCap) return blocked;
14385
14676
  for (const run of deps.db.listRuns("queued")) {
14386
14677
  const repo = effectiveRepo(deps, run);
14387
14678
  if (!repo) continue;
14388
- let exclusive;
14389
- try {
14390
- exclusive = await deps.exclusiveProject(repo);
14391
- } catch {
14392
- continue;
14393
- }
14394
- if (exclusive && activeDronesOnRepo(deps, repo, run.id).length > 0) blocked.add(run.id);
14679
+ const { cap } = await projectCapFor(deps, repo);
14680
+ if (Number.isFinite(cap) && activeDronesOnRepo(deps, repo, run.id).length >= cap) blocked.add(run.id);
14395
14681
  }
14396
14682
  return blocked;
14397
14683
  }
@@ -14407,6 +14693,19 @@ async function fillSuggestedArea(deps, run) {
14407
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`
14408
14694
  );
14409
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
+ }
14410
14709
  async function trySpawnQueuedRun(deps, run) {
14411
14710
  if (deps.control?.paused) return "skipped";
14412
14711
  const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
@@ -14417,11 +14716,13 @@ async function trySpawnQueuedRun(deps, run) {
14417
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.`);
14418
14717
  return "closed";
14419
14718
  }
14420
- 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";
14421
14721
  if (!await envGateAllows(deps, run)) return "skipped";
14422
- if (!await exclusiveGateAllows(deps, run)) return "skipped";
14722
+ if (!await projectQuotaGateAllows(deps, run)) return "skipped";
14423
14723
  if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
14424
14724
  if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
14725
+ noteExhaustedDeps(deps, run, depVerdicts);
14425
14726
  await fillSuggestedArea(deps, run);
14426
14727
  try {
14427
14728
  await deps.spawn(run);
@@ -14456,7 +14757,7 @@ async function resumeWatchdogParked(deps, droneCap) {
14456
14757
  if (!parked.length) return;
14457
14758
  let candidate;
14458
14759
  for (const run of orderQueue(parked)) {
14459
- if (await exclusiveGateAllows(deps, run)) {
14760
+ if (await projectQuotaGateAllows(deps, run)) {
14460
14761
  candidate = run;
14461
14762
  break;
14462
14763
  }
@@ -14786,6 +15087,28 @@ async function postScrapedVerdictReport(deps, runId, taskSlug, verdict, droneNam
14786
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)}`);
14787
15088
  });
14788
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
+ }
14789
15112
  function buildEscalationCandidates(db) {
14790
15113
  const candidates = [];
14791
15114
  for (const run of db.listRuns("review")) {
@@ -14945,6 +15268,24 @@ async function tickOnce(deps) {
14945
15268
  continue;
14946
15269
  }
14947
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
+ }
14948
15289
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
14949
15290
  });
14950
15291
  deps.db.updateRun(run.id, { status: "done" });
@@ -14990,7 +15331,7 @@ async function tickOnce(deps) {
14990
15331
  if (st.conflicting && deps.resolveConflict && !stormActive) {
14991
15332
  if (deps.resourcesOk && !deps.resourcesOk()) continue;
14992
15333
  if (countActiveDrones(deps.db) >= droneCap) continue;
14993
- if (!await exclusiveGateAllows(deps, run)) continue;
15334
+ if (!await projectQuotaGateAllows(deps, run)) continue;
14994
15335
  if (deps.db.activeResolverFor(run.id)) continue;
14995
15336
  if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) {
14996
15337
  if (!deps.db.events(run.id).some((e) => e.kind === "conflict" && (e.text ?? "").includes(RESOLVE_ESCALATION_MARK))) {
@@ -15218,33 +15559,13 @@ async function tickOnce(deps) {
15218
15559
  }
15219
15560
  }
15220
15561
  if (!alive && run.status === "running" && run.reviews_run_id != null && run.pr_url && deps.verdictComment) {
15221
- const verdict = await deps.verdictComment(run.pr_url, run.created_at).catch(() => null);
15222
- if (verdict) {
15223
- const verdictText = verdictLabel(verdict.verdict);
15224
- deps.db.updateRun(run.reviews_run_id, { review_verdict: verdict.verdict });
15225
- deps.db.addEvent(
15226
- run.reviews_run_id,
15227
- "step",
15228
- `\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}`
15229
- );
15230
- if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
15231
- });
15232
- deps.db.updateRun(run.id, { status: "done" });
15233
- deps.db.addEvent(
15234
- run.id,
15235
- "done",
15236
- `\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)`
15237
- );
15238
- 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.`);
15239
- const reviewed = deps.db.getRun(run.reviews_run_id);
15240
- if (reviewed) {
15241
- 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);
15242
- if (verdict.verdict === "changes" && !stormActive && !deps.db.activeResolverFor(reviewed.id)) {
15243
- await wakeAuthorOnChanges(deps, reviewed, now, droneCap);
15244
- }
15245
- }
15246
- continue;
15247
- }
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;
15248
15569
  }
15249
15570
  if (!alive && run.status === "running" && run.reviews_run_id != null && run.pr_url && deps.ci) {
15250
15571
  const parent = deps.db.getRun(run.reviews_run_id);
@@ -15312,7 +15633,7 @@ async function tickOnce(deps) {
15312
15633
  if (run.stop_reason === RELIEF_PAUSE_REASON) continue;
15313
15634
  if (run.stop_reason != null && run.stop_reason.startsWith(EXITED_NO_DONE_REASON)) continue;
15314
15635
  if (run.pid != null && deps.isAlive(run.pid)) continue;
15315
- if (!await exclusiveGateAllows(deps, run)) continue;
15636
+ if (!await projectQuotaGateAllows(deps, run)) continue;
15316
15637
  const wt = run.worktree_path;
15317
15638
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
15318
15639
  const mode = canContinue ? "continue" : "fresh";
@@ -15392,7 +15713,7 @@ async function tickOnce(deps) {
15392
15713
  }
15393
15714
  if (deps.watchdog) {
15394
15715
  try {
15395
- const action = decideWatchdogAction(deps.db, now, deps.watchdog.state, deps.watchdog.thresholds, droneCap, { exists: worktreeExists, hasTranscript }, await exclusivityBlockedRunIds(deps));
15716
+ const action = decideWatchdogAction(deps.db, now, deps.watchdog.state, deps.watchdog.thresholds, droneCap, { exists: worktreeExists, hasTranscript }, await projectQuotaBlockedRunIds(deps));
15396
15717
  if (action) await applyWatchdogAction(deps, action);
15397
15718
  } catch (e) {
15398
15719
  console.error("[overmind] \u0441\u0442\u043E\u0440\u043E\u0436 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430: \u0442\u0438\u043A \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
@@ -15957,6 +16278,7 @@ async function runDaemon(opts) {
15957
16278
  let baseBranchReader;
15958
16279
  let envGate;
15959
16280
  let exclusiveProject;
16281
+ let projectDroneCap;
15960
16282
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
15961
16283
  const client = db;
15962
16284
  verifyModeReader = makeVerifyModeReader({
@@ -15996,6 +16318,13 @@ async function runDaemon(opts) {
15996
16318
  },
15997
16319
  ttlMs: 6e4
15998
16320
  });
16321
+ projectDroneCap = makeDroneCapReader({
16322
+ fetch: async (repo) => {
16323
+ const repoFullName = await remoteRepoFullName(repo).catch(() => null);
16324
+ return repoFullName ? client.repoDroneCap(repoFullName) : null;
16325
+ },
16326
+ ttlMs: 6e4
16327
+ });
15999
16328
  }
16000
16329
  let spawnReviewerFn;
16001
16330
  let verdictCommentFn;
@@ -16216,6 +16545,9 @@ async function runDaemon(opts) {
16216
16545
  // client-режим (настройка живёт в проекте); repoFallback — чтобы прогон с пустым repo не
16217
16546
  // выглядел отдельным проектом. Дедуп событий — своя мапа на цикл демона, как у envGate.
16218
16547
  ...exclusiveProject ? { exclusiveProject } : {},
16548
+ // TASWM-365: и квота «дронов на проект» рядом — один и тот же гейт очереди читает обе
16549
+ // настройки и берёт из них минимум (см. projectCapFor).
16550
+ ...projectDroneCap ? { projectDroneCap } : {},
16219
16551
  repoFallback: opts.repo,
16220
16552
  exclusiveLogged: /* @__PURE__ */ new Map(),
16221
16553
  // Зона задачи из истории репо (TASWM-348) — предлагаем сами, если постановщик
@@ -16672,6 +17004,7 @@ export {
16672
17004
  REVIEW_CYCLE_ESCALATION_MARK,
16673
17005
  REVIEW_CYCLE_LIMIT,
16674
17006
  REVIEW_ESCALATION_MARK,
17007
+ REVIEW_NO_VERDICT_REASON,
16675
17008
  REVIEW_PR_UNKNOWN_MARK,
16676
17009
  REVIEW_SPAWN_LIMIT,
16677
17010
  REVIEW_UNRECOVERABLE_MARK,