@taskless-app/swarm 0.6.65 → 0.6.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -254,7 +254,8 @@ var init_reviewPhase = __esm({
254
254
  verify_dead: "\u0412\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043D\u0435 \u0434\u0430\u043B \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
255
255
  ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
256
256
  ready_awaiting_ci: "\u0413\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443 \u2014 \u0436\u0434\u0451\u0442 CI",
257
- reviewer_dead: "\u0420\u0435\u0432\u044C\u044E\u0435\u0440 \u0443\u043C\u0435\u0440 \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A"
257
+ reviewer_dead: "\u0420\u0435\u0432\u044C\u044E\u0435\u0440 \u0443\u043C\u0435\u0440 \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
258
+ cycles_exhausted: "\u041B\u0438\u043C\u0438\u0442 \u043A\u0440\u0443\u0433\u043E\u0432 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A"
258
259
  };
259
260
  PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
260
261
  (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
@@ -705,16 +706,17 @@ var init_modelPolicy = __esm({
705
706
  roles: { executor: "glm-5.3", review: "glm-5.3" }
706
707
  },
707
708
  // Grok CLI. Каталог — живой `grok models` (16.10.2026), целиком и без выдуманных
708
- // имён: grok-4.7 (дефолт CLI и исполнителя), grok-4.7-build-fast, grok-4.6, grok-4.5
709
- // TASWM-605: обе роли — на свежей модели (решение владельца: рой работает на свежих
710
- // моделях). Это только фолбэк: демон берёт дефолт живого `grok models` (liveModels.ts).
709
+ // имён: grok-4.7 (дефолт CLI и исполнителя), grok-4.7-build-fast, grok-4.6, grok-4.5.
710
+ // TASWM-607: исполнитель остаётся на свежей модели. Живой дефолт CLI демон применяет
711
+ // только к роли executor (liveModels.ts). Ревьюер и проверяющий — на облегчённой
712
+ // grok-4.7-build-fast, пока человек не задал свою модель ревьюеров.
711
713
  // Тарифа нет: цену пишет CLI.
712
714
  {
713
715
  id: "xai",
714
716
  label: "xAI (Grok)",
715
717
  executorId: "grok",
716
718
  selectable: true,
717
- roles: { executor: "grok-4.7", review: "grok-4.7" },
719
+ roles: { executor: "grok-4.7", review: "grok-4.7-build-fast" },
718
720
  models: ["grok-4.7", "grok-4.7-build-fast", "grok-4.6", "grok-4.5"]
719
721
  },
720
722
  // TASWM-565 · шаг 1 из четырёх: провайдер в реестре ЕСТЬ, выбрать его ещё нельзя.
@@ -1788,17 +1790,22 @@ var init_effort = __esm({
1788
1790
  });
1789
1791
 
1790
1792
  // ../swarm-core/src/queuePlan.ts
1791
- var MIN_MS, QUEUE_NOMINAL_RUN_MS, QUEUE_MIN_TAIL_MS;
1793
+ var MIN_MS, QUEUE_NOMINAL_RUN_MS, QUEUE_MIN_TAIL_MS, SERVICE_DONE_KEEP_MS, PR_HEAD_LAG_MS, SERVICE_DONE_MARKER_MS;
1792
1794
  var init_queuePlan = __esm({
1793
1795
  "../swarm-core/src/queuePlan.ts"() {
1794
1796
  "use strict";
1795
1797
  init_status();
1796
1798
  init_hostPool();
1797
1799
  init_hosts();
1800
+ init_modelPolicy();
1798
1801
  init_time();
1802
+ init_verifyVerdict();
1799
1803
  MIN_MS = 6e4;
1800
1804
  QUEUE_NOMINAL_RUN_MS = 25 * MIN_MS;
1801
1805
  QUEUE_MIN_TAIL_MS = 3 * MIN_MS;
1806
+ SERVICE_DONE_KEEP_MS = 2 * 60 * MIN_MS;
1807
+ PR_HEAD_LAG_MS = 5 * MIN_MS;
1808
+ SERVICE_DONE_MARKER_MS = 8 * MIN_MS;
1802
1809
  }
1803
1810
  });
1804
1811
 
@@ -2852,7 +2859,8 @@ var init_liveModels = __esm({
2852
2859
 
2853
2860
  // src/swarmModels.ts
2854
2861
  function roleDefault(role, providerId) {
2855
- return liveDefaultModel(providerId) ?? defaultModelFor(role, providerId);
2862
+ if (role === "executor") return liveDefaultModel(providerId) ?? defaultModelFor(role, providerId);
2863
+ return defaultModelFor(role, providerId);
2856
2864
  }
2857
2865
  function pick2(v) {
2858
2866
  const s = typeof v === "string" ? v.trim() : "";
@@ -3872,7 +3880,7 @@ var init_package = __esm({
3872
3880
  "package.json"() {
3873
3881
  package_default = {
3874
3882
  name: "@taskless-app/swarm",
3875
- version: "0.6.65",
3883
+ version: "0.6.67",
3876
3884
  type: "module",
3877
3885
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
3878
3886
  keywords: [
@@ -3923,7 +3931,7 @@ var init_package = __esm({
3923
3931
  execa: "^10.0.0",
3924
3932
  zod: "3.25.76"
3925
3933
  },
3926
- gitHead: "254a8954d2f53f168b6cebc23b2daa0bc1562426"
3934
+ gitHead: "92a3176867bc1668677a4b6a25e02e0cb6661ce0"
3927
3935
  };
3928
3936
  }
3929
3937
  });
@@ -9253,6 +9261,8 @@ var SwarmClient = class _SwarmClient {
9253
9261
  if (!(typeof n === "number" && Number.isFinite(n) && n > 0)) return null;
9254
9262
  const str = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
9255
9263
  const cost = obj?.swarmSettings?.maxCostUsd;
9264
+ const rawLimit = obj?.swarmSettings?.reviewCycleLimit;
9265
+ const reviewCycleLimit = typeof rawLimit === "number" && Number.isInteger(rawLimit) && rawLimit >= 1 && rawLimit <= 20 ? rawLimit : null;
9256
9266
  return {
9257
9267
  maxDrones: Math.trunc(n),
9258
9268
  // Старый прод (до TASWM-364) поля swarmSettings не отдаёт вовсе — обе модели null,
@@ -9269,7 +9279,8 @@ var SwarmClient = class _SwarmClient {
9269
9279
  // значим («гарантия выключена осознанно»), поэтому пропускаем и его, а не только
9270
9280
  // положительные: null остаётся ровно за «ручка не задана / старый прод», и только
9271
9281
  // он уводит спавн на env-фолбэк и дефолт ядра.
9272
- maxCostUsd: typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : null
9282
+ maxCostUsd: typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : null,
9283
+ reviewCycleLimit
9273
9284
  };
9274
9285
  }
9275
9286
  /**
@@ -14847,10 +14858,11 @@ switch (cmd) {
14847
14858
  // TASWM-592 (ревью): сессию ищет харнесс ПРОГОНА, а не глобальный исполнитель —
14848
14859
  // иначе побудка codex-прогона при мёртвом демоне теряла продолжаемую сессию.
14849
14860
  hasTranscript: (run2) => !!run2.worktree_path && executorForRun(run2, null).session.exists(run2.worktree_path),
14850
- respawn: (r, mode) => respawnRun(
14861
+ respawn: (r, mode, nudge) => respawnRun(
14851
14862
  { db, launch, repoFallback: SWARM_REPO, worktreesDir: effectiveWorktreesDir(), getTask: () => Promise.resolve(null) },
14852
14863
  r,
14853
- mode
14864
+ mode,
14865
+ nudge
14854
14866
  )
14855
14867
  },
14856
14868
  runId
package/dist/overmind.js CHANGED
@@ -188,6 +188,9 @@ function reviewPhaseKindOf(label2) {
188
188
  function computeReviewPhase(input) {
189
189
  const { reviewVerdict, ciRed } = input;
190
190
  const verifyVerdict = input.verifyVerdict ?? null;
191
+ if (input.cyclesExhausted) {
192
+ return { kind: "cycles_exhausted", label: REVIEW_PHASE_TEXT.cycles_exhausted, ready: true };
193
+ }
191
194
  if (!ciRed && isNegativeVerifyVerdict(verifyVerdict)) {
192
195
  const kind = verifyVerdict === "found_issues" ? "verify_found" : "verify_not_reproduced";
193
196
  return { kind, label: REVIEW_PHASE_TEXT[kind], ready: false };
@@ -242,7 +245,8 @@ var init_reviewPhase = __esm({
242
245
  verify_dead: "\u0412\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043D\u0435 \u0434\u0430\u043B \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
243
246
  ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
244
247
  ready_awaiting_ci: "\u0413\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443 \u2014 \u0436\u0434\u0451\u0442 CI",
245
- reviewer_dead: "\u0420\u0435\u0432\u044C\u044E\u0435\u0440 \u0443\u043C\u0435\u0440 \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A"
248
+ reviewer_dead: "\u0420\u0435\u0432\u044C\u044E\u0435\u0440 \u0443\u043C\u0435\u0440 \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
249
+ cycles_exhausted: "\u041B\u0438\u043C\u0438\u0442 \u043A\u0440\u0443\u0433\u043E\u0432 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A"
246
250
  };
247
251
  PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
248
252
  (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
@@ -702,16 +706,17 @@ var init_modelPolicy = __esm({
702
706
  roles: { executor: "glm-5.3", review: "glm-5.3" }
703
707
  },
704
708
  // Grok CLI. Каталог — живой `grok models` (16.10.2026), целиком и без выдуманных
705
- // имён: grok-4.7 (дефолт CLI и исполнителя), grok-4.7-build-fast, grok-4.6, grok-4.5
706
- // TASWM-605: обе роли — на свежей модели (решение владельца: рой работает на свежих
707
- // моделях). Это только фолбэк: демон берёт дефолт живого `grok models` (liveModels.ts).
709
+ // имён: grok-4.7 (дефолт CLI и исполнителя), grok-4.7-build-fast, grok-4.6, grok-4.5.
710
+ // TASWM-607: исполнитель остаётся на свежей модели. Живой дефолт CLI демон применяет
711
+ // только к роли executor (liveModels.ts). Ревьюер и проверяющий — на облегчённой
712
+ // grok-4.7-build-fast, пока человек не задал свою модель ревьюеров.
708
713
  // Тарифа нет: цену пишет CLI.
709
714
  {
710
715
  id: "xai",
711
716
  label: "xAI (Grok)",
712
717
  executorId: "grok",
713
718
  selectable: true,
714
- roles: { executor: "grok-4.7", review: "grok-4.7" },
719
+ roles: { executor: "grok-4.7", review: "grok-4.7-build-fast" },
715
720
  models: ["grok-4.7", "grok-4.7-build-fast", "grok-4.6", "grok-4.5"]
716
721
  },
717
722
  // TASWM-565 · шаг 1 из четырёх: провайдер в реестре ЕСТЬ, выбрать его ещё нельзя.
@@ -2019,17 +2024,22 @@ var init_effort = __esm({
2019
2024
  });
2020
2025
 
2021
2026
  // ../swarm-core/src/queuePlan.ts
2022
- var MIN_MS, QUEUE_NOMINAL_RUN_MS, QUEUE_MIN_TAIL_MS;
2027
+ var MIN_MS, QUEUE_NOMINAL_RUN_MS, QUEUE_MIN_TAIL_MS, SERVICE_DONE_KEEP_MS, PR_HEAD_LAG_MS, SERVICE_DONE_MARKER_MS;
2023
2028
  var init_queuePlan = __esm({
2024
2029
  "../swarm-core/src/queuePlan.ts"() {
2025
2030
  "use strict";
2026
2031
  init_status();
2027
2032
  init_hostPool();
2028
2033
  init_hosts();
2034
+ init_modelPolicy();
2029
2035
  init_time();
2036
+ init_verifyVerdict();
2030
2037
  MIN_MS = 6e4;
2031
2038
  QUEUE_NOMINAL_RUN_MS = 25 * MIN_MS;
2032
2039
  QUEUE_MIN_TAIL_MS = 3 * MIN_MS;
2040
+ SERVICE_DONE_KEEP_MS = 2 * 60 * MIN_MS;
2041
+ PR_HEAD_LAG_MS = 5 * MIN_MS;
2042
+ SERVICE_DONE_MARKER_MS = 8 * MIN_MS;
2033
2043
  }
2034
2044
  });
2035
2045
 
@@ -3099,7 +3109,8 @@ var init_liveModels = __esm({
3099
3109
 
3100
3110
  // src/swarmModels.ts
3101
3111
  function roleDefault(role, providerId) {
3102
- return liveDefaultModel(providerId) ?? defaultModelFor(role, providerId);
3112
+ if (role === "executor") return liveDefaultModel(providerId) ?? defaultModelFor(role, providerId);
3113
+ return defaultModelFor(role, providerId);
3103
3114
  }
3104
3115
  function pick2(v) {
3105
3116
  const s = typeof v === "string" ? v.trim() : "";
@@ -3135,6 +3146,10 @@ function resolveRunModelRef(run2, swarm, env = process.env) {
3135
3146
  const providerId = pick2(env.SWARM_MODEL_PROVIDER) ?? DEFAULT_PROVIDER_ID;
3136
3147
  return { providerId, model: roleDefault(modelRoleOf(run2.kind), providerId) };
3137
3148
  }
3149
+ function modelRefToRecord(run2, ref) {
3150
+ if (!run2 || pick2(run2.model)) return null;
3151
+ return `${ref.providerId}/${ref.model}`;
3152
+ }
3138
3153
  function resolveRunEffort(run2, swarm, env = process.env) {
3139
3154
  return toEffortLevel(run2.effort) ?? forKind(run2.kind, swarm, toEffortLevel) ?? toEffortLevel(env.SWARM_DRONE_EFFORT) ?? null;
3140
3155
  }
@@ -9525,6 +9540,8 @@ var SwarmClient = class _SwarmClient {
9525
9540
  if (!(typeof n === "number" && Number.isFinite(n) && n > 0)) return null;
9526
9541
  const str2 = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
9527
9542
  const cost = obj?.swarmSettings?.maxCostUsd;
9543
+ const rawLimit = obj?.swarmSettings?.reviewCycleLimit;
9544
+ const reviewCycleLimit = typeof rawLimit === "number" && Number.isInteger(rawLimit) && rawLimit >= 1 && rawLimit <= 20 ? rawLimit : null;
9528
9545
  return {
9529
9546
  maxDrones: Math.trunc(n),
9530
9547
  // Старый прод (до TASWM-364) поля swarmSettings не отдаёт вовсе — обе модели null,
@@ -9541,7 +9558,8 @@ var SwarmClient = class _SwarmClient {
9541
9558
  // значим («гарантия выключена осознанно»), поэтому пропускаем и его, а не только
9542
9559
  // положительные: null остаётся ровно за «ручка не задана / старый прод», и только
9543
9560
  // он уводит спавн на env-фолбэк и дефолт ядра.
9544
- maxCostUsd: typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : null
9561
+ maxCostUsd: typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : null,
9562
+ reviewCycleLimit
9545
9563
  };
9546
9564
  }
9547
9565
  /**
@@ -13056,7 +13074,7 @@ function parkReasonUnretriable(reason) {
13056
13074
  // package.json
13057
13075
  var package_default = {
13058
13076
  name: "@taskless-app/swarm",
13059
- version: "0.6.65",
13077
+ version: "0.6.67",
13060
13078
  type: "module",
13061
13079
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
13062
13080
  keywords: [
@@ -13107,7 +13125,7 @@ var package_default = {
13107
13125
  execa: "^10.0.0",
13108
13126
  zod: "3.25.76"
13109
13127
  },
13110
- gitHead: "254a8954d2f53f168b6cebc23b2daa0bc1562426"
13128
+ gitHead: "92a3176867bc1668677a4b6a25e02e0cb6661ce0"
13111
13129
  };
13112
13130
 
13113
13131
  // src/version.ts
@@ -14116,12 +14134,36 @@ init_src();
14116
14134
 
14117
14135
  // src/reviewCycle.ts
14118
14136
  init_src();
14137
+ var REVIEW_CYCLE_LIMIT_DEFAULT = 3;
14138
+ var REVIEW_CYCLE_LIMIT_CAP = 20;
14139
+ function validReviewCycleLimit(n) {
14140
+ return n != null && Number.isInteger(n) && n >= 1 && n <= REVIEW_CYCLE_LIMIT_CAP;
14141
+ }
14142
+ function parseReviewCycleLimitEnv(raw) {
14143
+ if (raw == null || raw.trim() === "") return REVIEW_CYCLE_LIMIT_DEFAULT;
14144
+ const n = Number(raw);
14145
+ return validReviewCycleLimit(n) ? n : REVIEW_CYCLE_LIMIT_DEFAULT;
14146
+ }
14147
+ var REVIEW_CYCLE_LIMIT = parseReviewCycleLimitEnv(process.env.SWARM_REVIEW_CYCLE_LIMIT);
14148
+ var configuredReviewCycleLimit = null;
14149
+ function configureReviewCycleLimit(n) {
14150
+ configuredReviewCycleLimit = validReviewCycleLimit(n) ? n : null;
14151
+ }
14152
+ function effectiveReviewCycleLimit() {
14153
+ return configuredReviewCycleLimit ?? REVIEW_CYCLE_LIMIT;
14154
+ }
14155
+ function reviewCyclesExhausted(run2) {
14156
+ return (run2.review_cycle ?? 0) >= effectiveReviewCycleLimit();
14157
+ }
14119
14158
  function needsFixIteration(run2) {
14120
14159
  return run2.review_verdict === "changes" || isNegativeVerifyVerdict(toVerifyVerdict(run2.verify_verdict));
14121
14160
  }
14122
14161
  function reviewTransitionPatch(run2, prUrl) {
14123
14162
  const isFixIteration = needsFixIteration(run2);
14124
14163
  if (isFixIteration) {
14164
+ if (reviewCyclesExhausted(run2)) {
14165
+ return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
14166
+ }
14125
14167
  const cycle = (run2.review_cycle ?? 0) + 1;
14126
14168
  return {
14127
14169
  isFixIteration: true,
@@ -19122,7 +19164,87 @@ var BASE_MERGED_MARK = "\u0431\u0430\u0437\u0430 \u0432\u043B\u0438\u0442\u0430
19122
19164
  var BASE_NOOP_MARK = "\u0432\u043B\u0438\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E";
19123
19165
  var RESOLVER_RESOLVED_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 main \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D \u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440\u043E\u043C";
19124
19166
  var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
19125
- var REVIEW_CYCLE_LIMIT = Number(process.env.SWARM_REVIEW_CYCLE_LIMIT) || 3;
19167
+ function reviewCycleHumanBody(cycle, limit) {
19168
+ return `\u041B\u0438\u043C\u0438\u0442 \u043A\u0440\u0443\u0433\u043E\u0432 \u043F\u0440\u0430\u0432\u043E\u043A \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D (${cycle} \u0438\u0437 ${limit}) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A. \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0440\u0435\u0432\u044C\u044E, \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0438 \u043F\u043E\u0431\u0443\u0434\u043A\u0438 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u044B \u2014 PR \u043E\u0441\u0442\u0430\u0451\u0442\u0441\u044F \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443.`;
19169
+ }
19170
+ var CYCLE_TASK_COMMENT_SENT = "\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043B\u0438\u043C\u0438\u0442\u0430 \u043A\u0440\u0443\u0433\u043E\u0432 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D";
19171
+ var CYCLE_PR_COMMENT_SENT = "\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043B\u0438\u043C\u0438\u0442\u0430 \u043A\u0440\u0443\u0433\u043E\u0432 \u0432 PR \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D";
19172
+ var cycleCommentInflight = /* @__PURE__ */ new Map();
19173
+ function takeCycleCommentSlot(runId, side) {
19174
+ let slot = cycleCommentInflight.get(runId);
19175
+ if (!slot) {
19176
+ slot = { task: false, pr: false };
19177
+ cycleCommentInflight.set(runId, slot);
19178
+ }
19179
+ if (slot[side]) return false;
19180
+ slot[side] = true;
19181
+ return true;
19182
+ }
19183
+ function freeCycleCommentSlot(runId, side) {
19184
+ const slot = cycleCommentInflight.get(runId);
19185
+ if (slot) slot[side] = false;
19186
+ }
19187
+ async function noteReviewCyclesExhausted(deps, run2) {
19188
+ const texts = deps.db.events(run2.id).map((e) => e.text ?? "");
19189
+ const marked = texts.some((t) => t.includes(REVIEW_CYCLE_ESCALATION_MARK));
19190
+ const taskSent = texts.some((t) => t.includes(CYCLE_TASK_COMMENT_SENT));
19191
+ const prSent = texts.some((t) => t.includes(CYCLE_PR_COMMENT_SENT));
19192
+ const cycle = run2.review_cycle ?? 0;
19193
+ const body = reviewCycleHumanBody(cycle, effectiveReviewCycleLimit());
19194
+ if (!marked) {
19195
+ deps.db.addEvent(run2.id, "step", `${REVIEW_CYCLE_ESCALATION_MARK}: ${body}`);
19196
+ try {
19197
+ deps.narrate?.(body);
19198
+ } catch {
19199
+ }
19200
+ }
19201
+ if (deps.reportComment && !taskSent && takeCycleCommentSlot(run2.id, "task")) {
19202
+ const ok = await deps.reportComment(
19203
+ run2.task_slug,
19204
+ body,
19205
+ { swarmKind: "report", swarmDroneName: run2.name || "(\u0438\u043C\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)", swarmReviewCycle: cycle },
19206
+ run2.workspace_id
19207
+ ).then(() => true).catch((e) => {
19208
+ deps.db.addEvent(run2.id, "step", `\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \xAB\u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A\xBB \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0443\u0448\u0451\u043B: ${e instanceof Error ? e.message : String(e)}`);
19209
+ return false;
19210
+ });
19211
+ if (ok) deps.db.addEvent(run2.id, "step", CYCLE_TASK_COMMENT_SENT);
19212
+ freeCycleCommentSlot(run2.id, "task");
19213
+ }
19214
+ if (run2.pr_url && deps.prComment && !prSent && takeCycleCommentSlot(run2.id, "pr")) {
19215
+ const ok = await deps.prComment(run2.pr_url, body).then(() => true).catch((e) => {
19216
+ deps.db.addEvent(run2.id, "step", `\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \xAB\u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A\xBB \u0432 PR \u043D\u0435 \u0443\u0448\u0451\u043B: ${e instanceof Error ? e.message : String(e)}`);
19217
+ return false;
19218
+ });
19219
+ if (ok) deps.db.addEvent(run2.id, "step", CYCLE_PR_COMMENT_SENT);
19220
+ freeCycleCommentSlot(run2.id, "pr");
19221
+ }
19222
+ }
19223
+ function cycleHolder(db, run2) {
19224
+ if (run2.kind === "hero" || run2.resolves_run_id != null) return null;
19225
+ const parentId = run2.reviews_run_id ?? run2.verifies_run_id;
19226
+ if (parentId != null) return db.getRun(parentId) ?? null;
19227
+ return run2;
19228
+ }
19229
+ function cycleCapHolder(db, run2) {
19230
+ const holder = cycleHolder(db, run2);
19231
+ return holder && reviewCyclesExhausted(holder) ? holder : null;
19232
+ }
19233
+ var CYCLE_CAP_STOP_REASON = "\u043B\u0438\u043C\u0438\u0442 \u043A\u0440\u0443\u0433\u043E\u0432 \u043F\u0440\u0430\u0432\u043E\u043A \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A";
19234
+ async function handOffAtCycleCap(deps, run2, holder, where) {
19235
+ await noteReviewCyclesExhausted(deps, holder);
19236
+ const text = `${CYCLE_CAP_STOP_REASON} (${holder.review_cycle ?? 0} \u0438\u0437 ${effectiveReviewCycleLimit()}): ${where} \u2014 \u0441\u0435\u0441\u0441\u0438\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E`;
19237
+ if (holder.id !== run2.id) {
19238
+ if (run2.worktree_path) await deps.removeWorktree(run2.repo, run2.worktree_path).catch(() => {
19239
+ });
19240
+ deps.db.updateRun(run2.id, { status: "stopped", stop_reason: CYCLE_CAP_STOP_REASON });
19241
+ deps.db.addEvent(run2.id, "stopped", text);
19242
+ return;
19243
+ }
19244
+ if (run2.pr_url) deps.db.updateRun(run2.id, { status: "review", stop_reason: null });
19245
+ else deps.db.updateRun(run2.id, { status: "paused", stop_reason: CYCLE_CAP_STOP_REASON });
19246
+ deps.db.addEvent(run2.id, "step", text);
19247
+ }
19126
19248
  var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
19127
19249
  var CI_WAKE_COOLDOWN_MS = Number(process.env.SWARM_CI_WAKE_COOLDOWN_MS) || 3 * 60 * 1e3;
19128
19250
  var CI_CHECK_COOLDOWN_MS = Number(process.env.SWARM_CI_CHECK_COOLDOWN_MS) || 60 * 1e3;
@@ -19149,7 +19271,7 @@ var CHANGES_WAKE_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430
19149
19271
  var CHANGES_FIX_COST_MARK = "\u0442\u0440\u0430\u0442\u044B \u0434\u043E \u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0438";
19150
19272
  var CHANGES_WAKE_FAILED_MARK = "\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C";
19151
19273
  var BUDGET_STOP_REASON = "\u043D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442: \u0441\u0435\u0441\u0441\u0438\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B \u0431\u044E\u0434\u0436\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430";
19152
- var NO_AUTO_WAKE_REASONS = [EXITED_NO_DONE_REASON, BUDGET_STOP_REASON, ABORTED_STOP_REASON, AWAITING_BUDGET_REASON];
19274
+ var NO_AUTO_WAKE_REASONS = [EXITED_NO_DONE_REASON, BUDGET_STOP_REASON, ABORTED_STOP_REASON, AWAITING_BUDGET_REASON, CYCLE_CAP_STOP_REASON];
19153
19275
  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)";
19154
19276
  function verdictCommentMark(createdAt) {
19155
19277
  return `\u043A\u043E\u043C\u043C\u0435\u043D\u0442-\u0432\u0435\u0440\u0434\u0438\u043A\u0442@${createdAt}`;
@@ -19312,7 +19434,9 @@ function reviewPhaseInputFor(deps, run2, projectWantsVerify = true) {
19312
19434
  reviewerDead,
19313
19435
  verifyVerdict: run2.verify_verdict,
19314
19436
  verifyExpected,
19315
- verifierDead
19437
+ verifierDead,
19438
+ // TASWM-606: потолок кругов важнее «чинит CI» и «ждёт ревьюера» — человека уже позвали.
19439
+ cyclesExhausted: reviewCyclesExhausted(run2)
19316
19440
  };
19317
19441
  }
19318
19442
  async function refreshReviewPhases(deps) {
@@ -19605,11 +19729,13 @@ function relinkUnboundReviewers(deps) {
19605
19729
  narrate(deps, `${droneLabel(rv)} \u0440\u0430\u0431\u043E\u0442\u0430\u043B \u0431\u0435\u0437 \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438 \u043A \u043F\u0440\u043E\u0433\u043E\u043D\u0443 \u2014 \u0441\u0432\u044F\u0437\u0430\u043B \u043F\u043E PR-\u0441\u0441\u044B\u043B\u043A\u0435, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0442\u0435\u043F\u0435\u0440\u044C \u043B\u044F\u0436\u0435\u0442 \u0432 ${parent.task_slug}, \u0430 \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442 GitHub.`);
19606
19730
  }
19607
19731
  }
19608
- var ESCALATION_LIMITS = { reviewSpawn: REVIEW_SPAWN_LIMIT, reviewCycle: REVIEW_CYCLE_LIMIT };
19732
+ function escalationLimits() {
19733
+ return { reviewSpawn: REVIEW_SPAWN_LIMIT, reviewCycle: effectiveReviewCycleLimit() };
19734
+ }
19609
19735
  function deliberateReviewStop(db, run2) {
19610
19736
  const texts = db.events(run2.id).map((e) => e.text ?? "");
19611
19737
  const hit = findEscalation(texts, run2, "reviewer");
19612
- return hit?.stopReason?.(run2, ESCALATION_LIMITS) ?? null;
19738
+ return hit?.stopReason?.(run2, escalationLimits()) ?? null;
19613
19739
  }
19614
19740
  function noteReviewBlocked(deps, run2, reason) {
19615
19741
  if (!shouldNoteReason(deps.db, run2.id, reason)) return;
@@ -19694,6 +19820,10 @@ async function spawnVerifiersForOpenPrs(deps, stormActive) {
19694
19820
  if (run2.kind !== "drone") continue;
19695
19821
  if (run2.reviews_run_id != null || run2.resolves_run_id != null || run2.verifies_run_id != null) continue;
19696
19822
  if (!run2.pr_url || !isGithubPrUrl(run2.pr_url)) continue;
19823
+ if (reviewCyclesExhausted(run2)) {
19824
+ await noteReviewCyclesExhausted(deps, run2);
19825
+ continue;
19826
+ }
19697
19827
  if (run2.verify_verdict != null) continue;
19698
19828
  if (deps.db.verifierCountFor(run2.id) > 0) continue;
19699
19829
  const attempts = deps.db.verifierAttemptCountFor(run2.id);
@@ -19719,6 +19849,18 @@ async function spawnVerifiersForOpenPrs(deps, stormActive) {
19719
19849
  }
19720
19850
  }
19721
19851
  var CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C (\u0441\u0435\u0442\u044C/\u043F\u0440\u043E\u0446\u0435\u0441\u0441) \u2014 \u0442\u044B headless-\u0434\u0440\u043E\u043D, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u0423 \u0416\u0415 \u0437\u0430\u0434\u0430\u0447\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 `git status`/`git log` \u0438 \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0441\u0432\u043E\u0438\u0445 \u0448\u0430\u0433\u043E\u0432: \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E \u2014 \u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438 \u0441 \u043C\u0435\u0441\u0442\u0430 \u043E\u0431\u0440\u044B\u0432\u0430. \u0414\u043E\u0432\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u043E \u043A\u043E\u043D\u0446\u0430, \u041A\u041E\u041C\u041C\u0418\u0422\u042C \u0418 \u041F\u0423\u0428\u042C \u043F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0448\u0430\u0433\u0430, \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url). \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.";
19852
+ function buildSubmittedContinueNudge(opts) {
19853
+ const verify = opts.verifySummary ? `\u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044E\u0449\u0435\u0433\u043E:
19854
+ ${opts.verifySummary}` : "\u0412\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044E\u0449\u0435\u0433\u043E \u0432 \u043B\u0435\u043D\u0442\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u2014 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0439 \u0435\u0433\u043E \u0441\u0430\u043C: `gh pr view --comments`.";
19855
+ const review = opts.reviewSummary ? `\u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430:
19856
+ ${opts.reviewSummary}` : "";
19857
+ return [
19858
+ `\u0422\u044B \u0443\u0436\u0435 \u0441\u0434\u0430\u043B \u0440\u0430\u0431\u043E\u0442\u0443: PR ${opts.prUrl} \u043E\u0442\u043A\u0440\u044B\u0442. \u041D\u0415 \u0432\u044B\u0437\u044B\u0432\u0430\u0439 done() \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u0435 \u2014 \u044D\u0442\u043E \u0441\u043E\u0436\u0436\u0451\u0442 \u0435\u0449\u0451 \u043E\u0434\u0438\u043D \u043A\u0440\u0443\u0433 \u043F\u0440\u0430\u0432\u043E\u043A \u0432\u043F\u0443\u0441\u0442\u0443\u044E.`,
19859
+ verify,
19860
+ review,
19861
+ "\u041F\u0440\u043E\u0447\u0438\u0442\u0430\u0439 \u0432\u0435\u0440\u0434\u0438\u043A\u0442, \u043F\u043E\u0447\u0438\u043D\u0438 \u043F\u0440\u0438\u0447\u0438\u043D\u0443, \u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0442\u044C \u041D\u041E\u0412\u0423\u042E \u0440\u0430\u0431\u043E\u0442\u0443 \u0438 \u0437\u0430\u043F\u0443\u0448\u044C \u0432 \u0442\u0443 \u0436\u0435 \u0432\u0435\u0442\u043A\u0443. \u0422\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u043B\u0435 \u043D\u043E\u0432\u043E\u0433\u043E \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url) \u0441 \u0442\u043E\u0439 \u0436\u0435 \u0441\u0441\u044B\u043B\u043A\u043E\u0439. \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."
19862
+ ].filter((part) => part.length > 0).join("\n\n");
19863
+ }
19722
19864
  function buildCiWakeNudge(summary) {
19723
19865
  return [
19724
19866
  "\u{1F534} CI \u0443\u043F\u0430\u043B \u043D\u0430 \u0442\u0432\u043E\u0451\u043C PR \u2014 \u0440\u043E\u0439 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043B `gh pr checks` \u0438 \u043F\u0440\u0438\u0441\u043B\u0430\u043B \u0442\u0435\u0431\u0435 \u0441\u0436\u0430\u0442\u044B\u0439 \u0440\u0430\u0437\u0431\u043E\u0440 \u0444\u0435\u0439\u043B\u0430 (\u043D\u0435 \u0432\u0435\u0441\u044C \u043B\u043E\u0433, \u0442\u043E\u043A\u0435\u043D-\u0434\u0438\u0441\u0446\u0438\u043F\u043B\u0438\u043D\u0430):",
@@ -20040,6 +20182,15 @@ function lastVerdictEvent(db, reviewRunId) {
20040
20182
  function lastChangesSummary(db, reviewRunId) {
20041
20183
  return lastVerdictEvent(db, reviewRunId)?.text ?? null;
20042
20184
  }
20185
+ var VERIFY_EVENT_PREFIXES = ["\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u043E\u0442", "\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u043F\u043E\u0434\u0445\u0432\u0430\u0447\u0435\u043D \u0438\u0437 PR-\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430"];
20186
+ function lastVerifySummary(db, runId) {
20187
+ const events = db.events(runId);
20188
+ for (let i = events.length - 1; i >= 0; i--) {
20189
+ const text = events[i].text ?? "";
20190
+ if (VERIFY_EVENT_PREFIXES.some((p) => text.startsWith(p))) return text;
20191
+ }
20192
+ return null;
20193
+ }
20043
20194
  function changesVerdictAt(db, reviewRunId) {
20044
20195
  return lastVerdictEvent(db, reviewRunId)?.at ?? null;
20045
20196
  }
@@ -20219,6 +20370,11 @@ async function resurrectStormDead(deps, worktreeExists, hasTranscript, opts) {
20219
20370
  let resurrected = 0;
20220
20371
  for (const run2 of deps.db.listRuns("storm_dead")) {
20221
20372
  if (countActiveDrones(deps.db) >= cap) break;
20373
+ const capHolder = cycleCapHolder(deps.db, run2);
20374
+ if (capHolder) {
20375
+ await handOffAtCycleCap(deps, run2, capHolder, "\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 \u043F\u043E\u0441\u043B\u0435 \u0448\u0442\u043E\u0440\u043C\u0430");
20376
+ continue;
20377
+ }
20222
20378
  if (!await projectQuotaGateAllows(deps, run2)) continue;
20223
20379
  const wt = run2.worktree_path;
20224
20380
  const mode = !!wt && worktreeExists(wt) && hasTranscript(run2) ? "continue" : "fresh";
@@ -20242,6 +20398,16 @@ async function wakeRun(deps, runId, opts = {}) {
20242
20398
  }
20243
20399
  if (run2.pid != null && deps.isAlive(run2.pid)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 (pid ${run2.pid}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
20244
20400
  if (isTerminalStatus(run2.status)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435\u043D (${run2.status}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
20401
+ const capHolder = cycleCapHolder(deps.db, run2);
20402
+ if (capHolder) {
20403
+ await noteReviewCyclesExhausted(deps, capHolder);
20404
+ const limit = effectiveReviewCycleLimit();
20405
+ const whose = capHolder.id === run2.id ? `\u043F\u0440\u043E\u0433\u043E\u043D #${runId}` : `\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D #${capHolder.id} (\u0434\u043B\u044F #${runId})`;
20406
+ return {
20407
+ ok: false,
20408
+ why: `${whose} \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043B \u043B\u0438\u043C\u0438\u0442 \u043A\u0440\u0443\u0433\u043E\u0432 \u043F\u0440\u0430\u0432\u043E\u043A (${capHolder.review_cycle ?? 0} \u0438\u0437 ${limit}) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A, \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u043F\u043E\u0431\u0443\u0434\u043A\u0438 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u044B`
20409
+ };
20410
+ }
20245
20411
  const stoppedForHuman = deps.db.events(run2.id).some((e) => isEscalationText(e.text ?? "", run2, "author"));
20246
20412
  if (run2.status === "review" && run2.pr_url && !needsFixIteration(run2) && !stoppedForHuman) {
20247
20413
  return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0441\u0434\u0430\u043D \u043D\u0430 \u0440\u0435\u0432\u044C\u044E (${run2.pr_url}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E; \u043F\u0440\u0430\u0432\u043A\u0438 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442 \u0440\u043E\u0439 \u043F\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0443` };
@@ -20255,8 +20421,11 @@ async function wakeRun(deps, runId, opts = {}) {
20255
20421
  return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u2014 \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0439 (\u0440\u0435\u0432\u044C\u044E\u0435\u0440/\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440), \u0430 \u0441\u0435\u0441\u0441\u0438\u044E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u043C: fresh \u043F\u043E\u0442\u0435\u0440\u044F\u043B \u0431\u044B \u0435\u0433\u043E \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442` };
20256
20422
  }
20257
20423
  const mode = canContinue ? "continue" : "fresh";
20424
+ const verifySummary = lastVerifySummary(deps.db, run2.id);
20425
+ const reviewSummary = lastChangesSummary(deps.db, run2.id);
20426
+ const nudge = mode === "continue" && !isSubrun(run2) && run2.kind !== "hero" && run2.pr_url && (needsFixIteration(run2) || verifySummary || reviewSummary) ? buildSubmittedContinueNudge({ prUrl: run2.pr_url, verifySummary, reviewSummary }) : void 0;
20258
20427
  try {
20259
- await deps.respawn(run2, mode);
20428
+ await deps.respawn(run2, mode, nudge);
20260
20429
  } catch (e) {
20261
20430
  return { ok: false, why: `\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${runId} \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C: ${String(e)}` };
20262
20431
  }
@@ -20448,6 +20617,10 @@ function closeCiWatch(deps, run2, mark, why, narration) {
20448
20617
  }
20449
20618
  async function wakeAuthorOnRedCi(deps, run2, now, ci, worktreeExists, hasTranscript, droneCap) {
20450
20619
  if (run2.pid != null && deps.isAlive(run2.pid)) return false;
20620
+ if (reviewCyclesExhausted(run2)) {
20621
+ await noteReviewCyclesExhausted(deps, run2);
20622
+ return false;
20623
+ }
20451
20624
  if (deps.resourcesOk && !deps.resourcesOk()) return false;
20452
20625
  if (countActiveDrones(deps.db) >= droneCap) return false;
20453
20626
  if (!await projectQuotaGateAllows(deps, run2)) return false;
@@ -20517,6 +20690,15 @@ async function wakeReviewerOnCiReady(deps, run2, over) {
20517
20690
  const reviewer = awaitingCiReviewer(deps.db, run2);
20518
20691
  if (!reviewer) return false;
20519
20692
  if (reviewer.pid != null && deps.isAlive(reviewer.pid)) return false;
20693
+ if (reviewCyclesExhausted(run2)) {
20694
+ await noteReviewCyclesExhausted(deps, run2);
20695
+ deps.db.addEvent(
20696
+ run2.id,
20697
+ "ci_checked",
20698
+ `${ciReadyLabel(over)} \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u0431\u0443\u0436\u0443: \u043B\u0438\u043C\u0438\u0442 \u043A\u0440\u0443\u0433\u043E\u0432 \u043F\u0440\u0430\u0432\u043E\u043A \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`
20699
+ );
20700
+ return true;
20701
+ }
20520
20702
  if (deps.resourcesOk && !deps.resourcesOk()) {
20521
20703
  deps.db.addEvent(run2.id, "ci_checked", `${ciReadyLabel(over)} \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0436\u0434\u0451\u0442, \u043C\u0430\u0448\u0438\u043D\u0430 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D\u0430`);
20522
20704
  return true;
@@ -20591,7 +20773,8 @@ async function spawnFreshReviewerAfterStall(deps, run2, why) {
20591
20773
  deps.db.addEvent(run2.id, "step", `\u0441\u0432\u0435\u0436\u0438\u0439 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D: ${live2[0].name} \u0435\u0449\u0451 \u0436\u0438\u0432 (${live2[0].stop_reason ?? live2[0].status})`);
20592
20774
  return;
20593
20775
  }
20594
- if (run2.review_cycle >= REVIEW_CYCLE_LIMIT || deps.db.reviewerAttemptCountFor(run2.id) >= REVIEW_SPAWN_LIMIT) {
20776
+ if (reviewCyclesExhausted(run2) || deps.db.reviewerAttemptCountFor(run2.id) >= REVIEW_SPAWN_LIMIT) {
20777
+ if (reviewCyclesExhausted(run2)) await noteReviewCyclesExhausted(deps, run2);
20595
20778
  return;
20596
20779
  }
20597
20780
  const gate = await prGateForReviewer(deps, run2, run2.pr_url, "\u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B CI");
@@ -20615,7 +20798,10 @@ async function wakeAuthorOnChanges(deps, snapshot, now, droneCap) {
20615
20798
  if (deps.resourcesOk && !deps.resourcesOk()) return false;
20616
20799
  if (countActiveDrones(deps.db) >= droneCap) return false;
20617
20800
  if (!await projectQuotaGateAllows(deps, run2)) return false;
20618
- if (run2.review_cycle >= REVIEW_CYCLE_LIMIT) return false;
20801
+ if (reviewCyclesExhausted(run2)) {
20802
+ await noteReviewCyclesExhausted(deps, run2);
20803
+ return false;
20804
+ }
20619
20805
  const prUrl = run2.pr_url;
20620
20806
  const events = deps.db.events(run2.id);
20621
20807
  const mark = `${CHANGES_WAKE_ESCALATION_MARK} (\u043A\u0440\u0443\u0433 ${run2.review_cycle ?? 0})`;
@@ -21168,6 +21354,11 @@ async function resumeWatchdogParked(deps, droneCap) {
21168
21354
  }
21169
21355
  if (!candidate) return;
21170
21356
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
21357
+ const capHolder = cycleCapHolder(deps.db, candidate);
21358
+ if (capHolder) {
21359
+ await handOffAtCycleCap(deps, candidate, capHolder, "\u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u0438\u0437 \u043F\u0430\u0440\u043A\u043E\u0432\u043A\u0438 \u0441\u0442\u043E\u0440\u043E\u0436\u0430");
21360
+ return;
21361
+ }
21171
21362
  const worktreeExists = deps.worktreeExists ?? existsSync22;
21172
21363
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
21173
21364
  const wt = candidate.worktree_path;
@@ -21227,6 +21418,11 @@ async function applyWatchdogAction(deps, action) {
21227
21418
  }
21228
21419
  case "fresh": {
21229
21420
  if (!deps.respawn) return;
21421
+ const capHolder = cycleCapHolder(deps.db, action.run);
21422
+ if (capHolder) {
21423
+ await handOffAtCycleCap(deps, action.run, capHolder, "\u0441\u0432\u0435\u0436\u0438\u0439 \u043F\u043E\u0434\u044A\u0451\u043C \u0441\u0442\u043E\u0440\u043E\u0436\u0435\u043C \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430");
21424
+ return;
21425
+ }
21230
21426
  try {
21231
21427
  await deps.respawn(action.run, "fresh", buildFreshRestartNudge(action.run));
21232
21428
  deps.db.addEvent(action.run.id, "resumed", `\u0441\u0442\u043E\u0440\u043E\u0436 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430: ${action.reason}`);
@@ -21470,6 +21666,11 @@ async function manageRelief(deps, now, droneCap, stormActive) {
21470
21666
  }
21471
21667
  if (now - loopEntry.lastPauseAt < reliefBackoffMs(loopEntry.pauses, cfg.rampWindowMs, loopCfg)) return;
21472
21668
  }
21669
+ const capHolder = cycleCapHolder(deps.db, resumeCandidate);
21670
+ if (capHolder) {
21671
+ await handOffAtCycleCap(deps, resumeCandidate, capHolder, "\u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u0438\u0437 \u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0438");
21672
+ return;
21673
+ }
21473
21674
  const wt = resumeCandidate.worktree_path;
21474
21675
  const canContinue = !!wt && worktreeAndTranscriptAlive(deps, resumeCandidate);
21475
21676
  const mode = canContinue ? "continue" : "fresh";
@@ -21890,6 +22091,8 @@ async function answerBudgetQuestions(deps, ctx) {
21890
22091
  respawn: deps.respawn,
21891
22092
  ...deps.worktreeExists ? { worktreeExists: deps.worktreeExists } : {},
21892
22093
  ...deps.hasTranscript ? { hasTranscript: deps.hasTranscript } : {},
22094
+ ...deps.prComment ? { prComment: deps.prComment } : {},
22095
+ ...deps.reportComment ? { reportComment: deps.reportComment } : {},
21893
22096
  narrate: (m) => narrate(deps, m)
21894
22097
  },
21895
22098
  run2.id,
@@ -21899,6 +22102,11 @@ async function answerBudgetQuestions(deps, ctx) {
21899
22102
  budgetSyncAt.delete(run2.id);
21900
22103
  continue;
21901
22104
  }
22105
+ if (reviewCyclesExhausted(run2) && run2.pr_url) {
22106
+ deps.db.updateRun(run2.id, { status: "review", stop_reason: null });
22107
+ budgetSyncAt.delete(run2.id);
22108
+ continue;
22109
+ }
21902
22110
  const note = `\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u043F\u043E \u043E\u0442\u0432\u0435\u0442\u0443 \xAB\u0434\u0430\xBB \u043D\u0435 \u0432\u044B\u0448\u043B\u0430: ${woken.why} \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u044E`;
21903
22111
  if (deps.db.events(run2.id).at(-1)?.text !== note) deps.db.addEvent(run2.id, "step", note);
21904
22112
  }
@@ -22284,11 +22492,8 @@ async function tickOnce(deps) {
22284
22492
  // рано, спавнить второго нельзя.
22285
22493
  liveOrWaitingReviewers(deps.db, run2.id).length === 0;
22286
22494
  if (reviewerGatesOk && deps.spawnReviewer && await projectWantsChecks(deps, run2)) {
22287
- if (run2.review_cycle >= REVIEW_CYCLE_LIMIT) {
22288
- if (!deps.db.events(run2.id).some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_CYCLE_ESCALATION_MARK))) {
22289
- deps.db.addEvent(run2.id, "step", `${REVIEW_CYCLE_ESCALATION_MARK}: ${run2.review_cycle} \u0446\u0438\u043A\u043B(\u043E\u0432) \u0440\u0435\u0432\u044C\u044E-\u0444\u0438\u043A\u0441\u043E\u0432 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
22290
- narrate(deps, `\u0420\u0435-\u0440\u0435\u0432\u044C\u044E ${droneLabel(run2)} \u0437\u0430\u0446\u0438\u043A\u043B\u0438\u043B\u043E\u0441\u044C (${run2.review_cycle} \u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0439) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`);
22291
- }
22495
+ if (reviewCyclesExhausted(run2)) {
22496
+ await noteReviewCyclesExhausted(deps, run2);
22292
22497
  continue;
22293
22498
  }
22294
22499
  const attempts = deps.db.reviewerAttemptCountFor(run2.id);
@@ -22444,6 +22649,14 @@ async function tickOnce(deps) {
22444
22649
  continue;
22445
22650
  }
22446
22651
  }
22652
+ if (!alive && run2.status === "running" && !isSubrun(run2) && run2.kind !== "hero") {
22653
+ const capHolder = cycleCapHolder(deps.db, run2);
22654
+ if (capHolder) {
22655
+ await handOffAtCycleCap(deps, run2, capHolder, `\u0441\u0435\u0441\u0441\u0438\u044F \u0430\u0432\u0442\u043E\u0440\u0430 \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u0430\u0441\u044C (${outcome})`);
22656
+ narrate(deps, `${droneLabel(run2)} \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u043B\u0438\u043C\u0438\u0442 \u043A\u0440\u0443\u0433\u043E\u0432 \u043F\u0440\u0430\u0432\u043E\u043A \u2014 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E, PR \u043E\u0441\u0442\u0430\u0451\u0442\u0441\u044F \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443.`);
22657
+ continue;
22658
+ }
22659
+ }
22447
22660
  if (!alive && (outcome === "graceful" || outcome === "aborted") && run2.status === "running" && !isSubrun(run2) && // суб-дроны (резолвер/ревьюер/верификатор) — своими ветками ниже, TASWM-528
22448
22661
  run2.kind !== "hero") {
22449
22662
  if (!wasAborted(outcome) && run2.pr_url) {
@@ -22549,6 +22762,11 @@ async function tickOnce(deps) {
22549
22762
  }
22550
22763
  }
22551
22764
  if (!alive && outcome === "graceful" && run2.status === "running" && isSubrun(run2)) {
22765
+ const capHolder = cycleCapHolder(deps.db, run2);
22766
+ if (capHolder) {
22767
+ await handOffAtCycleCap(deps, run2, capHolder, "\u0441\u0443\u0431-\u0434\u0440\u043E\u043D \u0432\u044B\u0448\u0435\u043B \u0441\u0430\u043C, \u043D\u0435 \u0432\u044B\u043D\u0435\u0441\u044F \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430");
22768
+ continue;
22769
+ }
22552
22770
  const wt = run2.worktree_path;
22553
22771
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(run2);
22554
22772
  if (deps.respawn && canContinue && run2.respawns < RESPAWN_LIMIT) {
@@ -22624,6 +22842,11 @@ async function tickOnce(deps) {
22624
22842
  if (isReliefPause(run2.stop_reason)) continue;
22625
22843
  if (NO_AUTO_WAKE_REASONS.some((r) => run2.stop_reason?.startsWith(r))) continue;
22626
22844
  if (run2.pid != null && deps.isAlive(run2.pid)) continue;
22845
+ const capHolder = cycleCapHolder(deps.db, run2);
22846
+ if (capHolder) {
22847
+ await handOffAtCycleCap(deps, run2, capHolder, "\u043F\u0440\u043E\u0431\u0443\u0436\u0434\u0435\u043D\u0438\u0435 \u0437\u0430\u043C\u0435\u0440\u0448\u0435\u0433\u043E");
22848
+ continue;
22849
+ }
22627
22850
  if (!await projectQuotaGateAllows(deps, run2)) continue;
22628
22851
  const wt = run2.worktree_path;
22629
22852
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(run2);
@@ -23386,9 +23609,8 @@ async function runDaemon(opts) {
23386
23609
  recordModelRef: (runId, ref, run2) => {
23387
23610
  if (runModelRefPin.size > 4096) runModelRefPin.clear();
23388
23611
  runModelRefPin.set(runId, ref);
23389
- if (run2 && !run2.model && !isReviewKind(run2.kind)) {
23390
- db.updateRun(runId, { model: `${ref.providerId}/${ref.model}` });
23391
- }
23612
+ const model = modelRefToRecord(run2, ref);
23613
+ if (model) db.updateRun(runId, { model });
23392
23614
  },
23393
23615
  maxCostUsd: () => lastMaxCostUsd,
23394
23616
  // TASWM-535: поднятый потолок тяжёлого прогона живёт в его ленте — спавн читает её
@@ -23574,6 +23796,7 @@ async function runDaemon(opts) {
23574
23796
  lastSwarmModels = settings.models;
23575
23797
  lastSwarmEfforts = settings.efforts;
23576
23798
  lastMaxCostUsd = settings.maxCostUsd;
23799
+ configureReviewCycleLimit(settings.reviewCycleLimit);
23577
23800
  }
23578
23801
  return settings?.maxDrones ?? null;
23579
23802
  },
@@ -24307,7 +24530,18 @@ async function runDaemon(opts) {
24307
24530
  wakeRun: async (slug) => {
24308
24531
  const run2 = db.activeRunForSlug(slug);
24309
24532
  if (!run2) return null;
24310
- return wakeRun({ db, isAlive: pidAlive, respawn: deps.respawn, narrate: narrator.narrate, ...deps.hasTranscript ? { hasTranscript: deps.hasTranscript } : {} }, run2.id);
24533
+ return wakeRun(
24534
+ {
24535
+ db,
24536
+ isAlive: pidAlive,
24537
+ respawn: deps.respawn,
24538
+ narrate: narrator.narrate,
24539
+ ...deps.hasTranscript ? { hasTranscript: deps.hasTranscript } : {},
24540
+ ...deps.prComment ? { prComment: deps.prComment } : {},
24541
+ ...deps.reportComment ? { reportComment: deps.reportComment } : {}
24542
+ },
24543
+ run2.id
24544
+ );
24311
24545
  },
24312
24546
  update: async (onStep) => {
24313
24547
  const pm = detectGlobalPm(process.argv[1]);
@@ -24457,7 +24691,18 @@ async function runDaemon(opts) {
24457
24691
  // Побудка прогона по id (TASWM-240): вход для `swarm wake <id>` — тот же respawn с
24458
24692
  // полными deps демона (getTask/verifyMode/реестр репо), что и авто-оживление, и по
24459
24693
  // зеркалу демона, а не по короткоживущему стору CLI.
24460
- wake: (runId) => wakeRun({ db, isAlive: pidAlive, respawn: deps.respawn, narrate: narrator.narrate, ...deps.hasTranscript ? { hasTranscript: deps.hasTranscript } : {} }, runId),
24694
+ wake: (runId) => wakeRun(
24695
+ {
24696
+ db,
24697
+ isAlive: pidAlive,
24698
+ respawn: deps.respawn,
24699
+ narrate: narrator.narrate,
24700
+ ...deps.hasTranscript ? { hasTranscript: deps.hasTranscript } : {},
24701
+ ...deps.prComment ? { prComment: deps.prComment } : {},
24702
+ ...deps.reportComment ? { reportComment: deps.reportComment } : {}
24703
+ },
24704
+ runId
24705
+ ),
24461
24706
  replay: (qid, chosen) => replayDecision(
24462
24707
  {
24463
24708
  db,
@@ -24592,6 +24837,7 @@ export {
24592
24837
  CI_WAKE_LIMIT,
24593
24838
  CONFLICT_RESOLVE_COOLDOWN_MS,
24594
24839
  CONTINUE_NUDGE,
24840
+ CYCLE_CAP_STOP_REASON,
24595
24841
  DEAD_HOLDER_SILENCE_MS,
24596
24842
  EXITED_CONTINUE_NUDGE,
24597
24843
  EXITED_NO_DONE_REASON,
@@ -24637,12 +24883,17 @@ export {
24637
24883
  buildResolverPrompt,
24638
24884
  buildReviewChecklist,
24639
24885
  buildReviewerPrompt,
24886
+ buildSubmittedContinueNudge,
24640
24887
  buildVerifierPrompt,
24641
24888
  cancelRun,
24642
24889
  capacityProvenance,
24890
+ configureReviewCycleLimit,
24891
+ cycleCapHolder,
24892
+ effectiveReviewCycleLimit,
24643
24893
  hasLiveDescendant,
24644
24894
  isSubrun,
24645
24895
  lastChangesSummary,
24896
+ lastVerifySummary,
24646
24897
  listDroneProcesses,
24647
24898
  livePpids,
24648
24899
  makeCheckerAttachmentsReader,
@@ -24651,6 +24902,8 @@ export {
24651
24902
  resolveConflict,
24652
24903
  respawnRun,
24653
24904
  reviewBudgetMark,
24905
+ reviewCycleHumanBody,
24906
+ reviewCyclesExhausted,
24654
24907
  reviewDefectClasses,
24655
24908
  reviewUnrecoverableMark,
24656
24909
  reviveStuck,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.65",
3
+ "version": "0.6.67",
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": [
@@ -39,7 +39,7 @@
39
39
  "execa": "^10.0.0",
40
40
  "zod": "3.25.76"
41
41
  },
42
- "gitHead": "254a8954d2f53f168b6cebc23b2daa0bc1562426",
42
+ "gitHead": "92a3176867bc1668677a4b6a25e02e0cb6661ce0",
43
43
  "scripts": {
44
44
  "overmind": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/overmind.ts",
45
45
  "swarm": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/cli.ts",