@taskless-app/swarm 0.6.26 → 0.6.28

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 +271 -218
  2. package/dist/overmind.js +494 -241
  3. package/package.json +2 -2
package/dist/overmind.js CHANGED
@@ -309,52 +309,6 @@ var init_timeline = __esm({
309
309
  }
310
310
  });
311
311
 
312
- // ../swarm-core/src/reviewPhase.ts
313
- function computeReviewPhase(input) {
314
- const { reviewVerdict, ciRed } = input;
315
- if (reviewVerdict === "approve" && !ciRed) {
316
- return { kind: "ready", label: REVIEW_PHASE_TEXT.ready, ready: true };
317
- }
318
- if (ciRed) {
319
- const jobs = (input.ciFailing ?? []).filter((j) => j && j !== "?");
320
- const label3 = jobs.length ? `${REVIEW_PHASE_TEXT.ci_fixing}: ${jobs.join(", ")}` : REVIEW_PHASE_TEXT.ci_fixing;
321
- return { kind: "ci_fixing", label: label3, ready: false };
322
- }
323
- if (reviewVerdict === "changes") {
324
- return { kind: "changes", label: REVIEW_PHASE_TEXT.changes, ready: false };
325
- }
326
- if (input.reviewerName) {
327
- return { kind: "in_review", label: `\u041D\u0430 \u0440\u0435\u0432\u044C\u044E \u0443 ${input.reviewerName}`, ready: false };
328
- }
329
- if (input.reviewerDead) {
330
- return { kind: "reviewer_dead", label: REVIEW_PHASE_TEXT.reviewer_dead, ready: true };
331
- }
332
- const p = input.ciProgress;
333
- if (p && p.total > 0 && p.pass >= p.total) {
334
- return { kind: "awaiting_reviewer", label: REVIEW_PHASE_TEXT.awaiting_reviewer, ready: false };
335
- }
336
- const label2 = p && p.total > 0 ? `${REVIEW_PHASE_TEXT.checks} \u2014 CI ${p.pass}/${p.total}` : REVIEW_PHASE_TEXT.checks;
337
- return { kind: "checks", label: label2, ready: false };
338
- }
339
- var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
340
- var init_reviewPhase = __esm({
341
- "../swarm-core/src/reviewPhase.ts"() {
342
- "use strict";
343
- REVIEW_PHASE_TEXT = {
344
- checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
345
- awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
346
- ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
347
- in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
348
- changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
349
- ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
350
- 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"
351
- };
352
- PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
353
- (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
354
- );
355
- }
356
- });
357
-
358
312
  // ../swarm-core/src/verifyVerdict.ts
359
313
  function isVerifyVerdict(v) {
360
314
  return typeof v === "string" && VERIFY_VERDICT_SET.has(v);
@@ -402,6 +356,68 @@ var init_verifyVerdict = __esm({
402
356
  }
403
357
  });
404
358
 
359
+ // ../swarm-core/src/reviewPhase.ts
360
+ function computeReviewPhase(input) {
361
+ const { reviewVerdict, ciRed } = input;
362
+ const verifyVerdict = input.verifyVerdict ?? null;
363
+ if (!ciRed && isNegativeVerifyVerdict(verifyVerdict)) {
364
+ const kind = verifyVerdict === "found_issues" ? "verify_found" : "verify_not_reproduced";
365
+ return { kind, label: REVIEW_PHASE_TEXT[kind], ready: false };
366
+ }
367
+ if (reviewVerdict === "approve" && !ciRed) {
368
+ if (verifyVerdict === "confirmed" || input.verifyExpected === false) {
369
+ return { kind: "ready", label: REVIEW_PHASE_TEXT.ready, ready: true };
370
+ }
371
+ if (input.verifierDead) {
372
+ return { kind: "verify_dead", label: REVIEW_PHASE_TEXT.verify_dead, ready: true };
373
+ }
374
+ return { kind: "awaiting_verify", label: REVIEW_PHASE_TEXT.awaiting_verify, ready: false };
375
+ }
376
+ if (ciRed) {
377
+ const jobs = (input.ciFailing ?? []).filter((j) => j && j !== "?");
378
+ const label3 = jobs.length ? `${REVIEW_PHASE_TEXT.ci_fixing}: ${jobs.join(", ")}` : REVIEW_PHASE_TEXT.ci_fixing;
379
+ return { kind: "ci_fixing", label: label3, ready: false };
380
+ }
381
+ if (reviewVerdict === "changes") {
382
+ return { kind: "changes", label: REVIEW_PHASE_TEXT.changes, ready: false };
383
+ }
384
+ if (input.reviewerName) {
385
+ return { kind: "in_review", label: `\u041D\u0430 \u0440\u0435\u0432\u044C\u044E \u0443 ${input.reviewerName}`, ready: false };
386
+ }
387
+ if (input.reviewerDead) {
388
+ return { kind: "reviewer_dead", label: REVIEW_PHASE_TEXT.reviewer_dead, ready: true };
389
+ }
390
+ const p = input.ciProgress;
391
+ if (p && p.total > 0 && p.pass >= p.total) {
392
+ return { kind: "awaiting_reviewer", label: REVIEW_PHASE_TEXT.awaiting_reviewer, ready: false };
393
+ }
394
+ const label2 = p && p.total > 0 ? `${REVIEW_PHASE_TEXT.checks} \u2014 CI ${p.pass}/${p.total}` : REVIEW_PHASE_TEXT.checks;
395
+ return { kind: "checks", label: label2, ready: false };
396
+ }
397
+ var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
398
+ var init_reviewPhase = __esm({
399
+ "../swarm-core/src/reviewPhase.ts"() {
400
+ "use strict";
401
+ init_verifyVerdict();
402
+ REVIEW_PHASE_TEXT = {
403
+ checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
404
+ awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
405
+ ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
406
+ in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
407
+ changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
408
+ verify_found: "\u0412\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043D\u0430\u0448\u0451\u043B \u2014 \u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438",
409
+ verify_not_reproduced: "\u0412\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043D\u0435 \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u0451\u043B \u2014 \u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438",
410
+ awaiting_verify: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u043C \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E",
411
+ 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",
412
+ ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
413
+ 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"
414
+ };
415
+ PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
416
+ (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
417
+ );
418
+ }
419
+ });
420
+
405
421
  // ../swarm-core/src/mergeGate.ts
406
422
  function computeMergeGate(input) {
407
423
  if (!input.hasRun) {
@@ -1709,6 +1725,57 @@ var init_zoneMemory = __esm({
1709
1725
  }
1710
1726
  });
1711
1727
 
1728
+ // ../swarm-core/src/taskPrs.ts
1729
+ function normalizePrUrl(raw) {
1730
+ const s = (raw ?? "").trim();
1731
+ if (!s) return null;
1732
+ const m = s.match(/^(?:https?:\/\/)?(?:www\.)?github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)/i);
1733
+ if (!m) return s.replace(/\/+$/, "").toLowerCase();
1734
+ return `github.com/${m[1]}/${m[2]}/pull/${m[3]}`.toLowerCase();
1735
+ }
1736
+ function samePr(a, b) {
1737
+ const x = normalizePrUrl(a);
1738
+ const y = normalizePrUrl(b);
1739
+ return x != null && y != null && x === y;
1740
+ }
1741
+ function prBelongsToTask(slug, pr) {
1742
+ const s = (slug ?? "").trim().toLowerCase();
1743
+ if (!s) return false;
1744
+ const branch = (pr.headRefName ?? "").trim().toLowerCase();
1745
+ if (branch === s || branch.startsWith(s) && /^[-/_.]/.test(branch.slice(s.length))) return true;
1746
+ const title = (pr.title ?? "").toLowerCase();
1747
+ if (!title.includes(s)) return false;
1748
+ const at = title.indexOf(s);
1749
+ const before = at > 0 ? title[at - 1] : "";
1750
+ const after = title[at + s.length] ?? "";
1751
+ return !/[a-z0-9]/.test(before) && !/[a-z0-9-]/.test(after);
1752
+ }
1753
+ function decideTaskWork(facts) {
1754
+ const candidate = (facts.mergedCandidate ?? "").trim() || null;
1755
+ const open = [];
1756
+ for (const raw of facts.openPrs ?? []) {
1757
+ const url = (raw ?? "").trim();
1758
+ if (!url) continue;
1759
+ if (!open.some((u) => samePr(u, url))) open.push(url);
1760
+ }
1761
+ if (open.length > 0) return { kind: "open", prs: open, candidate: candidate && !open.some((u) => samePr(u, candidate)) ? candidate : null };
1762
+ if (!candidate) return { kind: "unmerged" };
1763
+ return { kind: "merged", pr: candidate };
1764
+ }
1765
+ function formatMergedDuplicateReason(pr) {
1766
+ return `PR \u0437\u0430\u0434\u0430\u0447\u0438 \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D (${pr}) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D-\u0434\u0443\u0431\u043B\u044C \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u0441\u043F\u0430\u0432\u043D\u0430/\u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0438 (TASWM-206)`;
1767
+ }
1768
+ function formatOpenPrHold(v) {
1769
+ const list = v.prs.join(", ");
1770
+ const tail2 = v.candidate ? ` (\u0441\u043C\u0435\u0440\u0436\u0435\u043D ${v.candidate}, \u043D\u043E \u044D\u0442\u043E \u043D\u0435 \u0432\u0441\u044F \u0440\u0430\u0431\u043E\u0442\u0430)` : "";
1771
+ return `\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 \u043E\u0441\u0442\u0430\u043B\u0438\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0435 PR: ${list}${tail2} \u2014 \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u0432\u043B\u0438\u0442\u0430 \u0446\u0435\u043B\u0438\u043A\u043E\u043C, \u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u043C \u0438 \u043F\u0440\u043E\u0433\u043E\u043D \u0434\u0443\u0431\u043B\u0435\u043C \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u043C (TASWM-515)`;
1772
+ }
1773
+ var init_taskPrs = __esm({
1774
+ "../swarm-core/src/taskPrs.ts"() {
1775
+ "use strict";
1776
+ }
1777
+ });
1778
+
1712
1779
  // ../swarm-core/src/index.ts
