@taskless-app/swarm 0.6.66 → 0.6.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +211 -43
  2. package/dist/overmind.js +550 -63
  3. package/package.json +3 -2
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
 
@@ -4085,7 +4092,7 @@ var init_linkFailure = __esm({
4085
4092
  "use strict";
4086
4093
  init_agentToken();
4087
4094
  UNKNOWN_LINK_REASON = "\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F \u043D\u0435\u0442, \u0430 \u043F\u0440\u0438\u0447\u0438\u043D\u0443 \u043A\u043E\u043D\u043D\u0435\u043A\u0442\u043E\u0440 \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043B";
4088
- OFFLINE_RE = /fetch failed|econnreset|econnrefused|econnaborted|enotfound|eai_again|etimedout|ehostunreach|enetunreach|epipe|socket hang up|und_err|network error|connection (?:closed|refused|reset|error)|connect timeout/i;
4095
+ OFFLINE_RE = /fetch failed|econnreset|econnrefused|econnaborted|enotfound|eai_again|etimedout|ehostunreach|enetunreach|epipe|socket hang up|und_err|network error|connection (?:closed|refused|reset|error)|connect timeout|request timed out|-32001\b/i;
4089
4096
  MISSING_TOOL_RE = /tool[^.]{0,40}not found|unknown tool|tool_not_found|method not found|-32601\b/i;
4090
4097
  }
4091
4098
  });
@@ -6017,6 +6024,47 @@ var init_oauth = __esm({
6017
6024
  }
6018
6025
  });
6019
6026
 
