@taskless-app/swarm 0.6.66 → 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
@@ -1789,7 +1790,7 @@ var init_effort = __esm({
1789
1790
  });
1790
1791
 
1791
1792
  // ../swarm-core/src/queuePlan.ts
1792
- 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;
1793
1794
  var init_queuePlan = __esm({
1794
1795
  "../swarm-core/src/queuePlan.ts"() {
1795
1796
  "use strict";
@@ -1802,6 +1803,9 @@ var init_queuePlan = __esm({
1802
1803
  MIN_MS = 6e4;
1803
1804
  QUEUE_NOMINAL_RUN_MS = 25 * MIN_MS;
1804
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;
1805
1809
  }
1806
1810
  });
1807
1811
 
@@ -3876,7 +3880,7 @@ var init_package = __esm({
3876
3880
  "package.json"() {
3877
3881
  package_default = {
3878
3882
  name: "@taskless-app/swarm",
3879
- version: "0.6.66",
3883
+ version: "0.6.67",
3880
3884
  type: "module",
3881
3885
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
3882
3886
  keywords: [
@@ -3927,7 +3931,7 @@ var init_package = __esm({
3927
3931
  execa: "^10.0.0",
3928
3932
  zod: "3.25.76"
3929
3933
  },
3930
- gitHead: "a6bc0ea303cb5336fd4e130fba73d90365268b6f"
3934
+ gitHead: "92a3176867bc1668677a4b6a25e02e0cb6661ce0"
3931
3935
  };
3932
3936
  }
3933
3937
  });
@@ -14854,10 +14858,11 @@ switch (cmd) {
14854
14858
  // TASWM-592 (ревью): сессию ищет харнесс ПРОГОНА, а не глобальный исполнитель —
14855
14859
  // иначе побудка codex-прогона при мёртвом демоне теряла продолжаемую сессию.
14856
14860
  hasTranscript: (run2) => !!run2.worktree_path && executorForRun(run2, null).session.exists(run2.worktree_path),
14857
- respawn: (r, mode) => respawnRun(
14861
+ respawn: (r, mode, nudge) => respawnRun(
14858
14862
  { db, launch, repoFallback: SWARM_REPO, worktreesDir: effectiveWorktreesDir(), getTask: () => Promise.resolve(null) },
14859
14863
  r,
14860
- mode
14864
+ mode,
14865
+ nudge
14861
14866
  )
14862
14867
  },
14863
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
@@ -2020,7 +2024,7 @@ var init_effort = __esm({
2020
2024
  });
2021
2025
 
2022
2026
  // ../swarm-core/src/queuePlan.ts
2023
- 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;
2024
2028
  var init_queuePlan = __esm({
2025
2029
  "../swarm-core/src/queuePlan.ts"() {
2026
2030
  "use strict";
@@ -2033,6 +2037,9 @@ var init_queuePlan = __esm({
2033
2037
  MIN_MS = 6e4;
2034
2038
  QUEUE_NOMINAL_RUN_MS = 25 * MIN_MS;
2035
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;
2036
2043
  }
2037
2044
  });
2038
2045
 
@@ -13067,7 +13074,7 @@ function parkReasonUnretriable(reason) {
13067
13074
  // package.json
13068
13075
  var package_default = {
13069
13076
  name: "@taskless-app/swarm",
13070
- version: "0.6.66",
13077
+ version: "0.6.67",
13071
13078
  type: "module",
13072
13079
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
13073
13080
  keywords: [
@@ -13118,7 +13125,7 @@ var package_default = {
13118
13125
  execa: "^10.0.0",
13119
13126
  zod: "3.25.76"
13120
13127
  },
13121
- gitHead: "a6bc0ea303cb5336fd4e130fba73d90365268b6f"
13128
+ gitHead: "92a3176867bc1668677a4b6a25e02e0cb6661ce0"
13122
13129
  };
13123
13130
 
13124
13131
  // src/version.ts
@@ -14127,12 +14134,36 @@ init_src();
14127
14134
 
14128
14135
  // src/reviewCycle.ts
14129
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
+ }
14130
14158
  function needsFixIteration(run2) {
14131
14159
  return run2.review_verdict === "changes" || isNegativeVerifyVerdict(toVerifyVerdict(run2.verify_verdict));
14132
14160
  }
14133
14161
  function reviewTransitionPatch(run2, prUrl) {
14134
14162
  const isFixIteration = needsFixIteration(run2);
14135
14163
  if (isFixIteration) {
14164
+ if (reviewCyclesExhausted(run2)) {
14165
+ return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
14166
+ }
14136
14167
  const cycle = (run2.review_cycle ?? 0) + 1;
14137
14168
  return {
14138
14169
  isFixIteration: true,
@@ -19133,14 +19164,86 @@ var BASE_MERGED_MARK = "\u0431\u0430\u0437\u0430 \u0432\u043B\u0438\u0442\u0430
19133
19164
  var BASE_NOOP_MARK = "\u0432\u043B\u0438\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E";
19134
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";
19135
19166
  var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
19136
- var REVIEW_CYCLE_LIMIT = Number(process.env.SWARM_REVIEW_CYCLE_LIMIT) || 3;
19137
- var REVIEW_CYCLE_LIMIT_CAP = 20;
19138
- var configuredReviewCycleLimit = null;
19139
- function configureReviewCycleLimit(n) {
19140
- configuredReviewCycleLimit = n != null && Number.isInteger(n) && n >= 1 && n <= REVIEW_CYCLE_LIMIT_CAP ? n : null;
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;
19141
19182
  }
19142
- function effectiveReviewCycleLimit() {
19143
- return configuredReviewCycleLimit ?? REVIEW_CYCLE_LIMIT;
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);
19144
19247
  }
19145
19248
  var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
19146
19249
  var CI_WAKE_COOLDOWN_MS = Number(process.env.SWARM_CI_WAKE_COOLDOWN_MS) || 3 * 60 * 1e3;
@@ -19168,7 +19271,7 @@ var CHANGES_WAKE_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430
19168
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";
19169
19272
  var CHANGES_WAKE_FAILED_MARK = "\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C";
19170
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";
19171
- 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];
19172
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)";
19173
19276
  function verdictCommentMark(createdAt) {
19174
19277
  return `\u043A\u043E\u043C\u043C\u0435\u043D\u0442-\u0432\u0435\u0440\u0434\u0438\u043A\u0442@${createdAt}`;
@@ -19331,7 +19434,9 @@ function reviewPhaseInputFor(deps, run2, projectWantsVerify = true) {
19331
19434
  reviewerDead,
19332
19435
  verifyVerdict: run2.verify_verdict,
19333
19436
  verifyExpected,
19334
- verifierDead
19437
+ verifierDead,
19438
+ // TASWM-606: потолок кругов важнее «чинит CI» и «ждёт ревьюера» — человека уже позвали.
19439
+ cyclesExhausted: reviewCyclesExhausted(run2)
19335
19440
  };
19336
19441
  }
19337
19442
  async function refreshReviewPhases(deps) {
@@ -19715,6 +19820,10 @@ async function spawnVerifiersForOpenPrs(deps, stormActive) {
19715
19820
  if (run2.kind !== "drone") continue;
19716
19821
  if (run2.reviews_run_id != null || run2.resolves_run_id != null || run2.verifies_run_id != null) continue;
19717
19822
  if (!run2.pr_url || !isGithubPrUrl(run2.pr_url)) continue;
19823
+ if (reviewCyclesExhausted(run2)) {
19824
+ await noteReviewCyclesExhausted(deps, run2);
19825
+ continue;
19826
+ }
19718
19827
  if (run2.verify_verdict != null) continue;
19719
19828
  if (deps.db.verifierCountFor(run2.id) > 0) continue;
19720
19829
  const attempts = deps.db.verifierAttemptCountFor(run2.id);
@@ -19740,6 +19849,18 @@ async function spawnVerifiersForOpenPrs(deps, stormActive) {
19740
19849
  }
19741
19850
  }
19742
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
+ }
19743
19864
  function buildCiWakeNudge(summary) {
19744
19865
  return [
19745
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):",
@@ -20061,6 +20182,15 @@ function lastVerdictEvent(db, reviewRunId) {
20061
20182
  function lastChangesSummary(db, reviewRunId) {
20062
20183
  return lastVerdictEvent(db, reviewRunId)?.text ?? null;
20063
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
+ }
20064
20194
  function changesVerdictAt(db, reviewRunId) {
20065
20195
  return lastVerdictEvent(db, reviewRunId)?.at ?? null;
20066
20196
  }
@@ -20240,6 +20370,11 @@ async function resurrectStormDead(deps, worktreeExists, hasTranscript, opts) {
20240
20370
  let resurrected = 0;
20241
20371
  for (const run2 of deps.db.listRuns("storm_dead")) {
20242
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
+ }
20243
20378
  if (!await projectQuotaGateAllows(deps, run2)) continue;
20244
20379
  const wt = run2.worktree_path;
20245
20380
  const mode = !!wt && worktreeExists(wt) && hasTranscript(run2) ? "continue" : "fresh";
@@ -20263,6 +20398,16 @@ async function wakeRun(deps, runId, opts = {}) {
20263
20398
  }
20264
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` };
20265
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
+ }
20266
20411
  const stoppedForHuman = deps.db.events(run2.id).some((e) => isEscalationText(e.text ?? "", run2, "author"));
20267
20412
  if (run2.status === "review" && run2.pr_url && !needsFixIteration(run2) && !stoppedForHuman) {
20268
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` };
@@ -20276,8 +20421,11 @@ async function wakeRun(deps, runId, opts = {}) {
20276
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` };
20277
20422
  }
20278
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;
20279
20427
  try {
20280
- await deps.respawn(run2, mode);
20428
+ await deps.respawn(run2, mode, nudge);
20281
20429
  } catch (e) {
20282
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)}` };
20283
20431
  }
@@ -20469,6 +20617,10 @@ function closeCiWatch(deps, run2, mark, why, narration) {
20469
20617
  }
20470
20618
  async function wakeAuthorOnRedCi(deps, run2, now, ci, worktreeExists, hasTranscript, droneCap) {
20471
20619
  if (run2.pid != null && deps.isAlive(run2.pid)) return false;
20620
+ if (reviewCyclesExhausted(run2)) {
20621
+ await noteReviewCyclesExhausted(deps, run2);
20622
+ return false;
20623
+ }
20472
20624
  if (deps.resourcesOk && !deps.resourcesOk()) return false;
20473
20625
  if (countActiveDrones(deps.db) >= droneCap) return false;
20474
20626
  if (!await projectQuotaGateAllows(deps, run2)) return false;
@@ -20538,6 +20690,15 @@ async function wakeReviewerOnCiReady(deps, run2, over) {
20538
20690
  const reviewer = awaitingCiReviewer(deps.db, run2);
20539
20691
  if (!reviewer) return false;
20540
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
+ }
20541
20702
  if (deps.resourcesOk && !deps.resourcesOk()) {
20542
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`);
20543
20704
  return true;
@@ -20612,7 +20773,8 @@ async function spawnFreshReviewerAfterStall(deps, run2, why) {
20612
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})`);
20613
20774
  return;
20614
20775
  }
20615
- if (run2.review_cycle >= effectiveReviewCycleLimit() || 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);
20616
20778
  return;
20617
20779
  }
20618
20780
  const gate = await prGateForReviewer(deps, run2, run2.pr_url, "\u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B CI");
@@ -20636,7 +20798,10 @@ async function wakeAuthorOnChanges(deps, snapshot, now, droneCap) {
20636
20798
  if (deps.resourcesOk && !deps.resourcesOk()) return false;
20637
20799
  if (countActiveDrones(deps.db) >= droneCap) return false;
20638
20800
  if (!await projectQuotaGateAllows(deps, run2)) return false;
20639
- if (run2.review_cycle >= effectiveReviewCycleLimit()) return false;
20801
+ if (reviewCyclesExhausted(run2)) {
20802
+ await noteReviewCyclesExhausted(deps, run2);
20803
+ return false;
20804
+ }
20640
20805
  const prUrl = run2.pr_url;
20641
20806
  const events = deps.db.events(run2.id);
20642
20807
  const mark = `${CHANGES_WAKE_ESCALATION_MARK} (\u043A\u0440\u0443\u0433 ${run2.review_cycle ?? 0})`;
@@ -21189,6 +21354,11 @@ async function resumeWatchdogParked(deps, droneCap) {
21189
21354
  }
21190
21355
  if (!candidate) return;
21191
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
+ }
21192
21362
  const worktreeExists = deps.worktreeExists ?? existsSync22;
21193
21363
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
21194
21364
  const wt = candidate.worktree_path;
@@ -21248,6 +21418,11 @@ async function applyWatchdogAction(deps, action) {
21248
21418
  }
21249
21419
  case "fresh": {
21250
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
+ }
21251
21426
  try {
21252
21427
  await deps.respawn(action.run, "fresh", buildFreshRestartNudge(action.run));
21253
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}`);
@@ -21491,6 +21666,11 @@ async function manageRelief(deps, now, droneCap, stormActive) {
21491
21666
  }
21492
21667
  if (now - loopEntry.lastPauseAt < reliefBackoffMs(loopEntry.pauses, cfg.rampWindowMs, loopCfg)) return;
21493
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
+ }
21494
21674
  const wt = resumeCandidate.worktree_path;
21495
21675
  const canContinue = !!wt && worktreeAndTranscriptAlive(deps, resumeCandidate);
21496
21676
  const mode = canContinue ? "continue" : "fresh";
@@ -21911,6 +22091,8 @@ async function answerBudgetQuestions(deps, ctx) {
21911
22091
  respawn: deps.respawn,
21912
22092
  ...deps.worktreeExists ? { worktreeExists: deps.worktreeExists } : {},
21913
22093
  ...deps.hasTranscript ? { hasTranscript: deps.hasTranscript } : {},
22094
+ ...deps.prComment ? { prComment: deps.prComment } : {},
22095
+ ...deps.reportComment ? { reportComment: deps.reportComment } : {},
21914
22096
  narrate: (m) => narrate(deps, m)
21915
22097
  },
21916
22098
  run2.id,
@@ -21920,6 +22102,11 @@ async function answerBudgetQuestions(deps, ctx) {
21920
22102
  budgetSyncAt.delete(run2.id);
21921
22103
  continue;
21922
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
+ }
21923
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`;
21924
22111
  if (deps.db.events(run2.id).at(-1)?.text !== note) deps.db.addEvent(run2.id, "step", note);
21925
22112
  }
@@ -22305,11 +22492,8 @@ async function tickOnce(deps) {
22305
22492
  // рано, спавнить второго нельзя.
22306
22493
  liveOrWaitingReviewers(deps.db, run2.id).length === 0;
22307
22494
  if (reviewerGatesOk && deps.spawnReviewer && await projectWantsChecks(deps, run2)) {
22308
- if (run2.review_cycle >= effectiveReviewCycleLimit()) {
22309
- if (!deps.db.events(run2.id).some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_CYCLE_ESCALATION_MARK))) {
22310
- 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`);
22311
- 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.`);
22312
- }
22495
+ if (reviewCyclesExhausted(run2)) {
22496
+ await noteReviewCyclesExhausted(deps, run2);
22313
22497
  continue;
22314
22498
  }
22315
22499
  const attempts = deps.db.reviewerAttemptCountFor(run2.id);
@@ -22465,6 +22649,14 @@ async function tickOnce(deps) {
22465
22649
  continue;
22466
22650
  }
22467
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
+ }
22468
22660
  if (!alive && (outcome === "graceful" || outcome === "aborted") && run2.status === "running" && !isSubrun(run2) && // суб-дроны (резолвер/ревьюер/верификатор) — своими ветками ниже, TASWM-528
22469
22661
  run2.kind !== "hero") {
22470
22662
  if (!wasAborted(outcome) && run2.pr_url) {
@@ -22570,6 +22762,11 @@ async function tickOnce(deps) {
22570
22762
  }
22571
22763
  }
22572
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
+ }
22573
22770
  const wt = run2.worktree_path;
22574
22771
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(run2);
22575
22772
  if (deps.respawn && canContinue && run2.respawns < RESPAWN_LIMIT) {
@@ -22645,6 +22842,11 @@ async function tickOnce(deps) {
22645
22842
  if (isReliefPause(run2.stop_reason)) continue;
22646
22843
  if (NO_AUTO_WAKE_REASONS.some((r) => run2.stop_reason?.startsWith(r))) continue;
22647
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
+ }
22648
22850
  if (!await projectQuotaGateAllows(deps, run2)) continue;
22649
22851
  const wt = run2.worktree_path;
22650
22852
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(run2);
@@ -24328,7 +24530,18 @@ async function runDaemon(opts) {
24328
24530
  wakeRun: async (slug) => {
24329
24531
  const run2 = db.activeRunForSlug(slug);
24330
24532
  if (!run2) return null;
24331
- 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
+ );
24332
24545
  },
24333
24546
  update: async (onStep) => {
24334
24547
  const pm = detectGlobalPm(process.argv[1]);
@@ -24478,7 +24691,18 @@ async function runDaemon(opts) {
24478
24691
  // Побудка прогона по id (TASWM-240): вход для `swarm wake <id>` — тот же respawn с
24479
24692
  // полными deps демона (getTask/verifyMode/реестр репо), что и авто-оживление, и по
24480
24693
  // зеркалу демона, а не по короткоживущему стору CLI.
24481
- 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
+ ),
24482
24706
  replay: (qid, chosen) => replayDecision(
24483
24707
  {
24484
24708
  db,
@@ -24613,6 +24837,7 @@ export {
24613
24837
  CI_WAKE_LIMIT,
24614
24838
  CONFLICT_RESOLVE_COOLDOWN_MS,
24615
24839
  CONTINUE_NUDGE,
24840
+ CYCLE_CAP_STOP_REASON,
24616
24841
  DEAD_HOLDER_SILENCE_MS,
24617
24842
  EXITED_CONTINUE_NUDGE,
24618
24843
  EXITED_NO_DONE_REASON,
@@ -24658,14 +24883,17 @@ export {
24658
24883
  buildResolverPrompt,
24659
24884
  buildReviewChecklist,
24660
24885
  buildReviewerPrompt,
24886
+ buildSubmittedContinueNudge,
24661
24887
  buildVerifierPrompt,
24662
24888
  cancelRun,
24663
24889
  capacityProvenance,
24664
24890
  configureReviewCycleLimit,
24891
+ cycleCapHolder,
24665
24892
  effectiveReviewCycleLimit,
24666
24893
  hasLiveDescendant,
24667
24894
  isSubrun,
24668
24895
  lastChangesSummary,
24896
+ lastVerifySummary,
24669
24897
  listDroneProcesses,
24670
24898
  livePpids,
24671
24899
  makeCheckerAttachmentsReader,
@@ -24674,6 +24902,8 @@ export {
24674
24902
  resolveConflict,
24675
24903
  respawnRun,
24676
24904
  reviewBudgetMark,
24905
+ reviewCycleHumanBody,
24906
+ reviewCyclesExhausted,
24677
24907
  reviewDefectClasses,
24678
24908
  reviewUnrecoverableMark,
24679
24909
  reviveStuck,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.66",
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": "a6bc0ea303cb5336fd4e130fba73d90365268b6f",
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",