@taskless-app/swarm 0.6.26 → 0.6.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -340,26 +340,6 @@ var init_timeline = __esm({
340
340
  }
341
341
  });
342
342
 
343
- // ../swarm-core/src/reviewPhase.ts
344
- var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
345
- var init_reviewPhase = __esm({
346
- "../swarm-core/src/reviewPhase.ts"() {
347
- "use strict";
348
- REVIEW_PHASE_TEXT = {
349
- checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
350
- awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
351
- ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
352
- in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
353
- changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
354
- ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
355
- 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"
356
- };
357
- PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
358
- (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
359
- );
360
- }
361
- });
362
-
363
343
  // ../swarm-core/src/verifyVerdict.ts
364
344
  function isVerifyVerdict(v) {
365
345
  return typeof v === "string" && VERIFY_VERDICT_SET.has(v);
@@ -428,6 +408,31 @@ var init_verifyVerdict = __esm({
428
408
  }
429
409
  });
430
410
 
411
+ // ../swarm-core/src/reviewPhase.ts
412
+ var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
413
+ var init_reviewPhase = __esm({
414
+ "../swarm-core/src/reviewPhase.ts"() {
415
+ "use strict";
416
+ init_verifyVerdict();
417
+ REVIEW_PHASE_TEXT = {
418
+ checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
419
+ awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
420
+ ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
421
+ in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
422
+ changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
423
+ 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",
424
+ 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",
425
+ 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",
426
+ 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",
427
+ ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
428
+ 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"
429
+ };
430
+ PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
431
+ (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
432
+ );
433
+ }
434
+ });
435
+
431
436
  // ../swarm-core/src/mergeGate.ts