6027
+ // src/prodHttp.ts
6028
+ import { Agent, fetch as undiciFetch } from "undici";
6029
+ function prodDispatcher() {
6030
+ agent ??= new Agent({
6031
+ keepAliveTimeout: PROD_KEEPALIVE_MS,
6032
+ keepAliveMaxTimeout: PROD_KEEPALIVE_MS,
6033
+ headersTimeout: PROD_SOCKET_TIMEOUT_MS,
6034
+ bodyTimeout: PROD_SOCKET_TIMEOUT_MS,
6035
+ connect: { timeout: PROD_CONNECT_TIMEOUT_MS }
6036
+ });
6037
+ return agent;
6038
+ }
6039
+ function isProdTimeout(e) {
6040
+ for (let cur = e, depth = 0; cur != null && depth < 4; cur = cur.cause, depth++) {
6041
+ const code = cur.code;
6042
+ const name = cur.name;
6043
+ const msg = cur instanceof Error ? cur.message : String(cur);
6044
+ if (code === -32001 || code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT") return true;
6045
+ if (name === "TimeoutError" || /request timed out/i.test(msg)) return true;
6046
+ }
6047
+ return false;
6048
+ }
6049
+ var PROD_CALL_TIMEOUT_MS, FETCH_GRACE_MS, PROD_KEEPALIVE_MS, PROD_SOCKET_TIMEOUT_MS, PROD_CONNECT_TIMEOUT_MS, agent, prodFetch;
6050
+ var init_prodHttp = __esm({
6051
+ "src/prodHttp.ts"() {
6052
+ "use strict";
6053
+ PROD_CALL_TIMEOUT_MS = 15e3;
6054
+ FETCH_GRACE_MS = 5e3;
6055
+ PROD_KEEPALIVE_MS = 1e4;
6056
+ PROD_SOCKET_TIMEOUT_MS = 3e4;
6057
+ PROD_CONNECT_TIMEOUT_MS = 1e4;
6058
+ agent = null;
6059
+ prodFetch = (url, init) => {
6060
+ const method = (init?.method ?? "GET").toUpperCase();
6061
+ const deadline = method === "GET" ? null : AbortSignal.timeout(PROD_CALL_TIMEOUT_MS + FETCH_GRACE_MS);
6062
+ const signal = deadline && init?.signal ? AbortSignal.any([init.signal, deadline]) : deadline ?? init?.signal ?? void 0;
6063
+ return undiciFetch(url, { ...init, ...signal ? { signal } : {}, dispatcher: prodDispatcher() });
6064
+ };
6065
+ }
6066
+ });
6067
+
6020
6068
  // src/authState.ts
6021
6069
  function noteTokenRejected(nowMs = Date.now()) {
6022
6070
  if (!denial) {
@@ -6505,8 +6553,8 @@ function escapeRegExp(s) {
6505
6553
  }
6506
6554
  async function resolveApiToken(api, explicit) {
6507
6555
  if (explicit) return explicit;
6508
- const agent = loadAgentToken();
6509
- if (agent?.token) return agent.token;
6556
+ const agent2 = loadAgentToken();
6557
+ if (agent2?.token) return agent2.token;
6510
6558
  const store = loadStore();
6511
6559
  if (store) {
6512
6560
  const r = await ensureAccess(store, { api });
@@ -6531,9 +6579,9 @@ function resetLegacyTokenWarning() {
6531
6579
  async function preflightAuth(opts = {}) {
6532
6580
  const api = opts.api ?? TASKLESS_API;
6533
6581
  const out = opts.out ?? ((s) => console.error(s));
6534
- const agent = loadAgentToken();
6535
- if (!agent?.token) return true;
6536
- const probe = await probeAgentToken({ api, token: agent.token }, opts.fetchFn ?? fetch);
6582
+ const agent2 = loadAgentToken();
6583
+ if (!agent2?.token) return true;
6584
+ const probe = await probeAgentToken({ api, token: agent2.token }, opts.fetchFn ?? fetch);
6537
6585
  if (probe === "revoked") {
6538
6586
  out(noteTokenRejected() ?? revokedTokenComplaint());
6539
6587
  return false;
@@ -6553,7 +6601,8 @@ async function connectTaskless(cfg) {
6553
6601
  if (!token) return null;
6554
6602
  const url = new URL(mcpEndpoint(api));
6555
6603
  const transport = new StreamableHTTPClientTransport(url, {
6556
- requestInit: { headers: { Authorization: `Bearer ${token}` } }
6604
+ requestInit: { headers: { Authorization: `Bearer ${token}` } },
6605
+ fetch: prodFetch
6557
6606
  });
6558
6607
  const client = new Client({ name: "taskless-swarm", version: "0.1.0" }, { capabilities: {} });
6559
6608
  try {
@@ -6568,7 +6617,14 @@ async function connectTaskless(cfg) {
6568
6617
  return null;
6569
6618
  }
6570
6619
  noteTokenAccepted();
6571
- const call = async (name, args) => await client.callTool({ name, arguments: args });
6620
+ const call = async (name, args) => {
6621
+ try {
6622
+ return await client.callTool({ name, arguments: args }, void 0, { timeout: PROD_CALL_TIMEOUT_MS });
6623
+ } catch (e) {
6624
+ if (isProdTimeout(e)) console.warn(prodTimeoutLine("Taskless", name, PROD_CALL_TIMEOUT_MS));
6625
+ throw e;
6626
+ }
6627
+ };
6572
6628
  return makeTasklessClient(
6573
6629
  call,
6574
6630
  workspace || void 0,
@@ -6586,7 +6642,9 @@ async function connectSwarmMcp(cfg) {
6586
6642
  }
6587
6643
  const url = new URL(mcpEndpoint(api));
6588
6644
  const transport = new StreamableHTTPClientTransport(url, {
6589
- requestInit: { headers: { Authorization: `Bearer ${token}` } }
6645
+ requestInit: { headers: { Authorization: `Bearer ${token}` } },
6646
+ fetch: prodFetch
6647
+ // TASWM-611
6590
6648
  });
6591
6649
  const client = new Client({ name: "taskless-swarm-client", version: "0.1.0" }, { capabilities: {} });
6592
6650
  try {
@@ -6605,12 +6663,31 @@ async function connectSwarmMcp(cfg) {
6605
6663
  }
6606
6664
  swarmConnectFailure = null;
6607
6665
  noteTokenAccepted();
6608
- return async (name, args) => await client.callTool({ name, arguments: swarmToolArgs(name, args, workspace) });
6666
+ const timeout = cfg?.callTimeoutMs ?? PROD_CALL_TIMEOUT_MS;
6667
+ return async (name, args) => {
6668
+ try {
6669
+ return await client.callTool({ name, arguments: swarmToolArgs(name, args, workspace) }, void 0, { timeout });
6670
+ } catch (e) {
6671
+ if (isProdTimeout(e)) {
6672
+ console.warn(`${prodTimeoutLine("Swarm MCP", name, timeout)} \u2014 \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u0441\u0431\u0440\u043E\u0448\u0435\u043D\u043E, \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0432\u044B\u0437\u043E\u0432 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u0441\u044F`);
6673
+ }
6674
+ void client.close().catch(() => {
6675
+ });
6676
+ throw e;
6677
+ }
6678
+ };
6679
+ }
6680
+ function prodTimeoutLine(scope, tool, timeoutMs) {
6681
+ return `[${scope}] \u23F1 ${tool}: \u043F\u0440\u043E\u0434 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0437\u0430 ${Math.round(timeoutMs / 100) / 10}\u0441, \u0432\u044B\u0437\u043E\u0432 \u043F\u0440\u0435\u0440\u0432\u0430\u043D`;
6609
6682
  }
6610
6683
  function lastSwarmConnectFailure() {
6611
6684
  return swarmConnectFailure;
6612
6685
  }
6613
6686
  function swarmToolArgs(name, args, workspace) {
6687
+ if ("workspaceId" in args && args.workspaceId == null) {
6688
+ const { workspaceId: _drop, ...rest } = args;
6689
+ return rest;
6690
+ }
6614
6691
  if (!WS_SCOPED_SWARM_TOOLS.has(name) || !workspace) return args;
6615
6692
  return { workspaceId: workspace, ...args };
6616
6693
  }
@@ -6625,6 +6702,7 @@ var init_taskless = __esm({
6625
6702
  init_oauth();
6626
6703
  init_agentToken();
6627
6704
  init_linkFailure();
6705
+ init_prodHttp();
6628
6706
  init_toolFailure();
6629
6707
  init_authState();
6630
6708
  init_workspace();
@@ -10333,7 +10411,7 @@ var SwarmClient = class _SwarmClient {
10333
10411
  // 14.07/18.07, TASWM-201 — общий карантин; здесь — конкретный источник). Резолвим
10334
10412
  // слаг→cuid через get_task (кэш — taskIdCache) ПЕРЕД тем как слать create.
10335
10413
  taskIdCache = /* @__PURE__ */ new Map();
10336
- // task_slug → cuid прода
10414
+ // task_slug → cuid прода и воркспейс задачи
10337
10415
  // Create для этого runKey резолвился окончательно неудачно (задача не найдена/
10338
10416
  // недоступна) — событие/вопросы того же прогона тоже никогда не получат prod-id,
10339
10417
  // без карантина они бы бросали «нет prod-id» на каждом флаше и СНОВА вставали
@@ -10348,12 +10426,20 @@ var SwarmClient = class _SwarmClient {
10348
10426
  const cacheKey = `${taskRef}\0${repoFullName ?? ""}\0${workspaceId ?? ""}`;
10349
10427
  const cached = this.taskIdCache.get(cacheKey);
10350
10428
  if (cached) return cached;
10429
+ const first = await this.readTaskRef(taskRef, repoFullName, workspaceId);
10430
+ const res = first === null && repoFullName && !workspaceId ? await this.readTaskRef(taskRef, repoFullName, null) : first;
10431
+ if (res && res !== "transient") this.taskIdCache.set(cacheKey, res);
10432
+ return res;
10433
+ }
10434
+ /** Один get_task: cuid задачи и её воркспейс; null — прод ответил «нет такой». */
10435
+ async readTaskRef(taskRef, repoFullName, workspaceId) {
10351
10436
  let res;
10352
10437
  try {
10353
10438
  res = await this.call("get_task", {
10354
10439
  id: taskRef,
10355
10440
  ...repoFullName ? { repoFullName } : {},
10356
- ...workspaceId ? { workspaceId } : {}
10441
+ // null — «без пина демона» (см. swarmToolArgs), undefined — пин по умолчанию.
10442
+ ...workspaceId ? { workspaceId } : workspaceId === null ? { workspaceId: null } : {}
10357
10443
  });
10358
10444
  } catch {
10359
10445
  return "transient";
@@ -10363,10 +10449,9 @@ var SwarmClient = class _SwarmClient {
10363
10449
  const text = (res.content ?? []).map((c) => c.text ?? "").join(" ");
10364
10450
  return /not_found/i.test(text) ? null : "transient";
10365
10451
  }
10366
- const id = res.structuredContent?.task?.id;
10367
- if (!id) return null;
10368
- this.taskIdCache.set(cacheKey, id);
10369
- return id;
10452
+ const task = res.structuredContent?.task;
10453
+ if (!task?.id) return null;
10454
+ return { id: task.id, workspaceId: typeof task.workspaceId === "string" ? task.workspaceId : null };
10370
10455
  }
10371
10456
  // MCP-31: привести repo прогона к owner/repo-форме для подсказки резолверу
10372
10457
  // задачи. Прод сматчит подсказку с ProjectRepo.repoFullName → воркспейс. repo
@@ -10405,18 +10490,19 @@ var SwarmClient = class _SwarmClient {
10405
10490
  this.poisonedRunKeys.add(e.runKey);
10406
10491
  return "skip";
10407
10492
  }
10408
- const taskId = await this.resolveTaskCuid(e.taskRef, this.repoFullNameFor(fields.repo), e.workspaceId);
10409
- if (taskId === "transient") {
10493
+ const task = await this.resolveTaskCuid(e.taskRef, this.repoFullNameFor(fields.repo), e.workspaceId);
10494
+ if (task === "transient") {
10410
10495
  throw new Error(`slug\u2192cuid \u0434\u043B\u044F \xAB${e.taskRef}\xBB \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (\u043F\u0440\u043E\u0434 \u043C\u043E\u0440\u0433\u043D\u0443\u043B) \u2014 retry`);
10411
10496
  }
10412
- if (!taskId) {
10497
+ if (!task) {
10413
10498
  console.error(
10414
10499
  `[swarm-client] create-\u0444\u043B\u0430\u0448 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: \u043F\u0440\u043E\u0434 \u043D\u0435 \u0437\u043D\u0430\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0443 \xAB${e.taskRef}\xBB (runKey=${e.runKey}) \u2014 \u0437\u0430\u043F\u0438\u0441\u044C \u0432\u044B\u0431\u0440\u043E\u0448\u0435\u043D\u0430 \u0438\u0437 outbox`
10415
10500
  );
10416
10501
  this.poisonedRunKeys.add(e.runKey);
10417
10502
  return "skip";
10418
10503
  }
10419
- return { tool: "swarm_run_upsert", args: { taskId, ...e.workspaceId ? { workspaceId: e.workspaceId } : {}, ...fields } };
10504
+ const workspaceId = e.workspaceId || task.workspaceId;
10505
+ return { tool: "swarm_run_upsert", args: { taskId: task.id, ...workspaceId ? { workspaceId } : {}, ...fields } };
10420
10506
  }
10421
10507
  if (this.poisonedRunKeys.has(e.runKey)) {
10422
10508
  console.error(`[swarm-client] ${e.t}-\u0444\u043B\u0430\u0448 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: create \u043F\u0440\u043E\u0433\u043E\u043D\u0430 runKey=${e.runKey} \u043D\u0435 \u0440\u0435\u0437\u043E\u043B\u0432\u0438\u043B\u0441\u044F \u0440\u0430\u043D\u0435\u0435 \u2014 \u0437\u0430\u043F\u0438\u0441\u044C \u0432\u044B\u0431\u0440\u043E\u0448\u0435\u043D\u0430 \u0438\u0437 outbox`);
@@ -10468,6 +10554,7 @@ var SwarmClient = class _SwarmClient {
10468
10554
  } catch {
10469
10555
  break;
10470
10556
  }
10557
+ if (res?.isError && res.unavailable) break;
10471
10558
  if (res?.isError) {
10472
10559
  const reason = (res.content ?? []).map((c) => c.text).filter(Boolean).join(" ").trim() || "\u0431\u0435\u0437 \u043E\u0431\u044A\u044F\u0441\u043D\u0435\u043D\u0438\u044F";
10473
10560
  const tries = (this.rejected.get(e.seq) ?? 0) + 1;
@@ -10590,8 +10677,39 @@ function tickPulse(health, now, intervalMs) {
10590
10677
  return { seq: health.seq, ageMs: tickAgeMs(health, now), intervalMs };
10591
10678
  }
10592
10679
 
10680
+ // src/tickSteps.ts
10681
+ var SLOW_PASS_MS = 3e4;
10682
+ function makeStepClock(now = Date.now) {
10683
+ const startedAt = now();
10684
+ const spent = /* @__PURE__ */ new Map();
10685
+ let current = "\u043D\u0430\u0447\u0430\u043B\u043E \u043F\u0440\u043E\u0445\u043E\u0434\u0430";
10686
+ let since = startedAt;
10687
+ const close = () => {
10688
+ const t = now();
10689
+ spent.set(current, (spent.get(current) ?? 0) + (t - since));
10690
+ since = t;
10691
+ };
10692
+ return {
10693
+ mark(step) {
10694
+ close();
10695
+ current = step;
10696
+ },
10697
+ finish() {
10698
+ close();
10699
+ const steps = [...spent].map(([step, ms]) => ({ step, ms })).sort((a, b) => b.ms - a.ms);
10700
+ return { totalMs: since - startedAt, steps };
10701
+ }
10702
+ };
10703
+ }
10704
+ function slowPassLine(seq, done, thresholdMs = SLOW_PASS_MS) {
10705
+ if (done.totalMs <= thresholdMs) return null;
10706
+ const sec = (ms) => `${Math.round(ms / 100) / 10}\u0441`;
10707
+ const top = done.steps.slice(0, 3).map((s) => `${s.step} ${sec(s.ms)}`).join(", ");
10708
+ return `[overmind] \u23F1 \u043F\u0440\u043E\u0445\u043E\u0434 \u0442\u0438\u043A\u0430 #${seq} \u0434\u043B\u0438\u043B\u0441\u044F ${sec(done.totalMs)} (\u043F\u043E\u0440\u043E\u0433 ${sec(thresholdMs)}); \u0434\u043E\u043B\u044C\u0448\u0435 \u0432\u0441\u0435\u0433\u043E: ${top}`;
10709
+ }
10710
+
10593
10711
  // src/logRotate.ts
10594
- import { copyFileSync as copyFileSync4, renameSync as renameSync5, statSync as statSync7, fstatSync, truncateSync, unlinkSync as unlinkSync3, existsSync as existsSync9 } from "fs";
10712
+ import { copyFileSync as copyFileSync4, renameSync as renameSync5, statSync as statSync7, fstatSync, truncateSync, unlinkSync as unlinkSync3, existsSync as existsSync9, openSync as openSync2, writeSync, closeSync as closeSync2 } from "fs";
10595
10713
  var realIo = {
10596
10714
  size: (path) => {
10597
10715
  try {
@@ -10625,6 +10743,7 @@ var realOwnerIo = {
10625
10743
  }
10626
10744
  };
10627
10745
  function stdoutGoesTo(path, io = realOwnerIo) {
10746
+ if (teedTo === path) return true;
10628
10747
  const out = io.statFd(1);
10629
10748
  if (!out || !out.isFile) return false;
10630
10749
  const file = io.statPath(path);
@@ -10649,13 +10768,14 @@ function rotateDaemonLog(path, opts, io = realIo) {
10649
10768
  io.truncate(path);
10650
10769
  return "rotated";
10651
10770
  }
10771
+ var teedTo = null;
10652
10772
 
10653
10773
  // src/daemonLaunch.ts
10654
- import { openSync as openSync2, closeSync as closeSync2, readFileSync as readFileSync9 } from "fs";
10774
+ import { openSync as openSync3, closeSync as closeSync3, readFileSync as readFileSync9 } from "fs";
10655
10775
  import { spawn } from "child_process";
10656
10776
  var realRespawnIo = {
10657
10777
  isTty: () => process.stdout.isTTY === true,
10658
- open: (path) => openSync2(path, "a")
10778
+ open: (path) => openSync3(path, "a")
10659
10779
  };
10660
10780
  function respawnStdio(logPath, io = realRespawnIo) {
10661
10781
  if (io.isTty()) return "inherit";
@@ -11457,7 +11577,7 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
11457
11577
 
11458
11578
  // src/launcher.ts
11459
11579
  import { spawn as spawn2 } from "child_process";
11460
- import { closeSync as closeSync3, existsSync as existsSync12, mkdirSync as mkdirSync9, openSync as openSync3, readdirSync as readdirSync10 } from "fs";
11580
+ import { closeSync as closeSync4, existsSync as existsSync12, mkdirSync as mkdirSync9, openSync as openSync4, readdirSync as readdirSync10 } from "fs";
11461
11581
  import { createRequire } from "module";
11462
11582
  import { homedir as homedir6 } from "os";
11463
11583
  import { dirname as dirname3, join as join18 } from "path";
@@ -11666,14 +11786,14 @@ function openNextRunLog(runLogsDir, runLogKeys) {
11666
11786
  const path = nextRunLogPath(runLogsDir, runLogKeys);
11667
11787
  let out;
11668
11788
  try {
11669
- out = openSync3(path, "wx");
11789
+ out = openSync4(path, "wx");
11670
11790
  } catch (e) {
11671
11791
  if (e.code !== "EEXIST") throw e;
11672
11792
  continue;
11673
11793
  }
11674
11794
  let err = null;
11675
11795
  try {
11676
- err = openSync3(`${path.slice(0, -".json".length)}.err.log`, "w");
11796
+ err = openSync4(`${path.slice(0, -".json".length)}.err.log`, "w");
11677
11797
  } catch {
11678
11798
  err = null;
11679
11799
  }
@@ -11803,8 +11923,8 @@ function makeLauncher(cfg) {
11803
11923
  // они про его собственное состояние, и перебивать их ройным нечем.
11804
11924
  env: { ...droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null), ...plan.env }
11805
11925
  });
11806
- if (outFd !== void 0) closeSync3(outFd);
11807
- if (errFd !== void 0) closeSync3(errFd);
11926
+ if (outFd !== void 0) closeSync4(outFd);
11927
+ if (errFd !== void 0) closeSync4(errFd);
11808
11928
  child.unref();
11809
11929
  if (child.pid == null) throw new Error("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043D\u0435\u0442 pid)");
11810
11930
  return child.pid;
@@ -12085,7 +12205,7 @@ import { execa } from "execa";
12085
12205
 
12086
12206
  // src/prepareLock.ts
12087
12207
  import { createHash } from "crypto";
12088
- import { closeSync as closeSync4, openSync as openSync4, rmSync as rmSync2, statSync as statSync12, writeSync } from "fs";
12208
+ import { closeSync as closeSync5, openSync as openSync5, rmSync as rmSync2, statSync as statSync12, writeSync as writeSync2 } from "fs";
12089
12209
  import { join as join23 } from "path";
12090
12210
  import { tmpdir } from "os";
12091
12211
  var WAIT_TIMEOUT_MS = 5 * 6e4;
@@ -12096,11 +12216,11 @@ function machineLockPath(key, dir = tmpdir()) {
12096
12216
  }
12097
12217
  function defaultTryCreate(path, content) {
12098
12218
  try {
12099
- const fd = openSync4(path, "wx");
12219
+ const fd = openSync5(path, "wx");
12100
12220
  try {
12101
- writeSync(fd, content);
12221
+ writeSync2(fd, content);
12102
12222
  } finally {
12103
- closeSync4(fd);
12223
+ closeSync5(fd);
12104
12224
  }
12105
12225
  return true;
12106
12226
  } catch {
@@ -12839,6 +12959,18 @@ var FileOverlapError = class extends SpawnDeferredError {
12839
12959
  }
12840
12960
  blockedByRunId;
12841
12961
  };
12962
+ var SpawnAbandonedError = class extends Error {
12963
+ constructor(status) {
12964
+ super(`\u043F\u0440\u043E\u0433\u043E\u043D \u0441\u043D\u044F\u0442 (${status}), \u043F\u043E\u043A\u0430 \u0433\u043E\u0442\u043E\u0432\u0438\u043B\u0430\u0441\u044C \u0440\u0430\u0431\u043E\u0447\u0430\u044F \u043A\u043E\u043F\u0438\u044F \u2014 \u0434\u0440\u043E\u043D\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E`);
12965
+ this.status = status;
12966
+ this.name = "SpawnAbandonedError";
12967
+ }
12968
+ status;
12969
+ };
12970
+ function assertStillWanted(db, runId) {
12971
+ const fresh = db.getRun(runId);
12972
+ if (fresh && isTerminalStatus(fresh.status)) throw new SpawnAbandonedError(fresh.status);
12973
+ }
12842
12974
  function healTaskSlug(db, run2, task) {
12843
12975
  const slug = task?.slug;
12844
12976
  if (!slug || slug === run2.task_slug || !isTaskSlug(slug) || isTaskSlug(run2.task_slug)) return run2;
@@ -12856,6 +12988,7 @@ async function spawnDrone(deps, run2) {
12856
12988
  if (run2.area && deps.db.runningAreas("").includes(run2.area)) throw new AreaBusyError(run2.area);
12857
12989
  deps.db.addEvent(run2.id, "step", `repoless-\u043F\u0440\u043E\u0433\u043E\u043D: \u0441\u043A\u0440\u0430\u0442\u0447-\u043A\u0430\u0442\u0430\u043B\u043E\u0433 ${workdir} (\u0431\u0435\u0437 worktree/\u0432\u0435\u0442\u043A\u0438/PR \u2014 TASWM-595)`);
12858
12990
  const built2 = await deps.buildPrompt(run2, workdir);
12991
+ assertStillWanted(deps.db, run2.id);
12859
12992
  const pid2 = deps.launch({ worktree: workdir, run: run2, prompt: built2 });
12860
12993
  deps.db.addEvent(run2.id, "step", `\u0434\u0440\u043E\u043D \u0437\u0430\u043F\u0443\u0449\u0435\u043D \u0432 ${workdir} (pid ${pid2})`);
12861
12994
  return deps.db.updateRun(run2.id, {
@@ -12891,6 +13024,7 @@ async function spawnDrone(deps, run2) {
12891
13024
  const prompt = notice ? `${built}
12892
13025
 
12893
13026
  ${notice}` : built;
13027
+ assertStillWanted(deps.db, run2.id);
12894
13028
  const pid = deps.launch({ worktree, run: run2, prompt });
12895
13029
  deps.db.addEvent(run2.id, "step", `\u0434\u0440\u043E\u043D \u0437\u0430\u043F\u0443\u0449\u0435\u043D \u0432 ${worktree} (pid ${pid})`);
12896
13030
  return deps.db.updateRun(run2.id, {
@@ -13067,7 +13201,7 @@ function parkReasonUnretriable(reason) {
13067
13201
  // package.json
13068
13202
  var package_default = {
13069
13203
  name: "@taskless-app/swarm",
13070
- version: "0.6.66",
13204
+ version: "0.6.68",
13071
13205
  type: "module",
13072
13206
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
13073
13207
  keywords: [
@@ -13116,9 +13250,10 @@ var package_default = {
13116
13250
  "@hono/node-server": "^2.0.12",
13117
13251
  "@modelcontextprotocol/sdk": "^1.29.0",
13118
13252
  execa: "^10.0.0",
13253
+ undici: "^7.25.0",
13119
13254
  zod: "3.25.76"
13120
13255
  },
13121
- gitHead: "a6bc0ea303cb5336fd4e130fba73d90365268b6f"
13256
+ gitHead: "4bfd78926aa37ef7de8ea06a6763d7e84a2fded6"
13122
13257
  };
13123
13258
 
13124
13259
  // src/version.ts
@@ -14127,12 +14262,36 @@ init_src();
14127
14262
 
14128
14263
  // src/reviewCycle.ts
14129
14264
  init_src();
14265
+ var REVIEW_CYCLE_LIMIT_DEFAULT = 3;
14266
+ var REVIEW_CYCLE_LIMIT_CAP = 20;
14267
+ function validReviewCycleLimit(n) {
14268
+ return n != null && Number.isInteger(n) && n >= 1 && n <= REVIEW_CYCLE_LIMIT_CAP;
14269
+ }
14270
+ function parseReviewCycleLimitEnv(raw) {
14271
+ if (raw == null || raw.trim() === "") return REVIEW_CYCLE_LIMIT_DEFAULT;
14272
+ const n = Number(raw);
14273
+ return validReviewCycleLimit(n) ? n : REVIEW_CYCLE_LIMIT_DEFAULT;
14274
+ }
14275
+ var REVIEW_CYCLE_LIMIT = parseReviewCycleLimitEnv(process.env.SWARM_REVIEW_CYCLE_LIMIT);
14276
+ var configuredReviewCycleLimit = null;
14277
+ function configureReviewCycleLimit(n) {
14278
+ configuredReviewCycleLimit = validReviewCycleLimit(n) ? n : null;
14279
+ }
14280
+ function effectiveReviewCycleLimit() {
14281
+ return configuredReviewCycleLimit ?? REVIEW_CYCLE_LIMIT;
14282
+ }
14283
+ function reviewCyclesExhausted(run2) {
14284
+ return (run2.review_cycle ?? 0) >= effectiveReviewCycleLimit();
14285
+ }
14130
14286
  function needsFixIteration(run2) {
14131
14287
  return run2.review_verdict === "changes" || isNegativeVerifyVerdict(toVerifyVerdict(run2.verify_verdict));
14132
14288
  }
14133
14289
  function reviewTransitionPatch(run2, prUrl) {
14134
14290
  const isFixIteration = needsFixIteration(run2);
14135
14291
  if (isFixIteration) {
14292
+ if (reviewCyclesExhausted(run2)) {
14293
+ return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
14294
+ }
14136
14295
  const cycle = (run2.review_cycle ?? 0) + 1;
14137
14296
  return {
14138
14297
  isFixIteration: true,
@@ -15767,11 +15926,17 @@ init_procList();
15767
15926
  // src/mcpResilient.ts
15768
15927
  init_linkFailure();
15769
15928
  init_taskless();
15929
+ init_prodHttp();
15770
15930
  var DEFAULT_BACKOFF_MS = [5e3, 15e3, 3e4, 6e4];
15771
15931
  var DEFAULT_DOWN_AFTER_MS = 5 * 6e4;
15772
15932
  var DEFAULT_RETRY_DELAYS_MS = [200, 800];
15773
- function errorResult(reason) {
15774
- return { isError: true, content: [{ type: "text", text: `Swarm MCP \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: ${reason}` }] };
15933
+ var PROD_PASS_BUDGET_MS = 2e4;
15934
+ function errorResult(reason, unavailable) {
15935
+ return {
15936
+ isError: true,
15937
+ ...unavailable ? { unavailable: true } : {},
15938
+ content: [{ type: "text", text: `Swarm MCP \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: ${reason}` }]
15939
+ };
15775
15940
  }
15776
15941
  function resilientSwarmMcp(opts = {}) {
15777
15942
  const connect = opts.connect ?? (() => connectSwarmMcp());
@@ -15779,6 +15944,13 @@ function resilientSwarmMcp(opts = {}) {
15779
15944
  const downAfterMs = opts.downAfterMs ?? DEFAULT_DOWN_AFTER_MS;
15780
15945
  const retryDelays = opts.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
15781
15946
  const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
15947
+ const clock = opts.now ?? Date.now;
15948
+ const passBudgetMs = opts.passBudgetMs ?? PROD_PASS_BUDGET_MS;
15949
+ let passSpentMs = null;
15950
+ let budgetNoted = false;
15951
+ const spend = (since) => {
15952
+ if (passSpentMs !== null) passSpentMs += Math.max(0, clock() - since);
15953
+ };
15782
15954
  let caller = null;
15783
15955
  let lastTryAt = 0;
15784
15956
  let failures = 0;
@@ -15801,6 +15973,7 @@ function resilientSwarmMcp(opts = {}) {
15801
15973
  if (!force && lastTryAt !== 0 && now - lastTryAt < wait) return null;
15802
15974
  if (!force) lastTryAt = now;
15803
15975
  let fresh = null;
15976
+ const connectStart = clock();
15804
15977
  try {
15805
15978
  fresh = await connect();
15806
15979
  if (!fresh) lastConnectReason = lastSwarmConnectFailure() ?? UNKNOWN_LINK_REASON;
@@ -15817,17 +15990,30 @@ function resilientSwarmMcp(opts = {}) {
15817
15990
  downReported = false;
15818
15991
  return caller;
15819
15992
  }
15993
+ spend(connectStart);
15820
15994
  if (!force) failures += 1;
15821
15995
  if (downSince === null) downSince = now;
15822
15996
  return null;
15823
15997
  }
15824
15998
  const call = async (name, args) => {
15825
15999
  for (let attempt = 0; ; attempt++) {
16000
+ if (passSpentMs !== null && passSpentMs >= passBudgetMs) {
16001
+ if (!budgetNoted) {
16002
+ budgetNoted = true;
16003
+ opts.onBudgetSpent?.({ spentMs: passSpentMs, budgetMs: passBudgetMs });
16004
+ }
16005
+ maybeReportDown();
16006
+ return errorResult(
16007
+ `\u043F\u0440\u043E\u0434 \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442, \u0431\u044E\u0434\u0436\u0435\u0442 \u043F\u0440\u043E\u0445\u043E\u0434\u0430 \u043D\u0430 \u0441\u0435\u0442\u044C (${Math.round(passBudgetMs / 1e3)}\u0441) \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u2014 \u0434\u043E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043F\u0440\u043E\u0445\u043E\u0434\u0430 \u0431\u0435\u0437 \u0441\u0435\u0442\u0438${lastCallReason ?? lastConnectReason ? ` (\u043F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u043F\u0440\u0438\u0447\u0438\u043D\u0430: ${lastCallReason ?? lastConnectReason})` : ""}`,
16008
+ true
16009
+ );
16010
+ }
15826
16011
  const active = await ensureCaller(attempt > 0);
15827
16012
  if (!active) {
15828
16013
  maybeReportDown();
15829
- return errorResult(lastConnectReason ?? UNKNOWN_LINK_REASON);
16014
+ return errorResult(lastConnectReason ?? UNKNOWN_LINK_REASON, true);
15830
16015
  }
16016
+ const callStart = clock();
15831
16017
  try {
15832
16018
  const res = await active(name, args);
15833
16019
  lastOkAt = Date.now();
@@ -15835,13 +16021,14 @@ function resilientSwarmMcp(opts = {}) {
15835
16021
  return res;
15836
16022
  } catch (e) {
15837
16023
  caller = null;
16024
+ spend(callStart);
15838
16025
  if (downSince === null) downSince = Date.now();
15839
16026
  const { kind, detail: reason } = explainLinkFailure(e);
15840
16027
  lastCallReason = reason;
15841
16028
  const delay = retryDelays[attempt];
15842
- if (delay === void 0 || kind !== "offline") {
16029
+ if (delay === void 0 || kind !== "offline" || isProdTimeout(e)) {
15843
16030
  maybeReportDown();
15844
- return errorResult(reason);
16031
+ return errorResult(reason, kind === "offline" || kind === "unknown" || kind === "unauthorized" || isProdTimeout(e));
15845
16032
  }
15846
16033
  opts.onRetry?.({ tool: name, attempt: attempt + 1, reason });
15847
16034
  await sleep2(delay);
@@ -15849,6 +16036,10 @@ function resilientSwarmMcp(opts = {}) {
15849
16036
  }
15850
16037
  };
15851
16038
  return Object.assign(call, {
16039
+ beginPass: () => {
16040
+ passSpentMs = 0;
16041
+ budgetNoted = false;
16042
+ },
15852
16043
  // Причина — ТА ЖЕ, что уехала в лог: сначала свежий отказ вызова, иначе отказ
15853
16044
  // подключения. Своего разбора здесь нет намеренно (см. lastCallReason выше).
15854
16045
  link: () => ({ connected: caller !== null, downSince, lastOkAt, lastError: caller !== null ? null : lastCallReason ?? lastConnectReason })
@@ -19133,14 +19324,86 @@ var BASE_MERGED_MARK = "\u0431\u0430\u0437\u0430 \u0432\u043B\u0438\u0442\u0430
19133
19324
  var BASE_NOOP_MARK = "\u0432\u043B\u0438\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E";
19134
19325
  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
19326
  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;
19327
+ function reviewCycleHumanBody(cycle, limit) {
19328
+ 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.`;
19329
+ }
19330
+ 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";
19331
+ 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";
19332
+ var cycleCommentInflight = /* @__PURE__ */ new Map();
19333
+ function takeCycleCommentSlot(runId, side) {
19334
+ let slot = cycleCommentInflight.get(runId);
19335
+ if (!slot) {
19336
+ slot = { task: false, pr: false };
19337
+ cycleCommentInflight.set(runId, slot);
19338
+ }
19339
+ if (slot[side]) return false;
19340
+ slot[side] = true;
19341
+ return true;
19141
19342
  }
19142
- function effectiveReviewCycleLimit() {
19143
- return configuredReviewCycleLimit ?? REVIEW_CYCLE_LIMIT;
19343
+ function freeCycleCommentSlot(runId, side) {
19344
+ const slot = cycleCommentInflight.get(runId);
19345
+ if (slot) slot[side] = false;
19346
+ }
19347
+ async function noteReviewCyclesExhausted(deps, run2) {
19348
+ const texts = deps.db.events(run2.id).map((e) => e.text ?? "");
19349
+ const marked = texts.some((t) => t.includes(REVIEW_CYCLE_ESCALATION_MARK));
19350
+ const taskSent = texts.some((t) => t.includes(CYCLE_TASK_COMMENT_SENT));
19351
+ const prSent = texts.some((t) => t.includes(CYCLE_PR_COMMENT_SENT));
19352
+ const cycle = run2.review_cycle ?? 0;
19353
+ const body = reviewCycleHumanBody(cycle, effectiveReviewCycleLimit());
19354
+ if (!marked) {
19355
+ deps.db.addEvent(run2.id, "step", `${REVIEW_CYCLE_ESCALATION_MARK}: ${body}`);
19356
+ try {
19357
+ deps.narrate?.(body);
19358
+ } catch {
19359
+ }
19360
+ }
19361
+ if (deps.reportComment && !taskSent && takeCycleCommentSlot(run2.id, "task")) {
19362
+ const ok = await deps.reportComment(
19363
+ run2.task_slug,
19364
+ body,
19365
+ { swarmKind: "report", swarmDroneName: run2.name || "(\u0438\u043C\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)", swarmReviewCycle: cycle },
19366
+ run2.workspace_id
19367
+ ).then(() => true).catch((e) => {
19368
+ 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)}`);
19369
+ return false;
19370
+ });
19371
+ if (ok) deps.db.addEvent(run2.id, "step", CYCLE_TASK_COMMENT_SENT);
19372
+ freeCycleCommentSlot(run2.id, "task");
19373
+ }
19374
+ if (run2.pr_url && deps.prComment && !prSent && takeCycleCommentSlot(run2.id, "pr")) {
19375
+ const ok = await deps.prComment(run2.pr_url, body).then(() => true).catch((e) => {
19376
+ 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)}`);
19377
+ return false;
19378
+ });
19379
+ if (ok) deps.db.addEvent(run2.id, "step", CYCLE_PR_COMMENT_SENT);
19380
+ freeCycleCommentSlot(run2.id, "pr");
19381
+ }
19382
+ }
19383
+ function cycleHolder(db, run2) {
19384
+ if (run2.kind === "hero" || run2.resolves_run_id != null) return null;
19385
+ const parentId = run2.reviews_run_id ?? run2.verifies_run_id;
19386
+ if (parentId != null) return db.getRun(parentId) ?? null;
19387
+ return run2;
19388
+ }
19389
+ function cycleCapHolder(db, run2) {
19390
+ const holder = cycleHolder(db, run2);
19391
+ return holder && reviewCyclesExhausted(holder) ? holder : null;
19392
+ }
19393
+ 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";
19394
+ async function handOffAtCycleCap(deps, run2, holder, where) {
19395
+ await noteReviewCyclesExhausted(deps, holder);
19396
+ 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`;
19397
+ if (holder.id !== run2.id) {
19398
+ if (run2.worktree_path) await deps.removeWorktree(run2.repo, run2.worktree_path).catch(() => {
19399
+ });
19400
+ deps.db.updateRun(run2.id, { status: "stopped", stop_reason: CYCLE_CAP_STOP_REASON });
19401
+ deps.db.addEvent(run2.id, "stopped", text);
19402
+ return;
19403
+ }
19404
+ if (run2.pr_url) deps.db.updateRun(run2.id, { status: "review", stop_reason: null });
19405
+ else deps.db.updateRun(run2.id, { status: "paused", stop_reason: CYCLE_CAP_STOP_REASON });
19406
+ deps.db.addEvent(run2.id, "step", text);
19144
19407
  }
19145
19408
  var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
19146
19409
  var CI_WAKE_COOLDOWN_MS = Number(process.env.SWARM_CI_WAKE_COOLDOWN_MS) || 3 * 60 * 1e3;
@@ -19168,7 +19431,7 @@ var CHANGES_WAKE_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430
19168
19431
  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
19432
  var CHANGES_WAKE_FAILED_MARK = "\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C";
19170
19433
  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];
19434
+ var NO_AUTO_WAKE_REASONS = [EXITED_NO_DONE_REASON, BUDGET_STOP_REASON, ABORTED_STOP_REASON, AWAITING_BUDGET_REASON, CYCLE_CAP_STOP_REASON];
19172
19435
  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
19436
  function verdictCommentMark(createdAt) {
19174
19437
  return `\u043A\u043E\u043C\u043C\u0435\u043D\u0442-\u0432\u0435\u0440\u0434\u0438\u043A\u0442@${createdAt}`;
@@ -19331,7 +19594,9 @@ function reviewPhaseInputFor(deps, run2, projectWantsVerify = true) {
19331
19594
  reviewerDead,
19332
19595
  verifyVerdict: run2.verify_verdict,
19333
19596
  verifyExpected,
19334
- verifierDead
19597
+ verifierDead,
19598
+ // TASWM-606: потолок кругов важнее «чинит CI» и «ждёт ревьюера» — человека уже позвали.
19599
+ cyclesExhausted: reviewCyclesExhausted(run2)
19335
19600
  };
19336
19601
  }
19337
19602
  async function refreshReviewPhases(deps) {
@@ -19365,8 +19630,19 @@ function countInFlight(db) {
19365
19630
  return db.listRuns().filter((r) => r.kind !== "hero" && IN_FLIGHT_STATUSES.has(r.status)).length;
19366
19631
  }
19367
19632
  function countActiveDrones(db) {
19368
- return db.listRuns().filter((r) => r.kind !== "hero" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
19633
+ const preparing = preparingRuns(db);
19634
+ return db.listRuns().filter((r) => r.kind !== "hero" && (ACTIVE_DRONE_STATUSES.has(r.status) || preparing.has(r.id))).length;
19635
+ }
19636
+ var preparingByStore = /* @__PURE__ */ new WeakMap();
19637
+ function preparingRuns(db) {
19638
+ let map = preparingByStore.get(db);
19639
+ if (!map) preparingByStore.set(db, map = /* @__PURE__ */ new Map());
19640
+ return map;
19369
19641
  }
19642
+ function hasPreparingRuns(db) {
19643
+ return preparingRuns(db).size > 0;
19644
+ }
19645
+ var SPAWN_INLINE_WAIT_MS = 2e3;
19370
19646
  function countActiveReviewers(db) {
19371
19647
  return db.listRuns().filter((r) => r.kind === "reviewer" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
19372
19648
  }
@@ -19715,6 +19991,10 @@ async function spawnVerifiersForOpenPrs(deps, stormActive) {
19715
19991
  if (run2.kind !== "drone") continue;
19716
19992
  if (run2.reviews_run_id != null || run2.resolves_run_id != null || run2.verifies_run_id != null) continue;
19717
19993
  if (!run2.pr_url || !isGithubPrUrl(run2.pr_url)) continue;
19994
+ if (reviewCyclesExhausted(run2)) {
19995
+ await noteReviewCyclesExhausted(deps, run2);
19996
+ continue;
19997
+ }
19718
19998
  if (run2.verify_verdict != null) continue;
19719
19999
  if (deps.db.verifierCountFor(run2.id) > 0) continue;
19720
20000
  const attempts = deps.db.verifierAttemptCountFor(run2.id);
@@ -19740,6 +20020,18 @@ async function spawnVerifiersForOpenPrs(deps, stormActive) {
19740
20020
  }
19741
20021
  }
19742
20022
  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.";
20023
+ function buildSubmittedContinueNudge(opts) {
20024
+ 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:
20025
+ ${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`.";
20026
+ 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:
20027
+ ${opts.reviewSummary}` : "";
20028
+ return [
20029
+ `\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.`,
20030
+ verify,
20031
+ review,
20032
+ "\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."
20033
+ ].filter((part) => part.length > 0).join("\n\n");
20034
+ }
19743
20035
  function buildCiWakeNudge(summary) {
19744
20036
  return [
19745
20037
  "\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 +20353,15 @@ function lastVerdictEvent(db, reviewRunId) {
20061
20353
  function lastChangesSummary(db, reviewRunId) {
20062
20354
  return lastVerdictEvent(db, reviewRunId)?.text ?? null;
20063
20355
  }
20356
+ 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"];
20357
+ function lastVerifySummary(db, runId) {
20358
+ const events = db.events(runId);
20359
+ for (let i = events.length - 1; i >= 0; i--) {
20360
+ const text = events[i].text ?? "";
20361
+ if (VERIFY_EVENT_PREFIXES.some((p) => text.startsWith(p))) return text;
20362
+ }
20363
+ return null;
20364
+ }
20064
20365
  function changesVerdictAt(db, reviewRunId) {
20065
20366
  return lastVerdictEvent(db, reviewRunId)?.at ?? null;
20066
20367
  }
@@ -20240,6 +20541,11 @@ async function resurrectStormDead(deps, worktreeExists, hasTranscript, opts) {
20240
20541
  let resurrected = 0;
20241
20542
  for (const run2 of deps.db.listRuns("storm_dead")) {
20242
20543
  if (countActiveDrones(deps.db) >= cap) break;
20544
+ const capHolder = cycleCapHolder(deps.db, run2);
20545
+ if (capHolder) {
20546
+ 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");
20547
+ continue;
20548
+ }
20243
20549
  if (!await projectQuotaGateAllows(deps, run2)) continue;
20244
20550
  const wt = run2.worktree_path;
20245
20551
  const mode = !!wt && worktreeExists(wt) && hasTranscript(run2) ? "continue" : "fresh";
@@ -20263,6 +20569,16 @@ async function wakeRun(deps, runId, opts = {}) {
20263
20569
  }
20264
20570
  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
20571
  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` };
20572
+ const capHolder = cycleCapHolder(deps.db, run2);
20573
+ if (capHolder) {
20574
+ await noteReviewCyclesExhausted(deps, capHolder);
20575
+ const limit = effectiveReviewCycleLimit();
20576
+ 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})`;
20577
+ return {
20578
+ ok: false,
20579
+ 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`
20580
+ };
20581
+ }
20266
20582
  const stoppedForHuman = deps.db.events(run2.id).some((e) => isEscalationText(e.text ?? "", run2, "author"));
20267
20583
  if (run2.status === "review" && run2.pr_url && !needsFixIteration(run2) && !stoppedForHuman) {
20268
20584
  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 +20592,11 @@ async function wakeRun(deps, runId, opts = {}) {
20276
20592
  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
20593
  }
20278
20594
  const mode = canContinue ? "continue" : "fresh";
20595
+ const verifySummary = lastVerifySummary(deps.db, run2.id);
20596
+ const reviewSummary = lastChangesSummary(deps.db, run2.id);
20597
+ 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
20598
  try {
20280
- await deps.respawn(run2, mode);
20599
+ await deps.respawn(run2, mode, nudge);
20281
20600
  } catch (e) {
20282
20601
  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
20602
  }
@@ -20469,6 +20788,10 @@ function closeCiWatch(deps, run2, mark, why, narration) {
20469
20788
  }
20470
20789
  async function wakeAuthorOnRedCi(deps, run2, now, ci, worktreeExists, hasTranscript, droneCap) {
20471
20790
  if (run2.pid != null && deps.isAlive(run2.pid)) return false;
20791
+ if (reviewCyclesExhausted(run2)) {
20792
+ await noteReviewCyclesExhausted(deps, run2);
20793
+ return false;
20794
+ }
20472
20795
  if (deps.resourcesOk && !deps.resourcesOk()) return false;
20473
20796
  if (countActiveDrones(deps.db) >= droneCap) return false;
20474
20797
  if (!await projectQuotaGateAllows(deps, run2)) return false;
@@ -20538,6 +20861,15 @@ async function wakeReviewerOnCiReady(deps, run2, over) {
20538
20861
  const reviewer = awaitingCiReviewer(deps.db, run2);
20539
20862
  if (!reviewer) return false;
20540
20863
  if (reviewer.pid != null && deps.isAlive(reviewer.pid)) return false;
20864
+ if (reviewCyclesExhausted(run2)) {
20865
+ await noteReviewCyclesExhausted(deps, run2);
20866
+ deps.db.addEvent(
20867
+ run2.id,
20868
+ "ci_checked",
20869
+ `${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`
20870
+ );
20871
+ return true;
20872
+ }
20541
20873
  if (deps.resourcesOk && !deps.resourcesOk()) {
20542
20874
  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
20875
  return true;
@@ -20612,7 +20944,8 @@ async function spawnFreshReviewerAfterStall(deps, run2, why) {
20612
20944
  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
20945
  return;
20614
20946
  }
20615
- if (run2.review_cycle >= effectiveReviewCycleLimit() || deps.db.reviewerAttemptCountFor(run2.id) >= REVIEW_SPAWN_LIMIT) {
20947
+ if (reviewCyclesExhausted(run2) || deps.db.reviewerAttemptCountFor(run2.id) >= REVIEW_SPAWN_LIMIT) {
20948
+ if (reviewCyclesExhausted(run2)) await noteReviewCyclesExhausted(deps, run2);
20616
20949
  return;
20617
20950
  }
20618
20951
  const gate = await prGateForReviewer(deps, run2, run2.pr_url, "\u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B CI");
@@ -20636,7 +20969,10 @@ async function wakeAuthorOnChanges(deps, snapshot, now, droneCap) {
20636
20969
  if (deps.resourcesOk && !deps.resourcesOk()) return false;
20637
20970
  if (countActiveDrones(deps.db) >= droneCap) return false;
20638
20971
  if (!await projectQuotaGateAllows(deps, run2)) return false;
20639
- if (run2.review_cycle >= effectiveReviewCycleLimit()) return false;
20972
+ if (reviewCyclesExhausted(run2)) {
20973
+ await noteReviewCyclesExhausted(deps, run2);
20974
+ return false;
20975
+ }
20640
20976
  const prUrl = run2.pr_url;
20641
20977
  const events = deps.db.events(run2.id);
20642
20978
  const mark = `${CHANGES_WAKE_ESCALATION_MARK} (\u043A\u0440\u0443\u0433 ${run2.review_cycle ?? 0})`;
@@ -20927,6 +21263,7 @@ function noteStormStall(deps) {
20927
21263
  for (const run2 of waiting) deferSpawn(deps, run2, reason, { quiet: true });
20928
21264
  }
20929
21265
  async function trySpawnQueuedRun(deps, run2) {
21266
+ if (preparingRuns(deps.db).has(run2.id)) return "skipped";
20930
21267
  if (deps.control?.paused) {
20931
21268
  const why = deps.control.state?.reason;
20932
21269
  return deferSpawn(deps, run2, `\u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B \u043F\u0440\u0438\u0451\u043C \u043D\u043E\u0432\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435${why ? ` \u2014 ${why}` : ""} (\u043F\u0430\u0443\u0437\u0430)`, {
@@ -21021,12 +21358,55 @@ async function trySpawnQueuedRun(deps, run2) {
21021
21358
  await fillSuggestedArea(deps, run2);
21022
21359
  const repoGap = unresolvedRepoReason(run2.repo, deps.reposServed ? deps.reposServed() : []);
21023
21360
  if (repoGap) return await failBeforeStart(deps, run2, repoGap);
21361
+ return await spawnOrPrepareInBackground(deps, run2);
21362
+ }
21363
+ async function spawnOrPrepareInBackground(deps, run2) {
21364
+ const preparing = preparingRuns(deps.db);
21365
+ const repoKey = normalizeRepo(run2.repo || deps.repoFallback || "");
21366
+ for (const [otherId, otherRepo] of preparing) {
21367
+ if (otherId !== run2.id && otherRepo === repoKey) {
21368
+ return deferSpawn(deps, run2, `\u0432 \u044D\u0442\u043E\u043C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0435\u0449\u0451 \u0433\u043E\u0442\u043E\u0432\u0438\u0442\u0441\u044F \u0440\u0430\u0431\u043E\u0447\u0430\u044F \u043A\u043E\u043F\u0438\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${otherId} \u2014 \u0436\u0434\u0443 \u0435\u0451, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u044C \u0434\u0432\u0435 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0438 \u0440\u0430\u0437\u043E\u043C (TASWM-611)`);
21369
+ }
21370
+ }
21371
+ preparing.set(run2.id, repoKey);
21372
+ const attempt = spawnQueuedRun(deps, run2);
21373
+ const release = () => {
21374
+ preparing.delete(run2.id);
21375
+ };
21376
+ void attempt.then(release, release);
21377
+ let timer;
21378
+ const background = /* @__PURE__ */ Symbol("background");
21379
+ const waited = new Promise((resolve5) => {
21380
+ timer = setTimeout(() => resolve5(background), SPAWN_INLINE_WAIT_MS);
21381
+ });
21382
+ try {
21383
+ const first = await Promise.race([attempt, waited]);
21384
+ if (first !== background) return first;
21385
+ } finally {
21386
+ clearTimeout(timer);
21387
+ }
21388
+ deps.db.addEvent(run2.id, "step", "\u0433\u043E\u0442\u043E\u0432\u0438\u0442\u0441\u044F: \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 \u0440\u0430\u0431\u043E\u0447\u0435\u0439 \u043A\u043E\u043F\u0438\u0438 \u0438\u0434\u0451\u0442 \u0444\u043E\u043D\u043E\u043C, \u0442\u0438\u043A \u0435\u0451 \u043D\u0435 \u0436\u0434\u0451\u0442 (TASWM-611)");
21389
+ console.log(`[overmind] ${droneLabel(run2)} (${run2.task_slug}) \u0433\u043E\u0442\u043E\u0432\u0438\u0442\u0441\u044F \u0444\u043E\u043D\u043E\u043C \u2014 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 \u0434\u043E\u043B\u044C\u0448\u0435 ${SPAWN_INLINE_WAIT_MS / 1e3}\u0441, \u0442\u0438\u043A \u0438\u0434\u0451\u0442 \u0434\u0430\u043B\u044C\u0448\u0435`);
21390
+ void attempt.then(
21391
+ (outcome) => {
21392
+ if (outcome !== "spawned") console.warn(`[overmind] \u0444\u043E\u043D\u043E\u0432\u0430\u044F \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 ${run2.task_slug} \u043A\u043E\u043D\u0447\u0438\u043B\u0430\u0441\u044C \u0431\u0435\u0437 \u0434\u0440\u043E\u043D\u0430: ${outcome}`);
21393
+ },
21394
+ (e) => console.error(`[overmind] \u0444\u043E\u043D\u043E\u0432\u0430\u044F \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 ${run2.task_slug} \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C: ${describeTickError(e)}`)
21395
+ );
21396
+ return "spawned";
21397
+ }
21398
+ async function spawnQueuedRun(deps, run2) {
21024
21399
  try {
21025
21400
  await deps.spawn(run2);
21026
21401
  if (deps.spawnDefer) clearDefer(deps.spawnDefer, `run:${run2.id}`);
21027
21402
  narrate(deps, `\u0411\u0435\u0440\u0443 ${run2.task_slug} \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0441\u043F\u0430\u0432\u043D\u044E ${droneLabel(run2)}.`);
21028
21403
  return "spawned";
21029
21404
  } catch (e) {
21405
+ if (e instanceof SpawnAbandonedError) {
21406
+ deps.db.addEvent(run2.id, "step", e.message);
21407
+ console.log(`[overmind] ${run2.task_slug}: ${e.message}`);
21408
+ return "closed";
21409
+ }
21030
21410
  if (e instanceof FileOverlapError) {
21031
21411
  const logged = deps.overlapLogged ??= /* @__PURE__ */ new Map();
21032
21412
  if (logged.get(run2.id) !== e.message) {
@@ -21189,6 +21569,11 @@ async function resumeWatchdogParked(deps, droneCap) {
21189
21569
  }
21190
21570
  if (!candidate) return;
21191
21571
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
21572
+ const capHolder = cycleCapHolder(deps.db, candidate);
21573
+ if (capHolder) {
21574
+ 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");
21575
+ return;
21576
+ }
21192
21577
  const worktreeExists = deps.worktreeExists ?? existsSync22;
21193
21578
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
21194
21579
  const wt = candidate.worktree_path;
@@ -21248,6 +21633,11 @@ async function applyWatchdogAction(deps, action) {
21248
21633
  }
21249
21634
  case "fresh": {
21250
21635
  if (!deps.respawn) return;
21636
+ const capHolder = cycleCapHolder(deps.db, action.run);
21637
+ if (capHolder) {
21638
+ 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");
21639
+ return;
21640
+ }
21251
21641
  try {
21252
21642
  await deps.respawn(action.run, "fresh", buildFreshRestartNudge(action.run));
21253
21643
  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 +21881,11 @@ async function manageRelief(deps, now, droneCap, stormActive) {
21491
21881
  }
21492
21882
  if (now - loopEntry.lastPauseAt < reliefBackoffMs(loopEntry.pauses, cfg.rampWindowMs, loopCfg)) return;
21493
21883
  }
21884
+ const capHolder = cycleCapHolder(deps.db, resumeCandidate);
21885
+ if (capHolder) {
21886
+ await handOffAtCycleCap(deps, resumeCandidate, capHolder, "\u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u0438\u0437 \u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0438");
21887
+ return;
21888
+ }
21494
21889
  const wt = resumeCandidate.worktree_path;
21495
21890
  const canContinue = !!wt && worktreeAndTranscriptAlive(deps, resumeCandidate);
21496
21891
  const mode = canContinue ? "continue" : "fresh";
@@ -21911,6 +22306,8 @@ async function answerBudgetQuestions(deps, ctx) {
21911
22306
  respawn: deps.respawn,
21912
22307
  ...deps.worktreeExists ? { worktreeExists: deps.worktreeExists } : {},
21913
22308
  ...deps.hasTranscript ? { hasTranscript: deps.hasTranscript } : {},
22309
+ ...deps.prComment ? { prComment: deps.prComment } : {},
22310
+ ...deps.reportComment ? { reportComment: deps.reportComment } : {},
21914
22311
  narrate: (m) => narrate(deps, m)
21915
22312
  },
21916
22313
  run2.id,
@@ -21920,6 +22317,11 @@ async function answerBudgetQuestions(deps, ctx) {
21920
22317
  budgetSyncAt.delete(run2.id);
21921
22318
  continue;
21922
22319
  }
22320
+ if (reviewCyclesExhausted(run2) && run2.pr_url) {
22321
+ deps.db.updateRun(run2.id, { status: "review", stop_reason: null });
22322
+ budgetSyncAt.delete(run2.id);
22323
+ continue;
22324
+ }
21923
22325
  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
22326
  if (deps.db.events(run2.id).at(-1)?.text !== note) deps.db.addEvent(run2.id, "step", note);
21925
22327
  }
@@ -21978,7 +22380,9 @@ async function tickOnce(deps) {
21978
22380
  const now = deps.now ? deps.now() : Date.now();
21979
22381
  const worktreeExists = deps.worktreeExists ?? existsSync22;
21980
22382
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
22383
+ deps.stepClock?.mark("0 \u0437\u0430\u043C\u0435\u0440 \u0445\u043E\u0441\u0442\u0430");
21981
22384
  deps.sampleHost?.();
22385
+ deps.stepClock?.mark("0a re-hydrate \u0437\u0435\u0440\u043A\u0430\u043B\u0430");
21982
22386
  if (deps.rehydrate) {
21983
22387
  const rh = deps.rehydrate;
21984
22388
  const sinceLast = rh.lastAt == null ? Infinity : now - rh.lastAt;
@@ -21991,6 +22395,7 @@ async function tickOnce(deps) {
21991
22395
  }
21992
22396
  }
21993
22397
  }
22398
+ deps.stepClock?.mark("0a1 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u043E\u0447\u0435\u0440\u0435\u0434\u0438");
21994
22399
  if (deps.db.refreshDeps) {
21995
22400
  const waiting = deps.db.listRuns("queued").flatMap((r) => r.depends_on ?? []);
21996
22401
  if (waiting.length) {
@@ -22001,6 +22406,7 @@ async function tickOnce(deps) {
22001
22406
  }
22002
22407
  }
22003
22408
  }
22409
+ deps.stepClock?.mark("0a2b \u043C\u043E\u0441\u0442 \xABsqlite \u0434\u0440\u043E\u043D\u0430 \u2192 \u0437\u0435\u0440\u043A\u0430\u043B\u043E \u2192 \u043F\u0440\u043E\u0434\xBB");
22004
22410
  if (deps.droneBridge) {
22005
22411
  const br = deps.droneBridge;
22006
22412
  const sinceLast = br.lastAt == null ? Infinity : now - br.lastAt;
@@ -22013,6 +22419,7 @@ async function tickOnce(deps) {
22013
22419
  }
22014
22420
  }
22015
22421
  }
22422
+ deps.stepClock?.mark("0a3 \u0441\u0432\u0435\u0440\u043A\u0430 \u043A\u043E\u0434\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u0441 main");
22016
22423
  if (deps.daemonVersion) {
22017
22424
  const dv = deps.daemonVersion;
22018
22425
  const sinceLast = dv.lastAt == null ? Infinity : now - dv.lastAt;
@@ -22021,6 +22428,7 @@ async function tickOnce(deps) {
22021
22428
  await dv.run();
22022
22429
  }
22023
22430
  }
22431
+ deps.stepClock?.mark("0a4 \u043A\u0430\u0440\u0442\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u043C\u0430\u0448\u0438\u043D\u044B");
22024
22432
  if (deps.repoMap) {
22025
22433
  const rm4 = deps.repoMap;
22026
22434
  const sinceLast = rm4.lastAt == null ? Infinity : now - rm4.lastAt;
@@ -22033,6 +22441,7 @@ async function tickOnce(deps) {
22033
22441
  }
22034
22442
  }
22035
22443
  }
22444
+ deps.stepClock?.mark("0b reap \u0432\u043D\u0435\u0448\u043D\u0435-\u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0445");
22036
22445
  if (deps.rehydrate && deps.kill) {
22037
22446
  const kill = deps.kill;
22038
22447
  for (const run2 of deps.db.listRuns()) {
@@ -22046,6 +22455,7 @@ async function tickOnce(deps) {
22046
22455
  });
22047
22456
  }
22048
22457
  }
22458
+ deps.stepClock?.mark("0c \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0445 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432/doc");
22049
22459
  if (deps.sweep) {
22050
22460
  const sw = deps.sweep;
22051
22461
  const sinceLast = sw.lastAt == null ? Infinity : now - sw.lastAt;
@@ -22058,6 +22468,7 @@ async function tickOnce(deps) {
22058
22468
  }
22059
22469
  }
22060
22470
  }
22471
+ deps.stepClock?.mark("0c2 \u0436\u043D\u0435\u0446 worktree");
22061
22472
  if (deps.reapWorktrees) {
22062
22473
  const rw = deps.reapWorktrees;
22063
22474
  const sinceLast = rw.lastAt == null ? Infinity : now - rw.lastAt;
@@ -22070,6 +22481,7 @@ async function tickOnce(deps) {
22070
22481
  }
22071
22482
  }
22072
22483
  }
22484
+ deps.stepClock?.mark("0d \u0445\u043E\u0437\u044F\u0439\u0441\u0442\u0432\u043E \u0434\u043E\u043B\u0433\u043E\u0436\u0438\u0432\u0443\u0449\u0435\u0433\u043E \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430");
22073
22485
  if (deps.logRotate) {
22074
22486
  const lr = deps.logRotate;
22075
22487
  const sinceLast = lr.lastAt == null ? Infinity : now - lr.lastAt;
@@ -22083,6 +22495,7 @@ async function tickOnce(deps) {
22083
22495
  }
22084
22496
  }
22085
22497
  pruneGateLogs(deps);
22498
+ deps.stepClock?.mark("0 \u0448\u0442\u043E\u0440\u043C");
22086
22499
  let stormActive = false;
22087
22500
  if (deps.storm) {
22088
22501
  for (const run2 of deps.db.listRuns("running")) {
@@ -22100,7 +22513,9 @@ async function tickOnce(deps) {
22100
22513
  stormActive = await manageStorm(deps, now, worktreeExists, hasTranscript);
22101
22514
  }
22102
22515
  const droneCap = deps.maxDrones ? await deps.maxDrones() : Infinity;
22516
+ deps.stepClock?.mark("1 \u043F\u043E\u0434\u043D\u044F\u0442\u044C \u043E\u0447\u0435\u0440\u0435\u0434\u044C");
22103
22517
  if (stormActive) noteStormStall(deps);
22518
+ deps.stepClock?.mark("0b \u0432\u0435\u0441 \u0437\u0430\u0434\u0430\u0447\u0438");
22104
22519
  try {
22105
22520
  await manageWeight(deps);
22106
22521
  } catch (e) {
@@ -22111,14 +22526,17 @@ async function tickOnce(deps) {
22111
22526
  } else if (!stormActive) {
22112
22527
  await batchSpawnQueue(deps, droneCap, now);
22113
22528
  }
22529
+ deps.stepClock?.mark("1b \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u0437\u0430\u043F\u0430\u0440\u043A\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u0441\u0442\u043E\u0440\u043E\u0436\u0435\u043C \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430");
22114
22530
  if (!stormActive) {
22115
22531
  await resumeWatchdogParked(deps, droneCap);
22116
22532
  }
22533
+ deps.stepClock?.mark("1c \u043F\u043E\u0432\u0442\u043E\u0440 \u043E\u0442\u043B\u043E\u0436\u0435\u043D\u043D\u043E\u0433\u043E \u0441\u043F\u0430\u0432\u043D\u0430");
22117
22534
  try {
22118
22535
  await retryDeferredSpawns(deps, now, droneCap, stormActive);
22119
22536
  } catch (e) {
22120
22537
  console.error("[overmind] \u043F\u043E\u0432\u0442\u043E\u0440 \u043E\u0442\u043B\u043E\u0436\u0435\u043D\u043D\u043E\u0433\u043E \u0441\u043F\u0430\u0432\u043D\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
22121
22538
  }
22539
+ deps.stepClock?.mark("2 review \u2192 \u0441\u043C\u0435\u0440\u0436\u0435\u043D? \u0437\u0430\u043A\u0440\u044B\u0442\u044C");
22122
22540
  for (let run2 of deps.db.listRuns("review")) {
22123
22541
  try {
22124
22542
  if (run2.resolves_run_id != null) {
@@ -22305,11 +22723,8 @@ async function tickOnce(deps) {
22305
22723
  // рано, спавнить второго нельзя.
22306
22724
  liveOrWaitingReviewers(deps.db, run2.id).length === 0;
22307
22725
  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
- }
22726
+ if (reviewCyclesExhausted(run2)) {
22727
+ await noteReviewCyclesExhausted(deps, run2);
22313
22728
  continue;
22314
22729
  }
22315
22730
  const attempts = deps.db.reviewerAttemptCountFor(run2.id);
@@ -22371,12 +22786,19 @@ async function tickOnce(deps) {
22371
22786
  console.error(`[overmind] \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B review-\u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run2.id} (${run2.task_slug}) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
22372
22787
  }
22373
22788
  }
22789
+ deps.stepClock?.mark("2b \u0434\u0443\u0431\u043B\u0438-\u0440\u0430\u043D\u044B \u0437\u0430\u0434\u0430\u0447\u0438");
22374
22790
  await closeMergedTaskRuns(deps);
22791
+ deps.stepClock?.mark("2b1 \u0430\u0432\u0442\u043E\u043F\u043E\u0434\u044A\u0451\u043C \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044E\u0449\u0435\u0433\u043E");
22375
22792
  await rescueReviewlessPrs(deps, stormActive, now);
22793
+ deps.stepClock?.mark("2b1a \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0431\u0435\u0437 \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438");
22376
22794
  relinkUnboundReviewers(deps);
22795
+ deps.stepClock?.mark("2b2 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u044B-\u0441\u0438\u0440\u043E\u0442\u044B");
22377
22796
  await closeOrphanedReviewers(deps);
22797
+ deps.stepClock?.mark("2b3 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F");
22378
22798
  await spawnVerifiersForOpenPrs(deps, stormActive);
22799
+ deps.stepClock?.mark("2c \u0432\u044B\u0447\u0438\u0441\u043B\u044F\u0435\u043C\u0430\u044F \u0444\u0430\u0437\u0430 \u0440\u0435\u0432\u044C\u044E");
22379
22800
  await refreshReviewPhases(deps);
22801
+ deps.stepClock?.mark("3 liveness/stall/\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435");
22380
22802
  let liveChildPpids;
22381
22803
  try {
22382
22804
  liveChildPpids = deps.livePpids?.() ?? null;
@@ -22465,6 +22887,14 @@ async function tickOnce(deps) {
22465
22887
  continue;
22466
22888
  }
22467
22889
  }
22890
+ if (!alive && run2.status === "running" && !isSubrun(run2) && run2.kind !== "hero") {
22891
+ const capHolder = cycleCapHolder(deps.db, run2);
22892
+ if (capHolder) {
22893
+ 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})`);
22894
+ 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.`);
22895
+ continue;
22896
+ }
22897
+ }
22468
22898
  if (!alive && (outcome === "graceful" || outcome === "aborted") && run2.status === "running" && !isSubrun(run2) && // суб-дроны (резолвер/ревьюер/верификатор) — своими ветками ниже, TASWM-528
22469
22899
  run2.kind !== "hero") {
22470
22900
  if (!wasAborted(outcome) && run2.pr_url) {
@@ -22570,6 +23000,11 @@ async function tickOnce(deps) {
22570
23000
  }
22571
23001
  }
22572
23002
  if (!alive && outcome === "graceful" && run2.status === "running" && isSubrun(run2)) {
23003
+ const capHolder = cycleCapHolder(deps.db, run2);
23004
+ if (capHolder) {
23005
+ 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");
23006
+ continue;
23007
+ }
22573
23008
  const wt = run2.worktree_path;
22574
23009
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(run2);
22575
23010
  if (deps.respawn && canContinue && run2.respawns < RESPAWN_LIMIT) {
@@ -22623,6 +23058,7 @@ async function tickOnce(deps) {
22623
23058
  });
22624
23059
  }
22625
23060
  }
23061
+ deps.stepClock?.mark("3b \u043F\u043E\u043B \u0441\u043D\u0438\u0437\u0443");
22626
23062
  for (const run2 of deps.db.listRuns("paused")) {
22627
23063
  if (run2.kind === "hero") continue;
22628
23064
  if (run2.stop_reason === AWAITING_CI_REASON || isReliefPause(run2.stop_reason)) continue;
@@ -22637,7 +23073,9 @@ async function tickOnce(deps) {
22637
23073
  deps.db.addEvent(run2.id, "stalled", reason);
22638
23074
  narrate(deps, `${droneLabel(run2)} \u0432\u0438\u0441\u0435\u043B paused \u0431\u0435\u0437 \u0434\u0432\u0438\u0436\u0435\u043D\u0438\u044F ${Math.round(STALL_FLOOR_MS / 6e4)} \u043C\u0438\u043D \u2014 \u043F\u043E\u043C\u0435\u0447\u0430\u044E \u0437\u0430\u0432\u0438\u0441\u0448\u0438\u043C (TASWM-275).`);
22639
23075
  }
23076
+ deps.stepClock?.mark("3c \u043F\u0440\u0435\u0434\u0435\u043B \u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044F \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F ci");
22640
23077
  await escalateStaleCiWaits(deps, now);
23078
+ deps.stepClock?.mark("4 \u043F\u0440\u043E\u0431\u0443\u0436\u0434\u0435\u043D\u0438\u0435 \u0437\u0430\u043C\u0435\u0440\u0448\u0438\u0445");
22641
23079
  if (deps.respawn && !stormActive) {
22642
23080
  for (const run2 of deps.db.listRuns("paused")) {
22643
23081
  if (run2.kind === "hero") continue;
@@ -22645,6 +23083,11 @@ async function tickOnce(deps) {
22645
23083
  if (isReliefPause(run2.stop_reason)) continue;
22646
23084
  if (NO_AUTO_WAKE_REASONS.some((r) => run2.stop_reason?.startsWith(r))) continue;
22647
23085
  if (run2.pid != null && deps.isAlive(run2.pid)) continue;
23086
+ const capHolder = cycleCapHolder(deps.db, run2);
23087
+ if (capHolder) {
23088
+ 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");
23089
+ continue;
23090
+ }
22648
23091
  if (!await projectQuotaGateAllows(deps, run2)) continue;
22649
23092
  const wt = run2.worktree_path;
22650
23093
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(run2);
@@ -22662,9 +23105,11 @@ async function tickOnce(deps) {
22662
23105
  }
22663
23106
  }
22664
23107
  }
23108
+ deps.stepClock?.mark("4c \u0433\u0435\u0439\u0442-\u0443\u0432\u0430\u0436\u0430\u044E\u0449\u0438\u0439 \u043F\u043E\u0434\u044A\u0451\u043C \u043E\u0441\u0442\u0430\u0432\u0448\u0438\u0445\u0441\u044F storm_d");
22665
23109
  if (deps.respawn && deps.resourcesOk && !stormActive) {
22666
23110
  await resurrectStormDead(deps, worktreeExists, hasTranscript, { gateOpen: deps.resourcesOk(), cap: droneCap });
22667
23111
  }
23112
+ deps.stepClock?.mark("4d \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B");
22668
23113
  try {
22669
23114
  const reaped = reapOrphanedQuestions(deps.db, now);
22670
23115
  if (reaped.withdrawn > 0) {
@@ -22675,11 +23120,13 @@ async function tickOnce(deps) {
22675
23120
  } catch (e) {
22676
23121
  console.error("[overmind] \u0443\u0431\u043E\u0440\u043A\u0430 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0445 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e);
22677
23122
  }
23123
+ deps.stepClock?.mark("4e \u043E\u0442\u0432\u0435\u0442 \u043D\u0430 \u0432\u043E\u043F\u0440\u043E\u0441 \u043E \u0431\u044E\u0434\u0436\u0435\u0442\u0435");
22678
23124
  try {
22679
23125
  await answerBudgetQuestions(deps, { stormActive, droneCap });
22680
23126
  } catch (e) {
22681
23127
  console.error("[overmind] \u0440\u0430\u0437\u0431\u043E\u0440 \u043E\u0442\u0432\u0435\u0442\u043E\u0432 \u043E \u0431\u044E\u0434\u0436\u0435\u0442\u0435 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
22682
23128
  }
23129
+ deps.stepClock?.mark("4b \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0439 \u043D\u0430\u0434\u0437\u043E\u0440");
22683
23130
  if (deps.anomalies && !deps.control?.paused) {
22684
23131
  const az = deps.anomalies;
22685
23132
  let ready = [];
@@ -22756,6 +23203,7 @@ async function tickOnce(deps) {
22756
23203
  }
22757
23204
  }
22758
23205
  }
23206
+ deps.stepClock?.mark("4c \u0441\u0442\u043E\u0440\u043E\u0436 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430");
22759
23207
  if (deps.watchdog) {
22760
23208
  try {
22761
23209
  const action = decideWatchdogAction(deps.db, now, deps.watchdog.state, deps.watchdog.thresholds, droneCap, { exists: worktreeExists, hasTranscript }, await projectQuotaBlockedRunIds(deps));
@@ -22764,6 +23212,7 @@ async function tickOnce(deps) {
22764
23212
  console.error("[overmind] \u0441\u0442\u043E\u0440\u043E\u0436 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430: \u0442\u0438\u043A \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
22765
23213
  }
22766
23214
  }
23215
+ deps.stepClock?.mark("5 \u0430\u0432\u0442\u043E-\u0438\u043D\u0442\u0435\u0439\u043A");
22767
23216
  const heavyInFlight = heavyRunsInFlight(deps);
22768
23217
  if (heavyInFlight.length && deps.intake) {
22769
23218
  const d = noteDefer(deps.spawnDefer ??= /* @__PURE__ */ new Map(), "weight-intake-hold", intakeHoldReason(heavyInFlight.map(droneLabel).join(", ")), GATE_LOG_HEARTBEAT_TICKS);
@@ -23202,7 +23651,11 @@ async function makeStore(repo, worktreesDir, repoFlag) {
23202
23651
  // Ретрай не молчит: обрыв, переигранный в том же вызове, всё равно виден.
23203
23652
  // Без этой строки самолечение связи — единственный путь, о котором в логе
23204
23653
  // ни слова, и разбор «почему тик подтормозил» упирается в тишину.
23205
- onRetry: ({ tool, attempt, reason }) => console.warn(`[overmind] \u043F\u0440\u043E\u0434-MCP: ${tool} \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0441\u044F (${reason}) \u2014 \u043F\u043E\u0432\u0442\u043E\u0440 ${attempt}.`)
23654
+ onRetry: ({ tool, attempt, reason }) => console.warn(`[overmind] \u043F\u0440\u043E\u0434-MCP: ${tool} \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0441\u044F (${reason}) \u2014 \u043F\u043E\u0432\u0442\u043E\u0440 ${attempt}.`),
23655
+ // TASWM-611: молчащий прод больше не держит проход минутами — остаток идёт без сети.
23656
+ onBudgetSpent: ({ spentMs, budgetMs }) => console.warn(
23657
+ `[overmind] \u043F\u0440\u043E\u0434-MCP \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442: \u043F\u0440\u043E\u0445\u043E\u0434 \u0443\u0436\u0435 \u043F\u043E\u0442\u0440\u0430\u0442\u0438\u043B \u043D\u0430 \u043D\u0435\u0433\u043E ${Math.round(spentMs / 1e3)}\u0441 (\u0431\u044E\u0434\u0436\u0435\u0442 ${Math.round(budgetMs / 1e3)}\u0441) \u2014 \u0434\u043E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043F\u0440\u043E\u0445\u043E\u0434\u0430 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u0431\u0435\u0437 \u0441\u0435\u0442\u0438, \u0437\u0430\u043F\u0438\u0441\u0438 \u0436\u0434\u0443\u0442 \u0432 outbox.`
23658
+ )
23206
23659
  });
23207
23660
  const explicitRegistry = await buildRepoRegistry(repoPaths);
23208
23661
  if (repoPaths.length > 0 && explicitRegistry.length < repoPaths.length) {
@@ -24328,7 +24781,18 @@ async function runDaemon(opts) {
24328
24781
  wakeRun: async (slug) => {
24329
24782
  const run2 = db.activeRunForSlug(slug);
24330
24783
  if (!run2) return null;
24331
- return wakeRun({ db, isAlive: pidAlive, respawn: deps.respawn, narrate: narrator.narrate, ...deps.hasTranscript ? { hasTranscript: deps.hasTranscript } : {} }, run2.id);
24784
+ return wakeRun(
24785
+ {
24786
+ db,
24787
+ isAlive: pidAlive,
24788
+ respawn: deps.respawn,
24789
+ narrate: narrator.narrate,
24790
+ ...deps.hasTranscript ? { hasTranscript: deps.hasTranscript } : {},
24791
+ ...deps.prComment ? { prComment: deps.prComment } : {},
24792
+ ...deps.reportComment ? { reportComment: deps.reportComment } : {}
24793
+ },
24794
+ run2.id
24795
+ );
24332
24796
  },
24333
24797
  update: async (onStep) => {
24334
24798
  const pm = detectGlobalPm(process.argv[1]);
@@ -24478,7 +24942,18 @@ async function runDaemon(opts) {
24478
24942
  // Побудка прогона по id (TASWM-240): вход для `swarm wake <id>` — тот же respawn с
24479
24943
  // полными deps демона (getTask/verifyMode/реестр репо), что и авто-оживление, и по
24480
24944
  // зеркалу демона, а не по короткоживущему стору CLI.
24481
- wake: (runId) => wakeRun({ db, isAlive: pidAlive, respawn: deps.respawn, narrate: narrator.narrate, ...deps.hasTranscript ? { hasTranscript: deps.hasTranscript } : {} }, runId),
24945
+ wake: (runId) => wakeRun(
24946
+ {
24947
+ db,
24948
+ isAlive: pidAlive,
24949
+ respawn: deps.respawn,
24950
+ narrate: narrator.narrate,
24951
+ ...deps.hasTranscript ? { hasTranscript: deps.hasTranscript } : {},
24952
+ ...deps.prComment ? { prComment: deps.prComment } : {},
24953
+ ...deps.reportComment ? { reportComment: deps.reportComment } : {}
24954
+ },
24955
+ runId
24956
+ ),
24482
24957
  replay: (qid, chosen) => replayDecision(
24483
24958
  {
24484
24959
  db,
@@ -24550,12 +25025,14 @@ async function runDaemon(opts) {
24550
25025
  tickHealth.startedAt = Date.now();
24551
25026
  for (; ; ) {
24552
25027
  let tickError;
25028
+ prodMcp?.beginPass?.();
25029
+ deps.stepClock = makeStepClock();
24553
25030
  try {
24554
25031
  writeHeartbeat(HEARTBEAT_PATH, Date.now());
24555
25032
  await tickOnce(deps);
24556
25033
  if (restart.phase === "waiting") {
24557
25034
  const live2 = liveSessionIds(db, pidAlive);
24558
- if (live2.length === 0) performRestart([], restart.intent);
25035
+ if (live2.length === 0 && !hasPreparingRuns(db)) performRestart([], restart.intent);
24559
25036
  else onRestartStillWaiting?.(live2.length);
24560
25037
  }
24561
25038
  } catch (e) {
@@ -24563,6 +25040,8 @@ async function runDaemon(opts) {
24563
25040
  console.error(`[overmind] \u0442\u0438\u043A \u0443\u043F\u0430\u043B: ${describeTickError(e)} \u2014 \u0446\u0438\u043A\u043B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0442 \u0440\u0430\u0431\u043E\u0442\u0443`, e);
24564
25041
  }
24565
25042
  markTickDone(tickHealth, Date.now(), tickError);
25043
+ const slow = slowPassLine(tickHealth.seq, deps.stepClock.finish());
25044
+ if (slow) console.warn(slow);
24566
25045
  await new Promise((r) => setTimeout(r, interval));
24567
25046
  }
24568
25047
  }
@@ -24613,6 +25092,7 @@ export {
24613
25092
  CI_WAKE_LIMIT,
24614
25093
  CONFLICT_RESOLVE_COOLDOWN_MS,
24615
25094
  CONTINUE_NUDGE,
25095
+ CYCLE_CAP_STOP_REASON,
24616
25096
  DEAD_HOLDER_SILENCE_MS,
24617
25097
  EXITED_CONTINUE_NUDGE,
24618
25098
  EXITED_NO_DONE_REASON,
@@ -24639,6 +25119,7 @@ export {
24639
25119
  REVIEW_SPAWN_LIMIT,
24640
25120
  REVIEW_UNRECOVERABLE_MARK,
24641
25121
  SELF_REFERENCE_MARK,
25122
+ SPAWN_INLINE_WAIT_MS,
24642
25123
  STALL_FLOOR_MS,
24643
25124
  STALL_MS,
24644
25125
  STORM_DEAD_LIMIT,
@@ -24658,14 +25139,18 @@ export {
24658
25139
  buildResolverPrompt,
24659
25140
  buildReviewChecklist,
24660
25141
  buildReviewerPrompt,
25142
+ buildSubmittedContinueNudge,
24661
25143
  buildVerifierPrompt,
24662
25144
  cancelRun,
24663
25145
  capacityProvenance,
24664
25146
  configureReviewCycleLimit,
25147
+ cycleCapHolder,
24665
25148
  effectiveReviewCycleLimit,
24666
25149
  hasLiveDescendant,
25150
+ hasPreparingRuns,
24667
25151
  isSubrun,
24668
25152
  lastChangesSummary,
25153
+ lastVerifySummary,
24669
25154
  listDroneProcesses,
24670
25155
  livePpids,
24671
25156
  makeCheckerAttachmentsReader,
@@ -24674,6 +25159,8 @@ export {
24674
25159
  resolveConflict,
24675
25160
  respawnRun,
24676
25161
  reviewBudgetMark,
25162
+ reviewCycleHumanBody,
25163
+ reviewCyclesExhausted,
24677
25164
  reviewDefectClasses,
24678
25165
  reviewUnrecoverableMark,
24679
25166
  reviveStuck,