1713
1780
  var init_src = __esm({
1714
1781
  "../swarm-core/src/index.ts"() {
@@ -1743,6 +1810,7 @@ var init_src = __esm({
1743
1810
  init_effort();
1744
1811
  init_queuePlan();
1745
1812
  init_zoneMemory();
1813
+ init_taskPrs();
1746
1814
  }
1747
1815
  });
1748
1816
 
@@ -3024,76 +3092,12 @@ var init_storm = __esm({
3024
3092
  }
3025
3093
  });
3026
3094
 
3027
- // src/heartbeat.ts
3028
- import { readFileSync as readFileSync6, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
3029
- function writeHeartbeat(path, now) {
3030
- const tmp = `${path}.tmp`;
3031
- writeFileSync6(tmp, String(now));
3032
- renameSync4(tmp, path);
3033
- }
3034
- function readHeartbeat(path) {
3035
- try {
3036
- const ms = Number(readFileSync6(path, "utf8").trim());
3037
- return Number.isFinite(ms) ? ms : null;
3038
- } catch {
3039
- return null;
3040
- }
3041
- }
3042
- function overmindAlive(path, now, staleMs) {
3043
- const ts = readHeartbeat(path);
3044
- if (ts == null) return false;
3045
- return now - ts < staleMs;
3046
- }
3047
- var init_heartbeat = __esm({
3048
- "src/heartbeat.ts"() {
3049
- "use strict";
3050
- }
3051
- });
3052
-
3053
- // src/logNoise.ts
3054
- function timesWord(n) {
3055
- const tens = n % 100;
3056
- if (tens >= 11 && tens <= 14) return "\u0440\u0430\u0437";
3057
- return n % 10 >= 2 && n % 10 <= 4 ? "\u0440\u0430\u0437\u0430" : "\u0440\u0430\u0437";
3058
- }
3059
- function makeRepeatFilter(opts = {}) {
3060
- const remindAfterMs = opts.remindAfterMs ?? DEFAULT_REMIND_MS;
3061
- const streaks = /* @__PURE__ */ new Map();
3062
- return {
3063
- say(key, message, now) {
3064
- const prev = streaks.get(key);
3065
- if (!prev || prev.message !== message) {
3066
- const tail2 = prev && prev.hidden > 0 ? ` (\u0434\u043E \u044D\u0442\u043E\u0433\u043E \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 \u043C\u043E\u043B\u0447\u0430 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u043B\u043E\u0441\u044C \u0435\u0449\u0451 ${prev.hidden} ${timesWord(prev.hidden)})` : "";
3067
- streaks.set(key, { message, since: now, saidAt: now, hidden: 0 });
3068
- return `${message}${tail2}`;
3069
- }
3070
- prev.hidden += 1;
3071
- if (now - prev.saidAt < remindAfterMs) return null;
3072
- const hidden = prev.hidden;
3073
- const minutes = Math.max(1, Math.round((now - prev.since) / 6e4));
3074
- prev.saidAt = now;
3075
- prev.hidden = 0;
3076
- return `${message} \u2014 \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 ${hidden} ${timesWord(hidden)} \u043F\u043E\u0434\u0440\u044F\u0434, \u0442\u044F\u043D\u0435\u0442\u0441\u044F ${minutes} \u043C\u0438\u043D`;
3077
- },
3078
- forget(key) {
3079
- streaks.delete(key);
3080
- }
3081
- };
3082
- }
3083
- var DEFAULT_REMIND_MS;
3084
- var init_logNoise = __esm({
3085
- "src/logNoise.ts"() {
3086
- "use strict";
3087
- DEFAULT_REMIND_MS = 10 * 6e4;
3088
- }
3089
- });
3090
-
3091
3095
  // src/agentToken.ts
3092
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
3093
- import { join as join9 } from "path";
3096
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
3097
+ import { join as join7 } from "path";
3094
3098
  function loadAgentToken() {
3095
3099
  try {
3096
- const s = JSON.parse(readFileSync7(STORE_PATH, "utf8"));
3100
+ const s = JSON.parse(readFileSync5(STORE_PATH, "utf8"));
3097
3101
  return s.token ? s : null;
3098
3102
  } catch {
3099
3103
  return null;
@@ -3146,7 +3150,7 @@ var init_agentToken = __esm({
3146
3150
  "src/agentToken.ts"() {
3147
3151
  "use strict";
3148
3152
  init_config();
3149
- STORE_PATH = join9(SWARM_HOME, "agent.json");
3153
+ STORE_PATH = join7(SWARM_HOME, "agent.json");
3150
3154
  base = (api) => api.replace(/\/$/, "");
3151
3155
  TOKEN_PROBE_TIMEOUT_MS = 8e3;
3152
3156
  }
@@ -3187,6 +3191,142 @@ var init_linkFailure = __esm({
3187
3191
  }
3188
3192
  });
3189
3193
 
3194
+ // src/toolFailure.ts
3195
+ function trimDetail(s) {
3196
+ const t = s.trim();
3197
+ return t.length <= FAILURE_DETAIL_MAX ? t : `${t.slice(0, FAILURE_DETAIL_MAX)}\u2026`;
3198
+ }
3199
+ function classifyToolFailure(raw) {
3200
+ if (MISMATCH_RE.test(raw)) return TOOL_FAILURE.WORKSPACE_MISMATCH;
3201
+ if (FORBIDDEN_RE.test(raw)) return TOOL_FAILURE.FORBIDDEN;
3202
+ if (NOT_FOUND_RE.test(raw)) return TOOL_FAILURE.NOT_FOUND;
3203
+ return TOOL_FAILURE.TOOL_ERROR;
3204
+ }
3205
+ function classifyThrown(e) {
3206
+ const { kind, detail } = explainLinkFailure(e);
3207
+ const code = kind === "offline" ? TOOL_FAILURE.UNREACHABLE : kind === "unauthorized" ? TOOL_FAILURE.UNAUTHORIZED : (
3208
+ // «ручки нет» и «сервер отказал по существу» разбираем доменно: там может быть
3209
+ // и task_not_found, и отказ по правам — то есть наши коды, а не транспортные.
3210
+ classifyToolFailure(detail)
3211
+ );
3212
+ return { code, detail };
3213
+ }
3214
+ function describeToolFailure(f) {
3215
+ const text = TOOL_FAILURE_TEXT[f.code];
3216
+ const detail = trimDetail(f.detail);
3217
+ return detail ? `${text} (${detail})` : text;
3218
+ }
3219
+ var TOOL_FAILURE, TOOL_FAILURE_TEXT, TOOL_FAILURE_RETRIABLE, FAILURE_DETAIL_MAX, NOT_FOUND_RE, FORBIDDEN_RE, MISMATCH_RE;
3220
+ var init_toolFailure = __esm({
3221
+ "src/toolFailure.ts"() {
3222
+ "use strict";
3223
+ init_linkFailure();
3224
+ TOOL_FAILURE = {
3225
+ /** Тул отработал, но искомого в ответе нет: задачи с таким слагом в воркспейсе не существует. */
3226
+ NOT_FOUND: "not_found",
3227
+ /** Объект есть, но токену роя не виден (приватный проект, спейс, чужой воркспейс). */
3228
+ FORBIDDEN: "forbidden",
3229
+ /** Токен роя не состоит в воркспейсе, которым искали. */
3230
+ WORKSPACE_MISMATCH: "workspace_mismatch",
3231
+ /** До прода не достучались: сеть, DNS, таймаут, оборванный сокет. */
3232
+ UNREACHABLE: "unreachable",
3233
+ /** Прод ответил, но токен роя отвергнут (отозван, протух). Сетью это не лечится. */
3234
+ UNAUTHORIZED: "unauthorized",
3235
+ /** Тул ответил ошибкой, которую мы не узнали, — текст кладём в detail как есть. */
3236
+ TOOL_ERROR: "tool_error",
3237
+ /** Успех, но полезной нагрузки в ответе нет (форма ответа разошлась с ожиданием). */
3238
+ EMPTY: "empty"
3239
+ };
3240
+ TOOL_FAILURE_TEXT = {
3241
+ [TOOL_FAILURE.NOT_FOUND]: "\u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435\u0442 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043B\u0430\u0433 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
3242
+ [TOOL_FAILURE.FORBIDDEN]: "\u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0435\u0442: \u043E\u0431\u044A\u0435\u043A\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0440\u043E\u044F \u043D\u0435 \u0432\u0438\u0434\u0435\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
3243
+ [TOOL_FAILURE.WORKSPACE_MISMATCH]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435",
3244
+ [TOOL_FAILURE.UNREACHABLE]: "Taskless \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u2014 \u0441\u0435\u0442\u044C, DNS \u0438\u043B\u0438 \u0442\u0430\u0439\u043C\u0430\u0443\u0442",
3245
+ [TOOL_FAILURE.UNAUTHORIZED]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442 \u043F\u0440\u043E\u0434\u043E\u043C \u2014 \u043D\u0443\u0436\u0435\u043D swarm login \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435",
3246
+ [TOOL_FAILURE.TOOL_ERROR]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0448\u0438\u0431\u043A\u043E\u0439",
3247
+ [TOOL_FAILURE.EMPTY]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0443\u0441\u043F\u0435\u0445\u043E\u043C, \u043D\u043E \u0434\u0430\u043D\u043D\u044B\u0445 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442"
3248
+ };
3249
+ TOOL_FAILURE_RETRIABLE = {
3250
+ [TOOL_FAILURE.NOT_FOUND]: false,
3251
+ [TOOL_FAILURE.FORBIDDEN]: false,
3252
+ [TOOL_FAILURE.WORKSPACE_MISMATCH]: false,
3253
+ [TOOL_FAILURE.UNREACHABLE]: true,
3254
+ // Отозванный токен повтором не чинится — его меняет человек (`swarm login`).
3255
+ [TOOL_FAILURE.UNAUTHORIZED]: false,
3256
+ [TOOL_FAILURE.TOOL_ERROR]: false,
3257
+ [TOOL_FAILURE.EMPTY]: false
3258
+ };
3259
+ FAILURE_DETAIL_MAX = 300;
3260
+ NOT_FOUND_RE = /task_not_found|not_found|не найдена|не найден/i;
3261
+ FORBIDDEN_RE = /forbidden|not visible|no access|нет доступа|(?:http|status|code)[ =:]*403\b|\(403\)|403 forbidden/i;
3262
+ MISMATCH_RE = /workspace_mismatch|not a member of that workspace/i;
3263
+ }
3264
+ });
3265
+
3266
+ // src/heartbeat.ts
3267
+ import { readFileSync as readFileSync7, renameSync as renameSync4, writeFileSync as writeFileSync7 } from "fs";
3268
+ function writeHeartbeat(path, now) {
3269
+ const tmp = `${path}.tmp`;
3270
+ writeFileSync7(tmp, String(now));
3271
+ renameSync4(tmp, path);
3272
+ }
3273
+ function readHeartbeat(path) {
3274
+ try {
3275
+ const ms = Number(readFileSync7(path, "utf8").trim());
3276
+ return Number.isFinite(ms) ? ms : null;
3277
+ } catch {
3278
+ return null;
3279
+ }
3280
+ }
3281
+ function overmindAlive(path, now, staleMs) {
3282
+ const ts = readHeartbeat(path);
3283
+ if (ts == null) return false;
3284
+ return now - ts < staleMs;
3285
+ }
3286
+ var init_heartbeat = __esm({
3287
+ "src/heartbeat.ts"() {
3288
+ "use strict";
3289
+ }
3290
+ });
3291
+
3292
+ // src/logNoise.ts
3293
+ function timesWord(n) {
3294
+ const tens = n % 100;
3295
+ if (tens >= 11 && tens <= 14) return "\u0440\u0430\u0437";
3296
+ return n % 10 >= 2 && n % 10 <= 4 ? "\u0440\u0430\u0437\u0430" : "\u0440\u0430\u0437";
3297
+ }
3298
+ function makeRepeatFilter(opts = {}) {
3299
+ const remindAfterMs = opts.remindAfterMs ?? DEFAULT_REMIND_MS;
3300
+ const streaks = /* @__PURE__ */ new Map();
3301
+ return {
3302
+ say(key, message, now) {
3303
+ const prev = streaks.get(key);
3304
+ if (!prev || prev.message !== message) {
3305
+ const tail2 = prev && prev.hidden > 0 ? ` (\u0434\u043E \u044D\u0442\u043E\u0433\u043E \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 \u043C\u043E\u043B\u0447\u0430 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u043B\u043E\u0441\u044C \u0435\u0449\u0451 ${prev.hidden} ${timesWord(prev.hidden)})` : "";
3306
+ streaks.set(key, { message, since: now, saidAt: now, hidden: 0 });
3307
+ return `${message}${tail2}`;
3308
+ }
3309
+ prev.hidden += 1;
3310
+ if (now - prev.saidAt < remindAfterMs) return null;
3311
+ const hidden = prev.hidden;
3312
+ const minutes = Math.max(1, Math.round((now - prev.since) / 6e4));
3313
+ prev.saidAt = now;
3314
+ prev.hidden = 0;
3315
+ return `${message} \u2014 \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 ${hidden} ${timesWord(hidden)} \u043F\u043E\u0434\u0440\u044F\u0434, \u0442\u044F\u043D\u0435\u0442\u0441\u044F ${minutes} \u043C\u0438\u043D`;
3316
+ },
3317
+ forget(key) {
3318
+ streaks.delete(key);
3319
+ }
3320
+ };
3321
+ }
3322
+ var DEFAULT_REMIND_MS;
3323
+ var init_logNoise = __esm({
3324
+ "src/logNoise.ts"() {
3325
+ "use strict";
3326
+ DEFAULT_REMIND_MS = 10 * 6e4;
3327
+ }
3328
+ });
3329
+
3190
3330
  // src/procList.ts
3191
3331
  function parsePsRows(out) {
3192
3332
  const rows = [];
@@ -4836,78 +4976,6 @@ var init_oauth = __esm({
4836
4976
  }
4837
4977
  });
4838
4978
 
4839
- // src/toolFailure.ts
4840
- function trimDetail(s) {
4841
- const t = s.trim();
4842
- return t.length <= FAILURE_DETAIL_MAX ? t : `${t.slice(0, FAILURE_DETAIL_MAX)}\u2026`;
4843
- }
4844
- function classifyToolFailure(raw) {
4845
- if (MISMATCH_RE.test(raw)) return TOOL_FAILURE.WORKSPACE_MISMATCH;
4846
- if (FORBIDDEN_RE.test(raw)) return TOOL_FAILURE.FORBIDDEN;
4847
- if (NOT_FOUND_RE.test(raw)) return TOOL_FAILURE.NOT_FOUND;
4848
- return TOOL_FAILURE.TOOL_ERROR;
4849
- }
4850
- function classifyThrown(e) {
4851
- const { kind, detail } = explainLinkFailure(e);
4852
- const code = kind === "offline" ? TOOL_FAILURE.UNREACHABLE : kind === "unauthorized" ? TOOL_FAILURE.UNAUTHORIZED : (
4853
- // «ручки нет» и «сервер отказал по существу» разбираем доменно: там может быть
4854
- // и task_not_found, и отказ по правам — то есть наши коды, а не транспортные.
4855
- classifyToolFailure(detail)
4856
- );
4857
- return { code, detail };
4858
- }
4859
- function describeToolFailure(f) {
4860
- const text = TOOL_FAILURE_TEXT[f.code];
4861
- const detail = trimDetail(f.detail);
4862
- return detail ? `${text} (${detail})` : text;
4863
- }
4864
- var TOOL_FAILURE, TOOL_FAILURE_TEXT, TOOL_FAILURE_RETRIABLE, FAILURE_DETAIL_MAX, NOT_FOUND_RE, FORBIDDEN_RE, MISMATCH_RE;
4865
- var init_toolFailure = __esm({
4866
- "src/toolFailure.ts"() {
4867
- "use strict";
4868
- init_linkFailure();
4869
- TOOL_FAILURE = {
4870
- /** Тул отработал, но искомого в ответе нет: задачи с таким слагом в воркспейсе не существует. */
4871
- NOT_FOUND: "not_found",
4872
- /** Объект есть, но токену роя не виден (приватный проект, спейс, чужой воркспейс). */
4873
- FORBIDDEN: "forbidden",
4874
- /** Токен роя не состоит в воркспейсе, которым искали. */
4875
- WORKSPACE_MISMATCH: "workspace_mismatch",
4876
- /** До прода не достучались: сеть, DNS, таймаут, оборванный сокет. */
4877
- UNREACHABLE: "unreachable",
4878
- /** Прод ответил, но токен роя отвергнут (отозван, протух). Сетью это не лечится. */
4879
- UNAUTHORIZED: "unauthorized",
4880
- /** Тул ответил ошибкой, которую мы не узнали, — текст кладём в detail как есть. */
4881
- TOOL_ERROR: "tool_error",
4882
- /** Успех, но полезной нагрузки в ответе нет (форма ответа разошлась с ожиданием). */
4883
- EMPTY: "empty"
4884
- };
4885
- TOOL_FAILURE_TEXT = {
4886
- [TOOL_FAILURE.NOT_FOUND]: "\u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435\u0442 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043B\u0430\u0433 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
4887
- [TOOL_FAILURE.FORBIDDEN]: "\u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0435\u0442: \u043E\u0431\u044A\u0435\u043A\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0440\u043E\u044F \u043D\u0435 \u0432\u0438\u0434\u0435\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
4888
- [TOOL_FAILURE.WORKSPACE_MISMATCH]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435",
4889
- [TOOL_FAILURE.UNREACHABLE]: "Taskless \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u2014 \u0441\u0435\u0442\u044C, DNS \u0438\u043B\u0438 \u0442\u0430\u0439\u043C\u0430\u0443\u0442",
4890
- [TOOL_FAILURE.UNAUTHORIZED]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442 \u043F\u0440\u043E\u0434\u043E\u043C \u2014 \u043D\u0443\u0436\u0435\u043D swarm login \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435",
4891
- [TOOL_FAILURE.TOOL_ERROR]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0448\u0438\u0431\u043A\u043E\u0439",
4892
- [TOOL_FAILURE.EMPTY]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0443\u0441\u043F\u0435\u0445\u043E\u043C, \u043D\u043E \u0434\u0430\u043D\u043D\u044B\u0445 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442"
4893
- };
4894
- TOOL_FAILURE_RETRIABLE = {
4895
- [TOOL_FAILURE.NOT_FOUND]: false,
4896
- [TOOL_FAILURE.FORBIDDEN]: false,
4897
- [TOOL_FAILURE.WORKSPACE_MISMATCH]: false,
4898
- [TOOL_FAILURE.UNREACHABLE]: true,
4899
- // Отозванный токен повтором не чинится — его меняет человек (`swarm login`).
4900
- [TOOL_FAILURE.UNAUTHORIZED]: false,
4901
- [TOOL_FAILURE.TOOL_ERROR]: false,
4902
- [TOOL_FAILURE.EMPTY]: false
4903
- };
4904
- FAILURE_DETAIL_MAX = 300;
4905
- NOT_FOUND_RE = /task_not_found|not_found|не найдена|не найден/i;
4906
- FORBIDDEN_RE = /forbidden|not visible|no access|нет доступа|(?:http|status|code)[ =:]*403\b|\(403\)|403 forbidden/i;
4907
- MISMATCH_RE = /workspace_mismatch|not a member of that workspace/i;
4908
- }
4909
- });
4910
-
4911
4979
  // src/authState.ts
4912
4980
  function noteTokenRejected(nowMs = Date.now()) {
4913
4981
  if (!denial) {
@@ -6621,7 +6689,7 @@ init_config();
6621
6689
  init_resources();
6622
6690
  init_names2();
6623
6691
  init_storm();
6624
- import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
6692
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
6625
6693
  import { dirname as dirname2 } from "path";
6626
6694
  import { randomUUID } from "crypto";
6627
6695
 
@@ -6673,13 +6741,16 @@ function renderPendingRulesBlock(rules) {
6673
6741
  ].join("\n");
6674
6742
  }
6675
6743
 
6744
+ // src/swarmClient.ts
6745
+ init_toolFailure();
6746
+
6676
6747
  // src/logTail.ts
6677
6748
  import { closeSync, existsSync as existsSync6, openSync, readSync, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
6678
- import { join as join8 } from "path";
6749
+ import { join as join9 } from "path";
6679
6750
 
6680
6751
  // src/runLogDir.ts
6681
6752
  import { existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync4, renameSync as renameSync3, statSync as statSync3 } from "fs";
6682
- import { join as join7 } from "path";
6753
+ import { join as join8 } from "path";
6683
6754
  var LEGACY_RUN_LOGS_SUBDIR = "legacy";
6684
6755
  function runLogDirName(runLogKey) {
6685
6756
  const safe2 = runLogKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
@@ -6687,7 +6758,7 @@ function runLogDirName(runLogKey) {
6687
6758
  return /^\d+$/.test(safe2) ? `k-${safe2}` : safe2;
6688
6759
  }
6689
6760
  function runLogDirPath(runLogsDir, runLogKey) {
6690
- return join7(runLogsDir, runLogDirName(runLogKey));
6761
+ return join8(runLogsDir, runLogDirName(runLogKey));
6691
6762
  }
6692
6763
  function findRunLogDir(runLogsDir, keys) {
6693
6764
  for (const key of keys) {
@@ -6702,7 +6773,7 @@ function runLogDirForWrite(runLogsDir, keys) {
6702
6773
  return findRunLogDir(runLogsDir, keys) ?? runLogDirPath(runLogsDir, canonical);
6703
6774
  }
6704
6775
  function sweepLegacyRunLogs(runLogsDir) {
6705
- const legacyDir = join7(runLogsDir, LEGACY_RUN_LOGS_SUBDIR);
6776
+ const legacyDir = join8(runLogsDir, LEGACY_RUN_LOGS_SUBDIR);
6706
6777
  let entries;
6707
6778
  try {
6708
6779
  entries = readdirSync4(runLogsDir);
@@ -6713,12 +6784,12 @@ function sweepLegacyRunLogs(runLogsDir) {
6713
6784
  let failed = 0;
6714
6785
  for (const name of entries) {
6715
6786
  if (!/^\d+$/.test(name)) continue;
6716
- const from = join7(runLogsDir, name);
6787
+ const from = join8(runLogsDir, name);
6717
6788
  try {
6718
6789
  if (!statSync3(from).isDirectory()) continue;
6719
6790
  mkdirSync5(legacyDir, { recursive: true });
6720
- let to = join7(legacyDir, name);
6721
- for (let n = 2; existsSync5(to) && n < 100; n++) to = join7(legacyDir, `${name}-${n}`);
6791
+ let to = join8(legacyDir, name);
6792
+ for (let n = 2; existsSync5(to) && n < 100; n++) to = join8(legacyDir, `${name}-${n}`);
6722
6793
  renameSync3(from, to);
6723
6794
  moved++;
6724
6795
  } catch {
@@ -6784,8 +6855,8 @@ function latestRunLaunch(runLogsDir, runLogKeys) {
6784
6855
  }
6785
6856
  if (indexes.size === 0) return null;
6786
6857
  const index = Math.max(...indexes);
6787
- const out = [join8(dir, `${index}.json`), join8(dir, `${index}.json.done`)].find((p) => existsSync6(p)) ?? null;
6788
- const err = existsSync6(join8(dir, `${index}.err.log`)) ? join8(dir, `${index}.err.log`) : null;
6858
+ const out = [join9(dir, `${index}.json`), join9(dir, `${index}.json.done`)].find((p) => existsSync6(p)) ?? null;
6859
+ const err = existsSync6(join9(dir, `${index}.err.log`)) ? join9(dir, `${index}.err.log`) : null;
6789
6860
  return { index, total: indexes.size, out, err };
6790
6861
  }
6791
6862
  function runLogTail(runLogsDir, runLogKeys, opts) {
@@ -6872,6 +6943,14 @@ function toolErrorText(res) {
6872
6943
  if (!text) return "\u043F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0440\u0433 \u0432\u044B\u0437\u043E\u0432 \u0431\u0435\u0437 \u043E\u0431\u044A\u044F\u0441\u043D\u0435\u043D\u0438\u044F";
6873
6944
  return text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
6874
6945
  }
6946
+ var DEPS_UNRETRIABLE_CODES = /* @__PURE__ */ new Set([
6947
+ TOOL_FAILURE.FORBIDDEN,
6948
+ TOOL_FAILURE.UNAUTHORIZED,
6949
+ TOOL_FAILURE.WORKSPACE_MISMATCH
6950
+ ]);
6951
+ function depsFailureRetriable(code) {
6952
+ return !DEPS_UNRETRIABLE_CODES.has(code);
6953
+ }
6875
6954
  function runRef(runKey) {
6876
6955
  return { __runRef: runKey };
6877
6956
  }
@@ -6968,6 +7047,13 @@ var SwarmClient = class _SwarmClient {
6968
7047
  // дальше удвоением до потолка TTL: устойчиво отвергающий прод (старее демона, скоуп
6969
7048
  // токена) не должен получать вопрос каждые четыре секунды до конца своей жизни.
6970
7049
  static DEPS_RETRY_BASE_MS = 5e3;
7050
+ // TASWM-517: сколько раз спрашиваем НЕСАМОЛЕЧАЩИЙСЯ отказ, прежде чем перестать вовсе.
7051
+ // `agent_forbidden` («тул недоступен аккаунту-агенту роя») повтором не чинится: 26.08
7052
+ // два прогона получили его по 53 раза каждый и остались в очереди навсегда. Три захода
7053
+ // при backoff 5с/10с — это ~15 секунд: запас на случай, если классификатор ошибся и
7054
+ // отказ на деле транзиентный, но не час молчаливого простоя. По исчерпании прогон
7055
+ // роняет гейт спавна (DEPS_UNRETRIABLE_ATTEMPTS читается и там).
7056
+ static DEPS_UNRETRIABLE_ATTEMPTS = 3;
6971
7057
  // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
6972
7058
  // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
6973
7059
  // каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
@@ -7039,13 +7125,13 @@ var SwarmClient = class _SwarmClient {
7039
7125
  recover() {
7040
7126
  if (existsSync7(this.mapPath)) {
7041
7127
  try {
7042
- const obj = JSON.parse(readFileSync5(this.mapPath, "utf8"));
7128
+ const obj = JSON.parse(readFileSync6(this.mapPath, "utf8"));
7043
7129
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
7044
7130
  } catch {
7045
7131
  }
7046
7132
  }
7047
7133
  if (existsSync7(this.outboxPath)) {
7048
- const txt = readFileSync5(this.outboxPath, "utf8");
7134
+ const txt = readFileSync6(this.outboxPath, "utf8");
7049
7135
  for (const line of txt.split("\n")) {
7050
7136
  const s = line.trim();
7051
7137
  if (!s) continue;
@@ -7068,7 +7154,7 @@ var SwarmClient = class _SwarmClient {
7068
7154
  if (!existsSync7(this.stormPath)) return;
7069
7155
  let txt;
7070
7156
  try {
7071
- txt = readFileSync5(this.stormPath, "utf8");
7157
+ txt = readFileSync6(this.stormPath, "utf8");
7072
7158
  } catch {
7073
7159
  return;
7074
7160
  }
@@ -7573,11 +7659,12 @@ var SwarmClient = class _SwarmClient {
7573
7659
  res = await this.call("swarm_deps_status", { refs: ask });
7574
7660
  } catch (e) {
7575
7661
  for (const ref of ask) this.depsAskedAt.delete(ref);
7576
- this.noteDepsUnanswered(ask, nowMs, String(e));
7662
+ this.noteDepsUnanswered(ask, nowMs, String(e), depsFailureRetriable(classifyThrown(e).code));
7577
7663
  return;
7578
7664
  }
7579
7665
  if (res?.isError) {
7580
- this.noteDepsUnanswered(ask, nowMs, toolErrorText(res));
7666
+ const text = toolErrorText(res);
7667
+ this.noteDepsUnanswered(ask, nowMs, text, depsFailureRetriable(classifyToolFailure(text)));
7581
7668
  return;
7582
7669
  }
7583
7670
  const deps = res.structuredContent?.deps ?? [];
@@ -7617,6 +7704,7 @@ var SwarmClient = class _SwarmClient {
7617
7704
  depsRetryDelay(ref) {
7618
7705
  const fail = this.depsUnanswered.get(ref);
7619
7706
  if (fail) {
7707
+ if (!fail.retriable && fail.attempts >= _SwarmClient.DEPS_UNRETRIABLE_ATTEMPTS) return _SwarmClient.DEPS_TTL_MS;
7620
7708
  const backoff = _SwarmClient.DEPS_RETRY_BASE_MS * 2 ** Math.min(fail.attempts - 1, 10);
7621
7709
  return Math.min(backoff, _SwarmClient.DEPS_TTL_MS);
7622
7710
  }
@@ -7625,17 +7713,19 @@ var SwarmClient = class _SwarmClient {
7625
7713
  return _SwarmClient.DEPS_TTL_MS;
7626
7714
  }
7627
7715
  /** Запомнить, что прод по этим ссылкам не ответил (см. depsUnanswered / TASWM-486). */
7628
- noteDepsUnanswered(refs, nowMs, error) {
7716
+ noteDepsUnanswered(refs, nowMs, error, retriable = true) {
7629
7717
  for (const ref of refs) {
7630
7718
  const prev = this.depsUnanswered.get(ref);
7719
+ const sameKind = prev != null && prev.retriable === retriable;
7631
7720
  this.depsUnanswered.set(ref, {
7632
7721
  ref,
7633
7722
  slug: this.depSlug.get(ref) ?? ref,
7634
7723
  // `since` — начало ЭПИЗОДА слепоты, а не последняя попытка: именно возраст эпизода
7635
7724
  // отвечает на вопрос «сколько прогон уже стоит вслепую».
7636
7725
  since: prev?.since ?? nowMs,
7637
- attempts: (prev?.attempts ?? 0) + 1,
7638
- error
7726
+ attempts: sameKind ? prev.attempts + 1 : 1,
7727
+ error,
7728
+ retriable
7639
7729
  });
7640
7730
  }
7641
7731
  }
@@ -9179,13 +9269,13 @@ var SwarmClient = class _SwarmClient {
9179
9269
  }
9180
9270
  persistPending() {
9181
9271
  try {
9182
- writeFileSync5(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
9272
+ writeFileSync6(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
9183
9273
  } catch {
9184
9274
  }
9185
9275
  }
9186
9276
  persistMap() {
9187
9277
  try {
9188
- writeFileSync5(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
9278
+ writeFileSync6(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
9189
9279
  } catch {
9190
9280
  }
9191
9281
  }
@@ -9255,7 +9345,7 @@ function tickPulse(health, now, intervalMs) {
9255
9345
  }
9256
9346
 
9257
9347
  // src/logRotate.ts
9258
- import { copyFileSync as copyFileSync2, renameSync as renameSync5, statSync as statSync5, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync8 } from "fs";
9348
+ import { copyFileSync as copyFileSync2, renameSync as renameSync5, statSync as statSync5, fstatSync, truncateSync, unlinkSync as unlinkSync3, existsSync as existsSync8 } from "fs";
9259
9349
  var realIo = {
9260
9350
  size: (path) => {
9261
9351
  try {
@@ -9267,7 +9357,7 @@ var realIo = {
9267
9357
  copy: copyFileSync2,
9268
9358
  rename: renameSync5,
9269
9359
  truncate: (path) => truncateSync(path, 0),
9270
- unlink: unlinkSync2,
9360
+ unlink: unlinkSync3,
9271
9361
  exists: existsSync8
9272
9362
  };
9273
9363
  var realOwnerIo = {
@@ -11124,25 +11214,76 @@ var UNRETRIABLE_PARK_MARK = "\u043F\u043E\u0432\u0442\u043E\u0440 \u043D\u0435 \
11124
11214
  function isUnretriableParked(run) {
11125
11215
  return isPreStartFailure(run) && (run.stop_reason ?? "").includes(UNRETRIABLE_PARK_MARK);
11126
11216
  }
11217
+ var PARK_APPLIED_MARK = "\u0437\u0430\u0434\u0430\u0447\u0430 \u0441\u043D\u044F\u0442\u0430 \u0441 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0440\u043E\u044F";
11218
+ var PARK_APPLIED_RE = new RegExp(`${PARK_APPLIED_MARK} \\(([^,)]+)(?:, \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043F\u043E #(\\d+))?\\)`);
11219
+ function withParkApplied(reason, at, upToRunId) {
11220
+ if (PARK_APPLIED_RE.test(reason)) return reason;
11221
+ const upTo = upToRunId != null ? `, \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043F\u043E #${upToRunId}` : "";
11222
+ return `${reason} \u2014 ${PARK_APPLIED_MARK} (${at}${upTo})`;
11223
+ }
11224
+ function parkApplied(run) {
11225
+ if (!run || !isParkApplied(run)) return null;
11226
+ const m = PARK_APPLIED_RE.exec(run.stop_reason ?? "");
11227
+ if (!m) return null;
11228
+ const ms = m[1] ? stampMs(m[1]) : Number.NaN;
11229
+ return {
11230
+ at: Number.isFinite(ms) ? ms : null,
11231
+ upToRunId: m[2] ? Number(m[2]) : null
11232
+ };
11233
+ }
11234
+ function lastRunIdFor(runs, taskSlug) {
11235
+ return ownRuns(runs, taskSlug).reduce((max, r) => Math.max(max, r.id), 0);
11236
+ }
11237
+ function isParkApplied(run) {
11238
+ return isPreStartFailure(run) && (run.stop_reason ?? "").includes(PARK_APPLIED_MARK);
11239
+ }
11240
+ var PARK_CAUSE_PREFIX = "\u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ";
11241
+ function parkedCause(run) {
11242
+ const at = (run?.stop_reason ?? "").indexOf(PARK_CAUSE_PREFIX);
11243
+ if (at < 0) return null;
11244
+ const cause = run.stop_reason.slice(at + PARK_CAUSE_PREFIX.length).replace(new RegExp(`\\s*\u2014\\s*${PARK_APPLIED_MARK} \\([^)]*\\)\\s*$`), "").trim();
11245
+ return cause || null;
11246
+ }
11127
11247
  var SPAWN_ATTEMPT_WINDOW_MS = 60 * 6e4;
11248
+ function ownRuns(runs, taskSlug) {
11249
+ return runs.filter(
11250
+ (r) => r.task_slug === taskSlug && r.reviews_run_id == null && r.resolves_run_id == null && r.verifies_run_id == null
11251
+ );
11252
+ }
11253
+ function newestFirst(a, b) {
11254
+ const at = a.created_at ? stampMs(a.created_at) : Number.NaN;
11255
+ const bt = b.created_at ? stampMs(b.created_at) : Number.NaN;
11256
+ if (Number.isFinite(at) && Number.isFinite(bt) && at !== bt) return bt - at;
11257
+ return b.id - a.id;
11258
+ }
11128
11259
  function preStartAttempts(runs, taskSlug, opts = {}) {
11129
11260
  const windowMs = opts.windowMs ?? SPAWN_ATTEMPT_WINDOW_MS;
11130
11261
  const now = opts.now ?? Date.now();
11131
- const own = runs.filter((r) => r.task_slug === taskSlug && r.reviews_run_id == null && r.resolves_run_id == null && r.verifies_run_id == null).sort((a, b) => b.id - a.id);
11262
+ const own = ownRuns(runs, taskSlug).sort(newestFirst);
11132
11263
  let n = 0;
11264
+ let newestCreatedAt = Number.NEGATIVE_INFINITY;
11265
+ let newestSeenId = Number.NEGATIVE_INFINITY;
11133
11266
  for (const run of own) {
11267
+ const applied = parkApplied(run);
11268
+ if (applied) {
11269
+ const returned = applied.upToRunId != null ? newestSeenId > applied.upToRunId : applied.at != null && newestCreatedAt > applied.at;
11270
+ if (returned) break;
11271
+ }
11272
+ newestSeenId = Math.max(newestSeenId, run.id);
11273
+ const startedAt = run.created_at ? stampMs(run.created_at) : Number.NaN;
11274
+ if (Number.isFinite(startedAt)) newestCreatedAt = Math.max(newestCreatedAt, startedAt);
11134
11275
  if (run.status === "queued" || run.status === "running") continue;
11135
11276
  if (isSpawnParked(run) && run.status !== "failed") continue;
11136
11277
  if (!isPreStartFailure(run)) break;
11137
- const at = run.created_at ? stampMs(run.created_at) : Number.NaN;
11138
- if (Number.isFinite(at) && now - at >= windowMs) break;
11278
+ if (Number.isFinite(startedAt) && now - startedAt >= windowMs) break;
11279
+ if (opts.since != null && Number.isFinite(startedAt) && startedAt < opts.since) break;
11139
11280
  n += 1;
11140
11281
  if (isUnretriableParked(run)) return Math.max(n, PRE_START_ATTEMPT_LIMIT);
11141
11282
  }
11142
11283
  return n;
11143
11284
  }
11144
11285
  function lastPreStartAttempt(runs, taskSlug) {
11145
- return runs.filter((r) => r.task_slug === taskSlug && isPreStartFailure(r)).sort((a, b) => b.id - a.id)[0] ?? null;
11286
+ return runs.filter((r) => r.task_slug === taskSlug && isPreStartFailure(r)).sort(newestFirst)[0] ?? null;
11146
11287
  }
11147
11288
  function unresolvedRepoReason(repo, served) {
11148
11289
  const raw = repo.trim();
@@ -11325,6 +11466,17 @@ async function mergedPrForBranch(branch, repoDir, run = realRepoRunner) {
11325
11466
  return null;
11326
11467
  }
11327
11468
  }
11469
+ async function openPrsForTask(slug, repoDir, run = realRepoRunner) {
11470
+ if (!slug || !repoDir) return null;
11471
+ try {
11472
+ const out = await run(["pr", "list", "--state", "open", "--json", "url,headRefName,title", "--limit", "50"], repoDir);
11473
+ const arr = JSON.parse(out);
11474
+ if (!Array.isArray(arr)) return null;
11475
+ return arr.filter((p) => typeof p.url === "string" && prBelongsToTask(slug, { headRefName: p.headRefName, title: p.title })).map((p) => p.url);
11476
+ } catch {
11477
+ return null;
11478
+ }
11479
+ }
11328
11480
  async function closePR(prUrl, run = realRunner) {
11329
11481
  await run(["pr", "close", prUrl, "--comment", "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 (swarm)"]);
11330
11482
  }
@@ -14074,7 +14226,7 @@ import { serve as serve2 } from "@hono/node-server";
14074
14226
  // package.json
14075
14227
  var package_default = {
14076
14228
  name: "@taskless-app/swarm",
14077
- version: "0.6.26",
14229
+ version: "0.6.28",
14078
14230
  type: "module",
14079
14231
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
14080
14232
  keywords: [
@@ -14125,7 +14277,7 @@ var package_default = {
14125
14277
  execa: "^10.0.0",
14126
14278
  zod: "3.25.76"
14127
14279
  },
14128
- gitHead: "52416995d46b0a843374d14bfb00dd310f71c4bf"
14280
+ gitHead: "62910fc63850764f4562b7ec3bd65b44d3557050"
14129
14281
  };
14130
14282
 
14131
14283
  // src/version.ts
@@ -16625,6 +16777,7 @@ var RESPAWN_LIMIT = Number(process.env.SWARM_RESPAWN_LIMIT) || 2;
16625
16777
  var RESPAWN_YOUNG_MS = Number(process.env.SWARM_RESPAWN_YOUNG_MS) || 60 * 1e3;
16626
16778
  var EXITED_NO_DONE_REASON = "\u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441\u0430\u043C\u0430, done() \u043D\u0435 \u0432\u044B\u0437\u0432\u0430\u043D";
16627
16779
  var ABORTED_STOP_REASON = "\u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C, done() \u043D\u0435 \u0432\u044B\u0437\u0432\u0430\u043D";
16780
+ var SUBMITTED_EXIT_TEXT = "\u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C, \u043D\u043E \u0440\u0430\u0431\u043E\u0442\u0430 \u0443\u0436\u0435 \u0441\u0434\u0430\u043D\u0430 (PR \u043E\u0442\u043A\u0440\u044B\u0442, done() \u043F\u0440\u0438\u043D\u044F\u0442) \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E \u0432 \u0440\u0435\u0432\u044C\u044E, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D";
16628
16781
  var EXIT_WORK_PUSHED = "\u0432\u0435\u0442\u043A\u0430 \u0435\u0441\u0442\u044C \u043D\u0430 origin \u2014 \u0435\u0441\u0442\u044C \u0447\u0442\u043E \u0441\u043F\u0430\u0441\u0430\u0442\u044C, PR \u043D\u0435 \u043E\u0442\u043A\u0440\u044B\u0442";
16629
16782
  var EXIT_WORK_LOST = "\u0432\u0435\u0442\u043A\u0438 \u043D\u0430 origin \u043D\u0435\u0442 \u2014 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E";
16630
16783
  var EXIT_WORK_UNKNOWN = "\u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C origin \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u2014 \u0446\u0435\u043B\u0430 \u043B\u0438 \u0440\u0430\u0431\u043E\u0442\u0430, \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E";
@@ -16757,7 +16910,7 @@ function lastCiCheckMs(db, runId) {
16757
16910
  if (!evs.length) return null;
16758
16911
  return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
16759
16912
  }
16760
- function reviewPhaseInputFor(deps, run) {
16913
+ function reviewPhaseInputFor(deps, run, projectWantsVerify = true) {
16761
16914
  const events = deps.db.events(run.id);
16762
16915
  let ciRed = false;
16763
16916
  let ciFailing = [];
@@ -16782,20 +16935,35 @@ function reviewPhaseInputFor(deps, run) {
16782
16935
  const attempts = deps.db.reviewerAttemptCountFor(run.id);
16783
16936
  const lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
16784
16937
  const reviewerDead = !reviewer && run.review_verdict == null && attempts >= REVIEW_SPAWN_LIMIT && (lastAttempt?.status === "failed" || lastAttempt?.status === "stalled" || lastAttempt?.status === "stopped");
16938
+ const verifyExpected = deps.spawnVerifier != null && projectWantsVerify;
16939
+ const verifierDead = verifyExpected && run.verify_verdict == null && deps.db.verifierCountFor(run.id) === 0 && deps.db.verifierAttemptCountFor(run.id) >= VERIFY_SPAWN_LIMIT;
16785
16940
  return {
16786
16941
  reviewVerdict: run.review_verdict,
16787
16942
  ciRed,
16788
16943
  ciFailing,
16789
16944
  ciProgress,
16790
16945
  reviewerName: reviewer?.name ?? null,
16791
- reviewerDead
16946
+ reviewerDead,
16947
+ verifyVerdict: run.verify_verdict,
16948
+ verifyExpected,
16949
+ verifierDead
16792
16950
  };
16793
16951
  }
16794
- function refreshReviewPhases(deps) {
16795
- for (const run of deps.db.listRuns("review")) {
16796
- if (run.reviews_run_id != null || run.resolves_run_id != null) continue;
16797
- if (!run.pr_url) continue;
16798
- const phase = computeReviewPhase(reviewPhaseInputFor(deps, run));
16952
+ async function refreshReviewPhases(deps) {
16953
+ const runs = deps.db.listRuns("review").filter((r) => r.reviews_run_id == null && r.resolves_run_id == null && !!r.pr_url);
16954
+ const wantsChecks = /* @__PURE__ */ new Map();
16955
+ const needsAnswer = (r) => deps.spawnVerifier != null && r.review_verdict === "approve" && r.verify_verdict == null;
16956
+ for (const run of runs) {
16957
+ if (!needsAnswer(run) || wantsChecks.has(run.repo)) continue;
16958
+ try {
16959
+ wantsChecks.set(run.repo, await projectWantsChecks(deps, run));
16960
+ } catch (e) {
16961
+ console.warn(`[overmind] autoReview \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (${run.repo}) \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D \u2014 \u0444\u0430\u0437\u0430 \u0441\u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F \u043A\u0430\u043A \u0440\u0430\u043D\u044C\u0448\u0435:`, e);
16962
+ wantsChecks.set(run.repo, true);
16963
+ }
16964
+ }
16965
+ for (const run of runs) {
16966
+ const phase = computeReviewPhase(reviewPhaseInputFor(deps, run, wantsChecks.get(run.repo) ?? true));
16799
16967
  if (run.review_phase !== phase.label) deps.db.updateRun(run.id, { review_phase: phase.label });
16800
16968
  }
16801
16969
  }
@@ -16897,7 +17065,14 @@ async function retireOutdatedReviewers(deps, run) {
16897
17065
  narrate(deps, `${droneLabel(run)}: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u0440\u043E\u0448\u043B\u043E\u0433\u043E \u043A\u0440\u0443\u0433\u0430 \u0435\u0449\u0451 \u0436\u0438\u0432 \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u0435\u0433\u043E, \u0432\u0442\u043E\u0440\u043E\u0433\u043E \u043F\u043E\u0432\u0435\u0440\u0445 \u043D\u0435 \u0434\u0435\u0440\u0436\u0443.`);
16898
17066
  }
16899
17067
  }
16900
- async function finalizeMergedReview(deps, run) {
17068
+ async function finalizeMergedReview(deps, run, opts) {
17069
+ if (!opts?.workChecked) {
17070
+ const work = await taskWorkState(deps, run.task_slug, run.repo || "", run.pr_url ?? null);
17071
+ if (work.kind === "open") {
17072
+ noteOpenPrHold(deps, run, work);
17073
+ return false;
17074
+ }
17075
+ }
16901
17076
  await deps.finishTask(run.task_slug, run.workspace_id);
16902
17077
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
16903
17078
  });
@@ -16905,6 +17080,7 @@ async function finalizeMergedReview(deps, run) {
16905
17080
  deps.db.updateRun(run.id, { status: "done" });
16906
17081
  deps.db.addEvent(run.id, "done");
16907
17082
  await closeReviewersOnDeadPr(deps, run.id, "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E \u2014 \u0440\u0435\u0432\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E");
17083
+ return true;
16908
17084
  }
16909
17085
  async function finalizeClosedPrReview(deps, run, reason) {
16910
17086
  await closeReviewersOnDeadPr(deps, run.id, "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E");
@@ -16918,7 +17094,7 @@ async function prGateForReviewer(deps, run, prUrl, when) {
16918
17094
  const st = await deps.prStatus(prUrl).catch(() => null);
16919
17095
  switch (prLiveness(st)) {
16920
17096
  case "merged":
16921
- await finalizeMergedReview(deps, run);
17097
+ if (!await finalizeMergedReview(deps, run)) return { kind: "wait" };
16922
17098
  narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D, ${when} \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D, \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D.`);
16923
17099
  return { kind: "finalized" };
16924
17100
  case "closed":
@@ -16949,6 +17125,16 @@ function doneMergedPrForSlug(db, slug) {
16949
17125
  );
16950
17126
  return done?.pr_url ?? null;
16951
17127
  }
17128
+ var OPEN_PR_HOLD_MARK = "\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 \u043E\u0441\u0442\u0430\u043B\u0438\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0435 PR";
17129
+ async function taskWorkState(deps, slug, repoDir, candidate) {
17130
+ const openPrs = deps.openPrsForTask ? await deps.openPrsForTask(slug, repoDir).catch(() => null) : null;
17131
+ return decideTaskWork({ mergedCandidate: candidate, openPrs });
17132
+ }
17133
+ function noteOpenPrHold(deps, run, v) {
17134
+ if (deps.db.events(run.id).some((e) => (e.text ?? "").includes(OPEN_PR_HOLD_MARK))) return;
17135
+ deps.db.addEvent(run.id, "step", formatOpenPrHold(v));
17136
+ narrate(deps, `${droneLabel(run)} \u2014 \u0443 ${run.task_slug} \u0435\u0449\u0451 \u043E\u0442\u043A\u0440\u044B\u0442 PR \u0441 \u043D\u0435\u0432\u043B\u0438\u0442\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u043E\u0439 (${v.prs.join(", ")}), \u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E.`);
17137
+ }
16952
17138
  async function closeDuplicateTaskRun(deps, run, mergedUrl) {
16953
17139
  if (run.pid != null && deps.kill && deps.isAlive(run.pid)) {
16954
17140
  try {
@@ -16958,7 +17144,7 @@ async function closeDuplicateTaskRun(deps, run, mergedUrl) {
16958
17144
  }
16959
17145
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
16960
17146
  });
16961
- const reason = `PR \u0437\u0430\u0434\u0430\u0447\u0438 \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D (${mergedUrl}) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D-\u0434\u0443\u0431\u043B\u044C \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u0441\u043F\u0430\u0432\u043D\u0430/\u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0438 (TASWM-206)`;
17147
+ const reason = formatMergedDuplicateReason(mergedUrl);
16962
17148
  deps.db.updateRun(run.id, { status: "superseded", stop_reason: reason });
16963
17149
  deps.db.addEvent(run.id, "superseded", reason);
16964
17150
  }
@@ -16990,10 +17176,16 @@ async function closeMergedTaskRuns(deps) {
16990
17176
  mergedUrl = await deps.mergedPrForBranch(slug, group[0].repo || "").catch(() => null);
16991
17177
  }
16992
17178
  if (!mergedUrl) continue;
17179
+ const work = await taskWorkState(deps, slug, group[0].repo || "", mergedUrl);
17180
+ if (work.kind === "open") {
17181
+ for (const run of nonTerminal) noteOpenPrHold(deps, run, work);
17182
+ continue;
17183
+ }
17184
+ if (work.kind === "merged") mergedUrl = work.pr;
16993
17185
  const canonical = withPr && withPr.pr_url === mergedUrl && !isTerminalStatus(withPr.status) ? withPr : null;
16994
17186
  for (const run of nonTerminal) {
16995
17187
  if (canonical && run.id === canonical.id) {
16996
- await finalizeMergedReview(deps, run);
17188
+ await finalizeMergedReview(deps, run, { workChecked: true });
16997
17189
  narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430 (\u0434\u0443\u0431\u043B\u044C-\u0440\u0430\u043D\u044B \u0440\u043E\u044F, TASWM-206).`);
16998
17190
  } else {
16999
17191
  await closeDuplicateTaskRun(deps, run, mergedUrl);
@@ -17592,6 +17784,16 @@ async function wakeRun(deps, runId) {
17592
17784
  if (!run) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0437\u0435\u0440\u043A\u0430\u043B\u0435 \u0440\u043E\u044F` };
17593
17785
  if (run.pid != null && deps.isAlive(run.pid)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 (pid ${run.pid}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
17594
17786
  if (isTerminalStatus(run.status)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435\u043D (${run.status}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
17787
+ const authorSideStopped = deps.db.events(run.id).some(
17788
+ (e) => [RESOLVE_ESCALATION_MARK, CI_WAKE_ESCALATION_MARK, VERIFY_ESCALATION_MARK].some(
17789
+ (m) => (e.text ?? "").includes(m)
17790
+ )
17791
+ );
17792
+ const changesEscalated = deps.db.events(run.id).some((e) => (e.text ?? "").includes(`${CHANGES_WAKE_ESCALATION_MARK} (\u043A\u0440\u0443\u0433 ${run.review_cycle ?? 0})`));
17793
+ const stoppedForHuman = deliberateReviewStop(deps.db, run) != null || changesEscalated || authorSideStopped;
17794
+ if (run.status === "review" && run.pr_url && !needsFixIteration(run) && !stoppedForHuman) {
17795
+ 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 (${run.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` };
17796
+ }
17595
17797
  const worktreeExists = deps.worktreeExists ?? existsSync18;
17596
17798
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
17597
17799
  const wt = run.worktree_path;
@@ -18194,17 +18396,31 @@ async function trySpawnQueuedRun(deps, run) {
18194
18396
  // причина одна на машину — в лог уходит одной строкой, а не по строке на прогон
18195
18397
  });
18196
18398
  }
18197
- const spentAttempts = preStartAttempts(deps.db.listRuns(), run.task_slug);
18399
+ const spentAttempts = attemptsFor(deps, run.task_slug);
18198
18400
  if (spentAttempts >= PRE_START_ATTEMPT_LIMIT) {
18199
18401
  const last = lastPreStartAttempt(deps.db.listRuns(), run.task_slug);
18200
- const why = last && isUnretriableParked(last) ? `${SPAWN_PARK_MARK}: \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u043E\u0442\u043A\u0430\u0437\u0430 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u043D\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u0445\u043E\u0434\u0430 \u043D\u0435 \u0434\u0435\u043B\u0430\u044E (TASWM-485). \u0427\u0435\u043C \u043A\u043E\u043D\u0447\u0438\u043B\u043E\u0441\u044C \u2014 \u0432 \u043F\u0440\u043E\u0433\u043E\u043D\u0435 #${last.id}` : `${PRE_START_MARK} ${spentAttempts} \u0440\u0430\u0437\u0430 \u043F\u043E\u0434\u0440\u044F\u0434 \u2014 ${SPAWN_PARK_MARK}, \u0447\u0435\u0442\u0432\u0451\u0440\u0442\u043E\u0433\u043E \u0437\u0430\u0445\u043E\u0434\u0430 \u043D\u0435 \u0434\u0435\u043B\u0430\u044E (TASWM-422)`;
18402
+ const cause = parkedCause(last);
18403
+ const tail2 = cause ? ` \u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ${cause}` : "";
18404
+ const why = (last && isUnretriableParked(last) ? `${SPAWN_PARK_MARK}: \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u043E\u0442\u043A\u0430\u0437\u0430 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u043D\u043E\u0432\u043E\u0433\u043E \u0437\u0430\u0445\u043E\u0434\u0430 \u043D\u0435 \u0434\u0435\u043B\u0430\u044E (TASWM-485). \u0427\u0435\u043C \u043A\u043E\u043D\u0447\u0438\u043B\u043E\u0441\u044C \u2014 \u0432 \u043F\u0440\u043E\u0433\u043E\u043D\u0435 #${last.id}` : `${PRE_START_MARK} ${spentAttempts} \u0440\u0430\u0437\u0430 \u043F\u043E\u0434\u0440\u044F\u0434 \u2014 ${SPAWN_PARK_MARK}, \u0447\u0435\u0442\u0432\u0451\u0440\u0442\u043E\u0433\u043E \u0437\u0430\u0445\u043E\u0434\u0430 \u043D\u0435 \u0434\u0435\u043B\u0430\u044E (TASWM-422)`) + tail2;
18201
18405
  deps.db.updateRun(run.id, { status: "stopped", stop_reason: why });
18202
18406
  deps.db.addEvent(run.id, "step", why);
18407
+ if (deps.parkTask && last && !isParkApplied(last)) {
18408
+ const ok = await deps.parkTask(run, last.stop_reason ?? why, { comment: false }).catch(() => false);
18409
+ if (ok)
18410
+ deps.db.updateRun(last.id, {
18411
+ // Водораздел включает и ЭТОТ, только что закрытый гейтом прогон: он появился до
18412
+ // подтверждения, и принимать его за ручной возврат нельзя (code-review PR #984).
18413
+ stop_reason: withParkApplied(last.stop_reason ?? why, stamp(), lastRunIdFor(deps.db.listRuns(), run.task_slug))
18414
+ });
18415
+ }
18203
18416
  const d = noteDefer(deps.spawnDefer ??= /* @__PURE__ */ new Map(), `pre-start-exhausted:${run.task_slug}`, why, GATE_LOG_HEARTBEAT_TICKS);
18204
18417
  if (d.log) console.error(`[overmind] ${run.task_slug}: ${why}${d.changed ? "" : ` \u2014 \u0435\u0449\u0451 \u0432\u0438\u0441\u0438\u0442, \u0442\u0438\u043A ${d.ticks}`}`);
18205
18418
  return "closed";
18206
18419
  }
18207
- const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
18420
+ const mergedCandidate = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
18421
+ const work = mergedCandidate ? await taskWorkState(deps, run.task_slug, run.repo || "", mergedCandidate) : null;
18422
+ if (work?.kind === "open") noteOpenPrHold(deps, run, work);
18423
+ const mergedUrl = work?.kind === "merged" ? work.pr : null;
18208
18424
  if (mergedUrl) {
18209
18425
  await closeDuplicateTaskRun(deps, run, mergedUrl);
18210
18426
  await deps.finishTask(run.task_slug, run.workspace_id).catch(() => {
@@ -18217,6 +18433,15 @@ async function trySpawnQueuedRun(deps, run) {
18217
18433
  if (unmet.length) {
18218
18434
  const lost = unmet.filter((d) => d.outcome === "unknown");
18219
18435
  const blind = deps.db.depsUnconfirmed?.(run.depends_on) ?? [];
18436
+ const stuck = blind.find((d) => !d.retriable && d.attempts >= SwarmClient.DEPS_UNRETRIABLE_ATTEMPTS);
18437
+ if (stuck) {
18438
+ return await failBeforeStart(
18439
+ deps,
18440
+ run,
18441
+ `\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C ${stuck.slug} \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C: \u043F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0440\u0433\u0430\u0435\u0442 swarm_deps_status \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u044D\u0442\u043E \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F (${stuck.attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A), \u043E\u0442\u0432\u0435\u0442 \u2014 ${stuck.error}). \u0412\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043D\u0435 \u0436\u0434\u0443 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 (TASWM-517)`,
18442
+ { retriable: false }
18443
+ );
18444
+ }
18220
18445
  const why = lost.length ? `\u0436\u0434\u0451\u0442 \u0431\u043B\u043E\u043A\u0435\u0440\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u041D\u0415\u0422: ${lost.map((d) => d.slug).join(", ")} \u2014 \u043F\u043E \u044D\u0442\u043E\u0439 \u0441\u0441\u044B\u043B\u043A\u0435 \u043D\u0435\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430; \u0441\u0430\u043C \u043D\u0435 \u043F\u043E\u0435\u0434\u0435\u0442, \u0441\u043D\u0438\u043C\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C (swarm_run_upsert{dependsOn:[]}) \u0438\u043B\u0438 \u0437\u0430\u0432\u0435\u0434\u0438 \u0431\u043B\u043E\u043A\u0435\u0440 \u0437\u0430\u043D\u043E\u0432\u043E (TASWM-399)` : blind.length ? `\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 ${blind.map((d) => d.slug).join(", ")}, \u043D\u043E \u041F\u0420\u041E\u0414 \u0415\u0401 \u041D\u0415 \u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u0415\u0422: ${blind[0].attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) swarm_deps_status, \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u043E\u0448\u0438\u0431\u043A\u0430 \u2014 ${blind[0].error}. \u041F\u043E\u043A\u0430 \u043F\u0440\u043E\u0434 \u043C\u043E\u043B\u0447\u0438\u0442, \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438 \u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0430\u043C \u043D\u0435 \u043F\u043E\u0435\u0434\u0435\u0442 (TASWM-486)` : `\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438: ${unmet.map((d) => d.slug).join(", ")}`;
18221
18446
  return deferSpawn(deps, run, why);
18222
18447
  }
@@ -18299,10 +18524,13 @@ function spawnRetryOpts(e) {
18299
18524
  if (!(e instanceof TaskBodyUnavailableError)) return {};
18300
18525
  return { retriable: TOOL_FAILURE_RETRIABLE[e.code] };
18301
18526
  }
18527
+ function attemptsFor(deps, taskSlug) {
18528
+ return preStartAttempts(deps.db.listRuns(), taskSlug, deps.startedAt != null ? { since: deps.startedAt } : {});
18529
+ }
18302
18530
  async function failBeforeStart(deps, run, reason, opts = {}) {
18303
18531
  deps.db.updateRun(run.id, { status: "failed", stop_reason: `${PRE_START_MARK}: ${reason}` });
18304
18532
  deps.db.addEvent(run.id, "failed", `${PRE_START_MARK} \u2014 ${reason}`);
18305
- const attempts = preStartAttempts(deps.db.listRuns(), run.task_slug);
18533
+ const attempts = attemptsFor(deps, run.task_slug);
18306
18534
  if (opts.retriable !== false && attempts < PRE_START_ATTEMPT_LIMIT) {
18307
18535
  narrate(deps, `${droneLabel(run)} \u2014 ${PRE_START_MARK} (\u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempts} \u0438\u0437 ${PRE_START_ATTEMPT_LIMIT}): ${reason}`);
18308
18536
  return "failed";
@@ -18313,7 +18541,13 @@ async function failBeforeStart(deps, run, reason, opts = {}) {
18313
18541
  narrate(deps, `${run.task_slug}: ${parked}`);
18314
18542
  if (deps.parkTask) {
18315
18543
  const ok = await deps.parkTask(run, parked).catch(() => false);
18316
- if (!ok) {
18544
+ if (ok)
18545
+ deps.db.updateRun(run.id, {
18546
+ // Водораздел (code-review PR #984) — самый большой id прогона задачи ПРЯМО СЕЙЧАС:
18547
+ // всё, что появится после него, заведено уже по возвращённой в очередь задаче.
18548
+ stop_reason: withParkApplied(parked, stamp(), lastRunIdFor(deps.db.listRuns(), run.task_slug))
18549
+ });
18550
+ else {
18317
18551
  deps.db.addEvent(
18318
18552
  run.id,
18319
18553
  "step",
@@ -18983,8 +19217,7 @@ async function tickOnce(deps) {
18983
19217
  const st = await deps.prStatus(prUrl);
18984
19218
  run = syncBranchFromPr(deps.db, run, st.headBranch);
18985
19219
  if (st.merged) {
18986
- await finalizeMergedReview(deps, run);
18987
- narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${run.task_slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
19220
+ if (await finalizeMergedReview(deps, run)) narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${run.task_slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
18988
19221
  continue;
18989
19222
  }
18990
19223
  if (prLiveness(st) === "closed") {
@@ -19128,7 +19361,7 @@ async function tickOnce(deps) {
19128
19361
  relinkUnboundReviewers(deps);
19129
19362
  await closeOrphanedReviewers(deps);
19130
19363
  await spawnVerifiersForOpenPrs(deps, stormActive);
19131
- refreshReviewPhases(deps);
19364
+ await refreshReviewPhases(deps);
19132
19365
  let liveChildPpids;
19133
19366
  try {
19134
19367
  liveChildPpids = deps.livePpids?.() ?? null;
@@ -19218,6 +19451,12 @@ async function tickOnce(deps) {
19218
19451
  }
19219
19452
  }
19220
19453
  if (!alive && (outcome === "graceful" || outcome === "aborted") && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && run.verifies_run_id == null && run.kind !== "hero") {
19454
+ if (!wasAborted(outcome) && run.pr_url) {
19455
+ deps.db.updateRun(run.id, { status: "review", stop_reason: null });
19456
+ deps.db.addEvent(run.id, "step", `${SUBMITTED_EXIT_TEXT}: ${run.pr_url}`);
19457
+ narrate(deps, `${droneLabel(run)} \u0432\u044B\u0448\u0435\u043B, \u043D\u043E \u0440\u0430\u0431\u043E\u0442\u0430 \u0443\u0436\u0435 \u0441\u0434\u0430\u043D\u0430 (${run.pr_url}) \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E \u0432 \u0440\u0435\u0432\u044C\u044E, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D.`);
19458
+ continue;
19459
+ }
19221
19460
  const aborted = wasAborted(outcome);
19222
19461
  const abort = aborted ? deps.exitAbort?.(run) ?? null : null;
19223
19462
  const what = aborted ? abortReason(abort) : EXITED_NO_DONE_REASON;
@@ -20484,6 +20723,9 @@ async function runDaemon(opts) {
20484
20723
  // Смерженный PR ветки-слага (TASWM-206) — тот же фолбэк на дефолт-репо роя, что и
20485
20724
  // openPrsForBranch выше; реальный gh, логика закрытия дублей — в tickOnce (тестируется).
20486
20725
  mergedPrForBranch: (branch, repoDir) => mergedPrForBranch(branch, repoDir || opts.repo),
20726
+ // ВСЕ открытые PR задачи (TASWM-515) — тот же фолбэк на дефолт-репо роя. Реальный gh;
20727
+ // и отбор «этот PR — задачи», и решение «закончена ли работа» чистые (swarm-core).
20728
+ openPrsForTask: (slug, repoDir) => openPrsForTask(slug, repoDir || opts.repo),
20487
20729
  // Закрыта ли задача в Taskless (TASWM-137 п.г): death-detect гасит зомби-прогон, чья
20488
20730
  // задача уже completed/cancelled/archived, вместо респавна. null — Taskless недоступен
20489
20731
  // (не гасим). Без tl (нет токена) ключ не задаём — шаг инертен, старое поведение.
@@ -20499,17 +20741,22 @@ async function runDaemon(opts) {
20499
20741
  // задачу), а не кодом возврата: клиент глушит отказ сервера внутрь (safeCall), и по
20500
20742
  // возврату «отложил» было бы неотличимо от «сказал, но не доехало» — то самое молчание,
20501
20743
  // из-за которого 17.08 круг и крутился.
20502
- parkTask: async (run, reason) => {
20744
+ parkTask: async (run, reason, opts2) => {
20503
20745
  const client = tlFor(run.workspace_id);
20504
20746
  if (!client) return false;
20505
- await client.addComment(
20506
- run.task_slug,
20507
- `${reason}
20747
+ if (opts2?.comment !== false)
20748
+ await client.addComment(
20749
+ run.task_slug,
20750
+ // TASWM-514: советуем ИМЕННО возврат в очередь — теперь этот рычаг работает
20751
+ // (возврат в «не начато» обнуляет счётчик попыток). И причина стоит в первой
20752
+ // строке (её несёт `reason`, см. parkReason): человек, читающий пометку через
20753
+ // час, должен из неё понять, ЧТО чинить, а не раскапывать записи прогонов.
20754
+ `${reason}
20508
20755
 
20509
- \u0417\u0430\u0434\u0430\u0447\u0430 \u0441\u043D\u044F\u0442\u0430 \u0441 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0440\u043E\u044F \u2014 \u043F\u043E\u043A\u0430 \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u043D\u0435 \u0443\u0441\u0442\u0440\u0430\u043D\u0435\u043D\u0430, \u0431\u0440\u0430\u0442\u044C \u0435\u0451 \u0441\u043D\u043E\u0432\u0430 \u0441\u043C\u044B\u0441\u043B\u0430 \u043D\u0435\u0442. \u0420\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C \u0438 \u0432\u0435\u0440\u043D\u0438 \u0432 \u0431\u044D\u043A\u043B\u043E\u0433.`,
20510
- { swarmKind: "report", swarmDroneName: run.name }
20511
- ).catch(() => {
20512
- });
20756
+ \u0417\u0430\u0434\u0430\u0447\u0430 \u0441\u043D\u044F\u0442\u0430 \u0441 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0440\u043E\u044F \u2014 \u043F\u043E\u043A\u0430 \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u043D\u0435 \u0443\u0441\u0442\u0440\u0430\u043D\u0435\u043D\u0430, \u0431\u0440\u0430\u0442\u044C \u0435\u0451 \u0441\u043D\u043E\u0432\u0430 \u0441\u043C\u044B\u0441\u043B\u0430 \u043D\u0435\u0442. \u0423\u0441\u0442\u0440\u0430\u043D\u0438 \u043F\u0440\u0438\u0447\u0438\u043D\u0443 \u0438 \u0432\u0435\u0440\u043D\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0432 \xAB\u043D\u0435 \u043D\u0430\u0447\u0430\u0442\u043E\xBB \u2014 \u0440\u043E\u0439 \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u0438\u0442 \u0435\u0451 \u0441\u043D\u043E\u0432\u0430.`,
20757
+ { swarmKind: "report", swarmDroneName: run.name }
20758
+ ).catch(() => {
20759
+ });
20513
20760
  await client.startTask(run.task_slug).catch(() => {
20514
20761
  });
20515
20762
  const after = await client.getTask(run.task_slug).catch(() => null);
@@ -20723,6 +20970,9 @@ async function runDaemon(opts) {
20723
20970
  nudge
20724
20971
  ),
20725
20972
  worktreeExists: (p) => existsSync18(p),
20973
+ // TASWM-514: с этого момента считаем попытки спавна — счётчик живёт ровно одну жизнь
20974
+ // процесса демона и рестарт не переживает (см. preStartAttempts).
20975
+ startedAt: Date.now(),
20726
20976
  hasTranscript: executorHasSession,
20727
20977
  // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
20728
20978
  // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
@@ -21184,6 +21434,7 @@ export {
21184
21434
  GATE_LOG_HEARTBEAT_TICKS,
21185
21435
  HEADLESS_BLOCK,
21186
21436
  HERO_CONTINUE_NUDGE,
21437
+ OPEN_PR_HOLD_MARK,
21187
21438
  OVERMIND_ENTRY,
21188
21439
  RESOLVE_ESCALATION_MARK,
21189
21440
  RESOLVE_LIMIT,
@@ -21201,6 +21452,8 @@ export {
21201
21452
  STALL_FLOOR_MS,
21202
21453
  STALL_MS,
21203
21454
  STORM_DEAD_LIMIT,
21455
+ SUBMITTED_EXIT_TEXT,
21456
+ VERIFY_ESCALATION_MARK,
21204
21457
  VERIFY_SPAWN_LIMIT,
21205
21458
  WATCHDOG_PARK_REASON,
21206
21459
  abortReason,