432
437
  function computeMergeGate(input) {
433
438
  if (!input.hasRun) {
@@ -1619,6 +1624,13 @@ var init_zoneMemory = __esm({
1619
1624
  }
1620
1625
  });
1621
1626
 
1627
+ // ../swarm-core/src/taskPrs.ts
1628
+ var init_taskPrs = __esm({
1629
+ "../swarm-core/src/taskPrs.ts"() {
1630
+ "use strict";
1631
+ }
1632
+ });
1633
+
1622
1634
  // ../swarm-core/src/index.ts
1623
1635
  var init_src = __esm({
1624
1636
  "../swarm-core/src/index.ts"() {
@@ -1653,6 +1665,7 @@ var init_src = __esm({
1653
1665
  init_effort();
1654
1666
  init_queuePlan();
1655
1667
  init_zoneMemory();
1668
+ init_taskPrs();
1656
1669
  }
1657
1670
  });
1658
1671
 
@@ -3040,7 +3053,7 @@ var init_package = __esm({
3040
3053
  "package.json"() {
3041
3054
  package_default = {
3042
3055
  name: "@taskless-app/swarm",
3043
- version: "0.6.26",
3056
+ version: "0.6.27",
3044
3057
  type: "module",
3045
3058
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
3046
3059
  keywords: [
@@ -3091,7 +3104,7 @@ var init_package = __esm({
3091
3104
  execa: "^10.0.0",
3092
3105
  zod: "3.25.76"
3093
3106
  },
3094
- gitHead: "52416995d46b0a843374d14bfb00dd310f71c4bf"
3107
+ gitHead: "2e1ed83014422ae577be60271adc79e291b9e2ab"
3095
3108
  };
3096
3109
  }
3097
3110
  });
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
 
@@ -11124,25 +11192,76 @@ var UNRETRIABLE_PARK_MARK = "\u043F\u043E\u0432\u0442\u043E\u0440 \u043D\u0435 \
11124
11192
  function isUnretriableParked(run) {
11125
11193
  return isPreStartFailure(run) && (run.stop_reason ?? "").includes(UNRETRIABLE_PARK_MARK);
11126
11194
  }
11195
+ 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";
11196
+ var PARK_APPLIED_RE = new RegExp(`${PARK_APPLIED_MARK} \\(([^,)]+)(?:, \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043F\u043E #(\\d+))?\\)`);
11197
+ function withParkApplied(reason, at, upToRunId) {
11198
+ if (PARK_APPLIED_RE.test(reason)) return reason;
11199
+ const upTo = upToRunId != null ? `, \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043F\u043E #${upToRunId}` : "";
11200
+ return `${reason} \u2014 ${PARK_APPLIED_MARK} (${at}${upTo})`;
11201
+ }
11202
+ function parkApplied(run) {
11203
+ if (!run || !isParkApplied(run)) return null;
11204
+ const m = PARK_APPLIED_RE.exec(run.stop_reason ?? "");
11205
+ if (!m) return null;
11206
+ const ms = m[1] ? stampMs(m[1]) : Number.NaN;
11207
+ return {
11208
+ at: Number.isFinite(ms) ? ms : null,
11209
+ upToRunId: m[2] ? Number(m[2]) : null
11210
+ };
11211
+ }
11212
+ function lastRunIdFor(runs, taskSlug) {
11213
+ return ownRuns(runs, taskSlug).reduce((max, r) => Math.max(max, r.id), 0);
11214
+ }
11215
+ function isParkApplied(run) {
11216
+ return isPreStartFailure(run) && (run.stop_reason ?? "").includes(PARK_APPLIED_MARK);
11217
+ }
11218
+ var PARK_CAUSE_PREFIX = "\u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ";
11219
+ function parkedCause(run) {
11220
+ const at = (run?.stop_reason ?? "").indexOf(PARK_CAUSE_PREFIX);
11221
+ if (at < 0) return null;
11222
+ const cause = run.stop_reason.slice(at + PARK_CAUSE_PREFIX.length).replace(new RegExp(`\\s*\u2014\\s*${PARK_APPLIED_MARK} \\([^)]*\\)\\s*$`), "").trim();
11223
+ return cause || null;
11224
+ }
11127
11225
  var SPAWN_ATTEMPT_WINDOW_MS = 60 * 6e4;
11226
+ function ownRuns(runs, taskSlug) {
11227
+ return runs.filter(
11228
+ (r) => r.task_slug === taskSlug && r.reviews_run_id == null && r.resolves_run_id == null && r.verifies_run_id == null
11229
+ );
11230
+ }
11231
+ function newestFirst(a, b) {
11232
+ const at = a.created_at ? stampMs(a.created_at) : Number.NaN;
11233
+ const bt = b.created_at ? stampMs(b.created_at) : Number.NaN;
11234
+ if (Number.isFinite(at) && Number.isFinite(bt) && at !== bt) return bt - at;
11235
+ return b.id - a.id;
11236
+ }
11128
11237
  function preStartAttempts(runs, taskSlug, opts = {}) {
11129
11238
  const windowMs = opts.windowMs ?? SPAWN_ATTEMPT_WINDOW_MS;
11130
11239
  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);
11240
+ const own = ownRuns(runs, taskSlug).sort(newestFirst);
11132
11241
  let n = 0;
11242
+ let newestCreatedAt = Number.NEGATIVE_INFINITY;
11243
+ let newestSeenId = Number.NEGATIVE_INFINITY;
11133
11244
  for (const run of own) {
11245
+ const applied = parkApplied(run);
11246
+ if (applied) {
11247
+ const returned = applied.upToRunId != null ? newestSeenId > applied.upToRunId : applied.at != null && newestCreatedAt > applied.at;
11248
+ if (returned) break;
11249
+ }
11250
+ newestSeenId = Math.max(newestSeenId, run.id);
11251
+ const startedAt = run.created_at ? stampMs(run.created_at) : Number.NaN;
11252
+ if (Number.isFinite(startedAt)) newestCreatedAt = Math.max(newestCreatedAt, startedAt);
11134
11253
  if (run.status === "queued" || run.status === "running") continue;
11135
11254
  if (isSpawnParked(run) && run.status !== "failed") continue;
11136
11255
  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;
11256
+ if (Number.isFinite(startedAt) && now - startedAt >= windowMs) break;
11257
+ if (opts.since != null && Number.isFinite(startedAt) && startedAt < opts.since) break;
11139
11258
  n += 1;
11140
11259
  if (isUnretriableParked(run)) return Math.max(n, PRE_START_ATTEMPT_LIMIT);
11141
11260
  }
11142
11261
  return n;
11143
11262
  }
11144
11263
  function lastPreStartAttempt(runs, taskSlug) {
11145
- return runs.filter((r) => r.task_slug === taskSlug && isPreStartFailure(r)).sort((a, b) => b.id - a.id)[0] ?? null;
11264
+ return runs.filter((r) => r.task_slug === taskSlug && isPreStartFailure(r)).sort(newestFirst)[0] ?? null;
11146
11265
  }
11147
11266
  function unresolvedRepoReason(repo, served) {
11148
11267
  const raw = repo.trim();
@@ -11325,6 +11444,17 @@ async function mergedPrForBranch(branch, repoDir, run = realRepoRunner) {
11325
11444
  return null;
11326
11445
  }
11327
11446
  }
11447
+ async function openPrsForTask(slug, repoDir, run = realRepoRunner) {
11448
+ if (!slug || !repoDir) return null;
11449
+ try {
11450
+ const out = await run(["pr", "list", "--state", "open", "--json", "url,headRefName,title", "--limit", "50"], repoDir);
11451
+ const arr = JSON.parse(out);
11452
+ if (!Array.isArray(arr)) return null;
11453
+ return arr.filter((p) => typeof p.url === "string" && prBelongsToTask(slug, { headRefName: p.headRefName, title: p.title })).map((p) => p.url);
11454
+ } catch {
11455
+ return null;
11456
+ }
11457
+ }
11328
11458
  async function closePR(prUrl, run = realRunner) {
11329
11459
  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
11460
  }
@@ -14074,7 +14204,7 @@ import { serve as serve2 } from "@hono/node-server";
14074
14204
  // package.json
14075
14205
  var package_default = {
14076
14206
  name: "@taskless-app/swarm",
14077
- version: "0.6.26",
14207
+ version: "0.6.27",
14078
14208
  type: "module",
14079
14209
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
14080
14210
  keywords: [
@@ -14125,7 +14255,7 @@ var package_default = {
14125
14255
  execa: "^10.0.0",
14126
14256
  zod: "3.25.76"
14127
14257
  },
14128
- gitHead: "52416995d46b0a843374d14bfb00dd310f71c4bf"
14258
+ gitHead: "2e1ed83014422ae577be60271adc79e291b9e2ab"
14129
14259
  };
14130
14260
 
14131
14261
  // src/version.ts
@@ -16625,6 +16755,7 @@ var RESPAWN_LIMIT = Number(process.env.SWARM_RESPAWN_LIMIT) || 2;
16625
16755
  var RESPAWN_YOUNG_MS = Number(process.env.SWARM_RESPAWN_YOUNG_MS) || 60 * 1e3;
16626
16756
  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
16757
  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";
16758
+ 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
16759
  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
16760
  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
16761
  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 +16888,7 @@ function lastCiCheckMs(db, runId) {
16757
16888
  if (!evs.length) return null;
16758
16889
  return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
16759
16890
  }
16760
- function reviewPhaseInputFor(deps, run) {
16891
+ function reviewPhaseInputFor(deps, run, projectWantsVerify = true) {
16761
16892
  const events = deps.db.events(run.id);
16762
16893
  let ciRed = false;
16763
16894
  let ciFailing = [];
@@ -16782,20 +16913,35 @@ function reviewPhaseInputFor(deps, run) {
16782
16913
  const attempts = deps.db.reviewerAttemptCountFor(run.id);
16783
16914
  const lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
16784
16915
  const reviewerDead = !reviewer && run.review_verdict == null && attempts >= REVIEW_SPAWN_LIMIT && (lastAttempt?.status === "failed" || lastAttempt?.status === "stalled" || lastAttempt?.status === "stopped");
16916
+ const verifyExpected = deps.spawnVerifier != null && projectWantsVerify;
16917
+ const verifierDead = verifyExpected && run.verify_verdict == null && deps.db.verifierCountFor(run.id) === 0 && deps.db.verifierAttemptCountFor(run.id) >= VERIFY_SPAWN_LIMIT;
16785
16918
  return {
16786
16919
  reviewVerdict: run.review_verdict,
16787
16920
  ciRed,
16788
16921
  ciFailing,
16789
16922
  ciProgress,
16790
16923
  reviewerName: reviewer?.name ?? null,
16791
- reviewerDead
16924
+ reviewerDead,
16925
+ verifyVerdict: run.verify_verdict,
16926
+ verifyExpected,
16927
+ verifierDead
16792
16928
  };
16793
16929
  }
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));
16930
+ async function refreshReviewPhases(deps) {
16931
+ const runs = deps.db.listRuns("review").filter((r) => r.reviews_run_id == null && r.resolves_run_id == null && !!r.pr_url);
16932
+ const wantsChecks = /* @__PURE__ */ new Map();
16933
+ const needsAnswer = (r) => deps.spawnVerifier != null && r.review_verdict === "approve" && r.verify_verdict == null;
16934
+ for (const run of runs) {
16935
+ if (!needsAnswer(run) || wantsChecks.has(run.repo)) continue;
16936
+ try {
16937
+ wantsChecks.set(run.repo, await projectWantsChecks(deps, run));
16938
+ } catch (e) {
16939
+ 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);
16940
+ wantsChecks.set(run.repo, true);
16941
+ }
16942
+ }
16943
+ for (const run of runs) {
16944
+ const phase = computeReviewPhase(reviewPhaseInputFor(deps, run, wantsChecks.get(run.repo) ?? true));
16799
16945
  if (run.review_phase !== phase.label) deps.db.updateRun(run.id, { review_phase: phase.label });
16800
16946
  }
16801
16947
  }
@@ -16897,7 +17043,14 @@ async function retireOutdatedReviewers(deps, run) {
16897
17043
  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
17044
  }
16899
17045
  }
16900
- async function finalizeMergedReview(deps, run) {
17046
+ async function finalizeMergedReview(deps, run, opts) {
17047
+ if (!opts?.workChecked) {
17048
+ const work = await taskWorkState(deps, run.task_slug, run.repo || "", run.pr_url ?? null);
17049
+ if (work.kind === "open") {
17050
+ noteOpenPrHold(deps, run, work);
17051
+ return false;
17052
+ }
17053
+ }
16901
17054
  await deps.finishTask(run.task_slug, run.workspace_id);
16902
17055
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
16903
17056
  });
@@ -16905,6 +17058,7 @@ async function finalizeMergedReview(deps, run) {
16905
17058
  deps.db.updateRun(run.id, { status: "done" });
16906
17059
  deps.db.addEvent(run.id, "done");
16907
17060
  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");
17061
+ return true;
16908
17062
  }
16909
17063
  async function finalizeClosedPrReview(deps, run, reason) {
16910
17064
  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 +17072,7 @@ async function prGateForReviewer(deps, run, prUrl, when) {
16918
17072
  const st = await deps.prStatus(prUrl).catch(() => null);
16919
17073
  switch (prLiveness(st)) {
16920
17074
  case "merged":
16921
- await finalizeMergedReview(deps, run);
17075
+ if (!await finalizeMergedReview(deps, run)) return { kind: "wait" };
16922
17076
  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
17077
  return { kind: "finalized" };
16924
17078
  case "closed":
@@ -16949,6 +17103,16 @@ function doneMergedPrForSlug(db, slug) {
16949
17103
  );
16950
17104
  return done?.pr_url ?? null;
16951
17105
  }
17106
+ 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";
17107
+ async function taskWorkState(deps, slug, repoDir, candidate) {
17108
+ const openPrs = deps.openPrsForTask ? await deps.openPrsForTask(slug, repoDir).catch(() => null) : null;
17109
+ return decideTaskWork({ mergedCandidate: candidate, openPrs });
17110
+ }
17111
+ function noteOpenPrHold(deps, run, v) {
17112
+ if (deps.db.events(run.id).some((e) => (e.text ?? "").includes(OPEN_PR_HOLD_MARK))) return;
17113
+ deps.db.addEvent(run.id, "step", formatOpenPrHold(v));
17114
+ 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.`);
17115
+ }
16952
17116
  async function closeDuplicateTaskRun(deps, run, mergedUrl) {
16953
17117
  if (run.pid != null && deps.kill && deps.isAlive(run.pid)) {
16954
17118
  try {
@@ -16958,7 +17122,7 @@ async function closeDuplicateTaskRun(deps, run, mergedUrl) {
16958
17122
  }
16959
17123
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
16960
17124
  });
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)`;
17125
+ const reason = formatMergedDuplicateReason(mergedUrl);
16962
17126
  deps.db.updateRun(run.id, { status: "superseded", stop_reason: reason });
16963
17127
  deps.db.addEvent(run.id, "superseded", reason);
16964
17128
  }
@@ -16990,10 +17154,16 @@ async function closeMergedTaskRuns(deps) {
16990
17154
  mergedUrl = await deps.mergedPrForBranch(slug, group[0].repo || "").catch(() => null);
16991
17155
  }
16992
17156
  if (!mergedUrl) continue;
17157
+ const work = await taskWorkState(deps, slug, group[0].repo || "", mergedUrl);
17158
+ if (work.kind === "open") {
17159
+ for (const run of nonTerminal) noteOpenPrHold(deps, run, work);
17160
+ continue;
17161
+ }
17162
+ if (work.kind === "merged") mergedUrl = work.pr;
16993
17163
  const canonical = withPr && withPr.pr_url === mergedUrl && !isTerminalStatus(withPr.status) ? withPr : null;
16994
17164
  for (const run of nonTerminal) {
16995
17165
  if (canonical && run.id === canonical.id) {
16996
- await finalizeMergedReview(deps, run);
17166
+ await finalizeMergedReview(deps, run, { workChecked: true });
16997
17167
  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
17168
  } else {
16999
17169
  await closeDuplicateTaskRun(deps, run, mergedUrl);
@@ -17592,6 +17762,16 @@ async function wakeRun(deps, runId) {
17592
17762
  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
17763
  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
17764
  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` };
17765
+ const authorSideStopped = deps.db.events(run.id).some(
17766
+ (e) => [RESOLVE_ESCALATION_MARK, CI_WAKE_ESCALATION_MARK, VERIFY_ESCALATION_MARK].some(
17767
+ (m) => (e.text ?? "").includes(m)
17768
+ )
17769
+ );
17770
+ const changesEscalated = deps.db.events(run.id).some((e) => (e.text ?? "").includes(`${CHANGES_WAKE_ESCALATION_MARK} (\u043A\u0440\u0443\u0433 ${run.review_cycle ?? 0})`));
17771
+ const stoppedForHuman = deliberateReviewStop(deps.db, run) != null || changesEscalated || authorSideStopped;
17772
+ if (run.status === "review" && run.pr_url && !needsFixIteration(run) && !stoppedForHuman) {
17773
+ 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` };
17774
+ }
17595
17775
  const worktreeExists = deps.worktreeExists ?? existsSync18;
17596
17776
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
17597
17777
  const wt = run.worktree_path;
@@ -18194,17 +18374,31 @@ async function trySpawnQueuedRun(deps, run) {
18194
18374
  // причина одна на машину — в лог уходит одной строкой, а не по строке на прогон
18195
18375
  });
18196
18376
  }
18197
- const spentAttempts = preStartAttempts(deps.db.listRuns(), run.task_slug);
18377
+ const spentAttempts = attemptsFor(deps, run.task_slug);
18198
18378
  if (spentAttempts >= PRE_START_ATTEMPT_LIMIT) {
18199
18379
  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)`;
18380
+ const cause = parkedCause(last);
18381
+ const tail2 = cause ? ` \u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ${cause}` : "";
18382
+ 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
18383
  deps.db.updateRun(run.id, { status: "stopped", stop_reason: why });
18202
18384
  deps.db.addEvent(run.id, "step", why);
18385
+ if (deps.parkTask && last && !isParkApplied(last)) {
18386
+ const ok = await deps.parkTask(run, last.stop_reason ?? why, { comment: false }).catch(() => false);
18387
+ if (ok)
18388
+ deps.db.updateRun(last.id, {
18389
+ // Водораздел включает и ЭТОТ, только что закрытый гейтом прогон: он появился до
18390
+ // подтверждения, и принимать его за ручной возврат нельзя (code-review PR #984).
18391
+ stop_reason: withParkApplied(last.stop_reason ?? why, stamp(), lastRunIdFor(deps.db.listRuns(), run.task_slug))
18392
+ });
18393
+ }
18203
18394
  const d = noteDefer(deps.spawnDefer ??= /* @__PURE__ */ new Map(), `pre-start-exhausted:${run.task_slug}`, why, GATE_LOG_HEARTBEAT_TICKS);
18204
18395
  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
18396
  return "closed";
18206
18397
  }
18207
- const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
18398
+ const mergedCandidate = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
18399
+ const work = mergedCandidate ? await taskWorkState(deps, run.task_slug, run.repo || "", mergedCandidate) : null;
18400
+ if (work?.kind === "open") noteOpenPrHold(deps, run, work);
18401
+ const mergedUrl = work?.kind === "merged" ? work.pr : null;
18208
18402
  if (mergedUrl) {
18209
18403
  await closeDuplicateTaskRun(deps, run, mergedUrl);
18210
18404
  await deps.finishTask(run.task_slug, run.workspace_id).catch(() => {
@@ -18299,10 +18493,13 @@ function spawnRetryOpts(e) {
18299
18493
  if (!(e instanceof TaskBodyUnavailableError)) return {};
18300
18494
  return { retriable: TOOL_FAILURE_RETRIABLE[e.code] };
18301
18495
  }
18496
+ function attemptsFor(deps, taskSlug) {
18497
+ return preStartAttempts(deps.db.listRuns(), taskSlug, deps.startedAt != null ? { since: deps.startedAt } : {});
18498
+ }
18302
18499
  async function failBeforeStart(deps, run, reason, opts = {}) {
18303
18500
  deps.db.updateRun(run.id, { status: "failed", stop_reason: `${PRE_START_MARK}: ${reason}` });
18304
18501
  deps.db.addEvent(run.id, "failed", `${PRE_START_MARK} \u2014 ${reason}`);
18305
- const attempts = preStartAttempts(deps.db.listRuns(), run.task_slug);
18502
+ const attempts = attemptsFor(deps, run.task_slug);
18306
18503
  if (opts.retriable !== false && attempts < PRE_START_ATTEMPT_LIMIT) {
18307
18504
  narrate(deps, `${droneLabel(run)} \u2014 ${PRE_START_MARK} (\u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempts} \u0438\u0437 ${PRE_START_ATTEMPT_LIMIT}): ${reason}`);
18308
18505
  return "failed";
@@ -18313,7 +18510,13 @@ async function failBeforeStart(deps, run, reason, opts = {}) {
18313
18510
  narrate(deps, `${run.task_slug}: ${parked}`);
18314
18511
  if (deps.parkTask) {
18315
18512
  const ok = await deps.parkTask(run, parked).catch(() => false);
18316
- if (!ok) {
18513
+ if (ok)
18514
+ deps.db.updateRun(run.id, {
18515
+ // Водораздел (code-review PR #984) — самый большой id прогона задачи ПРЯМО СЕЙЧАС:
18516
+ // всё, что появится после него, заведено уже по возвращённой в очередь задаче.
18517
+ stop_reason: withParkApplied(parked, stamp(), lastRunIdFor(deps.db.listRuns(), run.task_slug))
18518
+ });
18519
+ else {
18317
18520
  deps.db.addEvent(
18318
18521
  run.id,
18319
18522
  "step",
@@ -18983,8 +19186,7 @@ async function tickOnce(deps) {
18983
19186
  const st = await deps.prStatus(prUrl);
18984
19187
  run = syncBranchFromPr(deps.db, run, st.headBranch);
18985
19188
  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.`);
19189
+ 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
19190
  continue;
18989
19191
  }
18990
19192
  if (prLiveness(st) === "closed") {
@@ -19128,7 +19330,7 @@ async function tickOnce(deps) {
19128
19330
  relinkUnboundReviewers(deps);
19129
19331
  await closeOrphanedReviewers(deps);
19130
19332
  await spawnVerifiersForOpenPrs(deps, stormActive);
19131
- refreshReviewPhases(deps);
19333
+ await refreshReviewPhases(deps);
19132
19334
  let liveChildPpids;
19133
19335
  try {
19134
19336
  liveChildPpids = deps.livePpids?.() ?? null;
@@ -19218,6 +19420,12 @@ async function tickOnce(deps) {
19218
19420
  }
19219
19421
  }
19220
19422
  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") {
19423
+ if (!wasAborted(outcome) && run.pr_url) {
19424
+ deps.db.updateRun(run.id, { status: "review", stop_reason: null });
19425
+ deps.db.addEvent(run.id, "step", `${SUBMITTED_EXIT_TEXT}: ${run.pr_url}`);
19426
+ 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.`);
19427
+ continue;
19428
+ }
19221
19429
  const aborted = wasAborted(outcome);
19222
19430
  const abort = aborted ? deps.exitAbort?.(run) ?? null : null;
19223
19431
  const what = aborted ? abortReason(abort) : EXITED_NO_DONE_REASON;
@@ -20484,6 +20692,9 @@ async function runDaemon(opts) {
20484
20692
  // Смерженный PR ветки-слага (TASWM-206) — тот же фолбэк на дефолт-репо роя, что и
20485
20693
  // openPrsForBranch выше; реальный gh, логика закрытия дублей — в tickOnce (тестируется).
20486
20694
  mergedPrForBranch: (branch, repoDir) => mergedPrForBranch(branch, repoDir || opts.repo),
20695
+ // ВСЕ открытые PR задачи (TASWM-515) — тот же фолбэк на дефолт-репо роя. Реальный gh;
20696
+ // и отбор «этот PR — задачи», и решение «закончена ли работа» чистые (swarm-core).
20697
+ openPrsForTask: (slug, repoDir) => openPrsForTask(slug, repoDir || opts.repo),
20487
20698
  // Закрыта ли задача в Taskless (TASWM-137 п.г): death-detect гасит зомби-прогон, чья
20488
20699
  // задача уже completed/cancelled/archived, вместо респавна. null — Taskless недоступен
20489
20700
  // (не гасим). Без tl (нет токена) ключ не задаём — шаг инертен, старое поведение.
@@ -20499,17 +20710,22 @@ async function runDaemon(opts) {
20499
20710
  // задачу), а не кодом возврата: клиент глушит отказ сервера внутрь (safeCall), и по
20500
20711
  // возврату «отложил» было бы неотличимо от «сказал, но не доехало» — то самое молчание,
20501
20712
  // из-за которого 17.08 круг и крутился.
20502
- parkTask: async (run, reason) => {
20713
+ parkTask: async (run, reason, opts2) => {
20503
20714
  const client = tlFor(run.workspace_id);
20504
20715
  if (!client) return false;
20505
- await client.addComment(
20506
- run.task_slug,
20507
- `${reason}
20716
+ if (opts2?.comment !== false)
20717
+ await client.addComment(
20718
+ run.task_slug,
20719
+ // TASWM-514: советуем ИМЕННО возврат в очередь — теперь этот рычаг работает
20720
+ // (возврат в «не начато» обнуляет счётчик попыток). И причина стоит в первой
20721
+ // строке (её несёт `reason`, см. parkReason): человек, читающий пометку через
20722
+ // час, должен из неё понять, ЧТО чинить, а не раскапывать записи прогонов.
20723
+ `${reason}
20508
20724
 
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
- });
20725
+ \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.`,
20726
+ { swarmKind: "report", swarmDroneName: run.name }
20727
+ ).catch(() => {
20728
+ });
20513
20729
  await client.startTask(run.task_slug).catch(() => {
20514
20730
  });
20515
20731
  const after = await client.getTask(run.task_slug).catch(() => null);
@@ -20723,6 +20939,9 @@ async function runDaemon(opts) {
20723
20939
  nudge
20724
20940
  ),
20725
20941
  worktreeExists: (p) => existsSync18(p),
20942
+ // TASWM-514: с этого момента считаем попытки спавна — счётчик живёт ровно одну жизнь
20943
+ // процесса демона и рестарт не переживает (см. preStartAttempts).
20944
+ startedAt: Date.now(),
20726
20945
  hasTranscript: executorHasSession,
20727
20946
  // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
20728
20947
  // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
@@ -21184,6 +21403,7 @@ export {
21184
21403
  GATE_LOG_HEARTBEAT_TICKS,
21185
21404
  HEADLESS_BLOCK,
21186
21405
  HERO_CONTINUE_NUDGE,
21406
+ OPEN_PR_HOLD_MARK,
21187
21407
  OVERMIND_ENTRY,
21188
21408
  RESOLVE_ESCALATION_MARK,
21189
21409
  RESOLVE_LIMIT,
@@ -21201,6 +21421,8 @@ export {
21201
21421
  STALL_FLOOR_MS,
21202
21422
  STALL_MS,
21203
21423
  STORM_DEAD_LIMIT,
21424
+ SUBMITTED_EXIT_TEXT,
21425
+ VERIFY_ESCALATION_MARK,
21204
21426
  VERIFY_SPAWN_LIMIT,
21205
21427
  WATCHDOG_PARK_REASON,
21206
21428
  abortReason,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.26",
3
+ "version": "0.6.27",
4
4
  "type": "module",
5
5
  "description": "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "execa": "^10.0.0",
40
40
  "zod": "3.25.76"
41
41
  },
42
- "gitHead": "52416995d46b0a843374d14bfb00dd310f71c4bf",
42
+ "gitHead": "2e1ed83014422ae577be60271adc79e291b9e2ab",
43
43
  "scripts": {
44
44
  "overmind": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/overmind.ts",
45
45
  "swarm": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/cli.ts",