@taskless-app/swarm 0.6.50 → 0.6.52

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 +104 -98
  2. package/dist/overmind.js +221 -123
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -164,6 +164,104 @@ var init_types = __esm({
164
164
  }
165
165
  });
166
166
 
167
+ // ../swarm-core/src/verifyVerdict.ts
168
+ function isVerifyVerdict(v) {
169
+ return typeof v === "string" && VERIFY_VERDICT_SET.has(v);
170
+ }
171
+ function toVerifyVerdict(v) {
172
+ return isVerifyVerdict(v) ? v : null;
173
+ }
174
+ function isNegativeVerifyVerdict(v) {
175
+ return v === "not_reproduced" || v === "found_issues";
176
+ }
177
+ function verifyVerdictLabel(v) {
178
+ switch (v) {
179
+ case "confirmed":
180
+ return "\u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u042E";
181
+ case "not_reproduced":
182
+ return "\u041D\u0415 \u0412\u041E\u0421\u041F\u0420\u041E\u0418\u0417\u0412\u041E\u0414\u0418\u0422\u0421\u042F";
183
+ case "found_issues":
184
+ return "\u041D\u0410\u0428\u0401\u041B";
185
+ }
186
+ }
187
+ function verifyStats(runs) {
188
+ let confirmed = 0;
189
+ let notReproduced = 0;
190
+ let foundIssues = 0;
191
+ for (const r of runs) {
192
+ switch (toVerifyVerdict(r.verify_verdict)) {
193
+ case "confirmed":
194
+ confirmed++;
195
+ break;
196
+ case "not_reproduced":
197
+ notReproduced++;
198
+ break;
199
+ case "found_issues":
200
+ foundIssues++;
201
+ break;
202
+ default:
203
+ break;
204
+ }
205
+ }
206
+ const negative = notReproduced + foundIssues;
207
+ const total = confirmed + negative;
208
+ return { total, confirmed, notReproduced, foundIssues, negative, negativeShare: total ? negative / total : 0 };
209
+ }
210
+ function verifyStatsByDrone(runs) {
211
+ const byName = /* @__PURE__ */ new Map();
212
+ for (const r of runs) {
213
+ if (!toVerifyVerdict(r.verify_verdict)) continue;
214
+ const name = (r.name ?? "").trim() || "(\u0438\u043C\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)";
215
+ const bucket = byName.get(name);
216
+ if (bucket) bucket.push(r);
217
+ else byName.set(name, [r]);
218
+ }
219
+ return [...byName.entries()].map(([name, rs]) => ({ name, ...verifyStats(rs) })).sort((a, b) => b.negative - a.negative || b.total - a.total || a.name.localeCompare(b.name));
220
+ }
221
+ function verifyStatsLine(s) {
222
+ if (!s.total) return "\u0432\u0435\u0440\u0434\u0438\u043A\u0442\u043E\u0432 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442";
223
+ const pct = Math.round(s.negativeShare * 100);
224
+ return `\u0432\u0435\u0440\u0434\u0438\u043A\u0442\u043E\u0432 ${s.total} \xB7 \u043E\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0445 ${s.negative} (${pct}%): \u043D\u0435 \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u0435\u043B\u043E\u0441\u044C ${s.notReproduced}, \u043D\u0430\u0448\u043B\u0438 \u0434\u0435\u0444\u0435\u043A\u0442 ${s.foundIssues}`;
225
+ }
226
+ var VERIFY_VERDICTS, VERIFY_VERDICT_SET;
227
+ var init_verifyVerdict = __esm({
228
+ "../swarm-core/src/verifyVerdict.ts"() {
229
+ "use strict";
230
+ VERIFY_VERDICTS = ["confirmed", "not_reproduced", "found_issues"];
231
+ VERIFY_VERDICT_SET = new Set(VERIFY_VERDICTS);
232
+ }
233
+ });
234
+
235
+ // ../swarm-core/src/reviewPhase.ts
236
+ function reviewPhaseKindOf(label2) {
237
+ if (!label2) return null;
238
+ return PHASE_BY_LENGTH.find((k) => label2.startsWith(REVIEW_PHASE_TEXT[k])) ?? null;
239
+ }
240
+ var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
241
+ var init_reviewPhase = __esm({
242
+ "../swarm-core/src/reviewPhase.ts"() {
243
+ "use strict";
244
+ init_verifyVerdict();
245
+ REVIEW_PHASE_TEXT = {
246
+ checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
247
+ awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
248
+ ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
249
+ in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
250
+ changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
251
+ 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",
252
+ 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",
253
+ 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",
254
+ verify_dead: "\u0412\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043D\u0435 \u0434\u0430\u043B \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
255
+ ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
256
+ ready_awaiting_ci: "\u0413\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443 \u2014 \u0436\u0434\u0451\u0442 CI",
257
+ reviewer_dead: "\u0420\u0435\u0432\u044C\u044E\u0435\u0440 \u0443\u043C\u0435\u0440 \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A"
258
+ };
259
+ PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
260
+ (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
261
+ );
262
+ }
263
+ });
264
+
167
265
  // ../swarm-core/src/timeline.ts
168
266
  function resolveDepRef(ref, lk) {
169
267
  const mapped = lk.slugByCuid.get(ref);
@@ -335,7 +433,8 @@ function computeCounters(lanes) {
335
433
  if (l.dependsOn.some((d) => !d.met)) c.waitingDep++;
336
434
  else c.waitingSlot++;
337
435
  } else if (l.status === "review") {
338
- if (l.blocker.tone === "merge" || l.reviewVerdict === "approve") c.waitingMerge++;
436
+ if (reviewPhaseKindOf(l.reviewPhase) === "ready_awaiting_ci") c.waitingCI++;
437
+ else if (l.blocker.tone === "merge" || l.reviewVerdict === "approve") c.waitingMerge++;
339
438
  else c.waitingCI++;
340
439
  }
341
440
  }
@@ -345,103 +444,11 @@ var DEFAULT_RUN_MS;
345
444
  var init_timeline = __esm({
346
445
  "../swarm-core/src/timeline.ts"() {
347
446
  "use strict";
447
+ init_reviewPhase();
348
448
  DEFAULT_RUN_MS = 25 * 6e4;
349
449
  }
350
450
  });
351
451
 
352
- // ../swarm-core/src/verifyVerdict.ts
353
- function isVerifyVerdict(v) {
354
- return typeof v === "string" && VERIFY_VERDICT_SET.has(v);
355
- }
356
- function toVerifyVerdict(v) {
357
- return isVerifyVerdict(v) ? v : null;
358
- }
359
- function isNegativeVerifyVerdict(v) {
360
- return v === "not_reproduced" || v === "found_issues";
361
- }
362
- function verifyVerdictLabel(v) {
363
- switch (v) {
364
- case "confirmed":
365
- return "\u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u042E";
366
- case "not_reproduced":
367
- return "\u041D\u0415 \u0412\u041E\u0421\u041F\u0420\u041E\u0418\u0417\u0412\u041E\u0414\u0418\u0422\u0421\u042F";
368
- case "found_issues":
369
- return "\u041D\u0410\u0428\u0401\u041B";
370
- }
371
- }
372
- function verifyStats(runs) {
373
- let confirmed = 0;
374
- let notReproduced = 0;
375
- let foundIssues = 0;
376
- for (const r of runs) {
377
- switch (toVerifyVerdict(r.verify_verdict)) {
378
- case "confirmed":
379
- confirmed++;
380
- break;
381
- case "not_reproduced":
382
- notReproduced++;
383
- break;
384
- case "found_issues":
385
- foundIssues++;
386
- break;
387
- default:
388
- break;
389
- }
390
- }
391
- const negative = notReproduced + foundIssues;
392
- const total = confirmed + negative;
393
- return { total, confirmed, notReproduced, foundIssues, negative, negativeShare: total ? negative / total : 0 };
394
- }
395
- function verifyStatsByDrone(runs) {
396
- const byName = /* @__PURE__ */ new Map();
397
- for (const r of runs) {
398
- if (!toVerifyVerdict(r.verify_verdict)) continue;
399
- const name = (r.name ?? "").trim() || "(\u0438\u043C\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)";
400
- const bucket = byName.get(name);
401
- if (bucket) bucket.push(r);
402
- else byName.set(name, [r]);
403
- }
404
- return [...byName.entries()].map(([name, rs]) => ({ name, ...verifyStats(rs) })).sort((a, b) => b.negative - a.negative || b.total - a.total || a.name.localeCompare(b.name));
405
- }
406
- function verifyStatsLine(s) {
407
- if (!s.total) return "\u0432\u0435\u0440\u0434\u0438\u043A\u0442\u043E\u0432 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442";
408
- const pct = Math.round(s.negativeShare * 100);
409
- return `\u0432\u0435\u0440\u0434\u0438\u043A\u0442\u043E\u0432 ${s.total} \xB7 \u043E\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0445 ${s.negative} (${pct}%): \u043D\u0435 \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u0435\u043B\u043E\u0441\u044C ${s.notReproduced}, \u043D\u0430\u0448\u043B\u0438 \u0434\u0435\u0444\u0435\u043A\u0442 ${s.foundIssues}`;
410
- }
411
- var VERIFY_VERDICTS, VERIFY_VERDICT_SET;
412
- var init_verifyVerdict = __esm({
413
- "../swarm-core/src/verifyVerdict.ts"() {
414
- "use strict";
415
- VERIFY_VERDICTS = ["confirmed", "not_reproduced", "found_issues"];
416
- VERIFY_VERDICT_SET = new Set(VERIFY_VERDICTS);
417
- }
418
- });
419
-
420
- // ../swarm-core/src/reviewPhase.ts
421
- var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
422
- var init_reviewPhase = __esm({
423
- "../swarm-core/src/reviewPhase.ts"() {
424
- "use strict";
425
- init_verifyVerdict();
426
- REVIEW_PHASE_TEXT = {
427
- checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
428
- awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
429
- ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
430
- in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
431
- changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
432
- 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",
433
- 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",
434
- 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",
435
- 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",
436
- ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
437
- 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"
438
- };
439
- PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
440
- (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
441
- );
442
- }
443
- });
444
-
445
452
  // ../swarm-core/src/mergeGate.ts
446
453
  function computeMergeGate(input) {
447
454
  if (!input.hasRun) {
@@ -1716,7 +1723,7 @@ var init_effort = __esm({
1716
1723
  });
1717
1724
 
1718
1725
  // ../swarm-core/src/queuePlan.ts
1719
- var MIN_MS, QUEUE_NOMINAL_RUN_MS, QUEUE_MIN_TAIL_MS, QUEUE_REVIEW_TAIL_MS;
1726
+ var MIN_MS, QUEUE_NOMINAL_RUN_MS, QUEUE_MIN_TAIL_MS;
1720
1727
  var init_queuePlan = __esm({
1721
1728
  "../swarm-core/src/queuePlan.ts"() {
1722
1729
  "use strict";
@@ -1727,7 +1734,6 @@ var init_queuePlan = __esm({
1727
1734
  MIN_MS = 6e4;
1728
1735
  QUEUE_NOMINAL_RUN_MS = 25 * MIN_MS;
1729
1736
  QUEUE_MIN_TAIL_MS = 3 * MIN_MS;
1730
- QUEUE_REVIEW_TAIL_MS = 10 * MIN_MS;
1731
1737
  }
1732
1738
  });
1733
1739
 
@@ -3486,7 +3492,7 @@ var init_package = __esm({
3486
3492
  "package.json"() {
3487
3493
  package_default = {
3488
3494
  name: "@taskless-app/swarm",
3489
- version: "0.6.50",
3495
+ version: "0.6.52",
3490
3496
  type: "module",
3491
3497
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
3492
3498
  keywords: [
@@ -3537,7 +3543,7 @@ var init_package = __esm({
3537
3543
  execa: "^10.0.0",
3538
3544
  zod: "3.25.76"
3539
3545
  },
3540
- gitHead: "859ffe2d91bf4c791af067147515b14a41b96739"
3546
+ gitHead: "f77e0473fd2f80e4418ceadd1b29b9a70241fc04"
3541
3547
  };
3542
3548
  }
3543
3549
  });
package/dist/overmind.js CHANGED
@@ -133,6 +133,123 @@ var init_types = __esm({
133
133
  }
134
134
  });
135
135
 
136
+ // ../swarm-core/src/verifyVerdict.ts
137
+ function isVerifyVerdict(v) {
138
+ return typeof v === "string" && VERIFY_VERDICT_SET.has(v);
139
+ }
140
+ function toVerifyVerdict(v) {
141
+ return isVerifyVerdict(v) ? v : null;
142
+ }
143
+ function isNegativeVerifyVerdict(v) {
144
+ return v === "not_reproduced" || v === "found_issues";
145
+ }
146
+ function verifyVerdictLabel(v) {
147
+ switch (v) {
148
+ case "confirmed":
149
+ return "\u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u042E";
150
+ case "not_reproduced":
151
+ return "\u041D\u0415 \u0412\u041E\u0421\u041F\u0420\u041E\u0418\u0417\u0412\u041E\u0414\u0418\u0422\u0421\u042F";
152
+ case "found_issues":
153
+ return "\u041D\u0410\u0428\u0401\u041B";
154
+ }
155
+ }
156
+ function parseVerifyVerdictComment(text) {
157
+ const line = (text ?? "").split("\n").find((l) => l.trim().length > 0);
158
+ if (!line) return null;
159
+ let best = null;
160
+ for (const [verdict, marks] of VERDICT_MARKS) {
161
+ for (const mark of marks) {
162
+ const at = line.indexOf(mark);
163
+ if (at < 0) continue;
164
+ if (!best || at < best.at) best = { verdict, at };
165
+ }
166
+ }
167
+ return best?.verdict ?? null;
168
+ }
169
+ var VERIFY_VERDICTS, VERIFY_VERDICT_SET, VERDICT_MARKS;
170
+ var init_verifyVerdict = __esm({
171
+ "../swarm-core/src/verifyVerdict.ts"() {
172
+ "use strict";
173
+ VERIFY_VERDICTS = ["confirmed", "not_reproduced", "found_issues"];
174
+ VERIFY_VERDICT_SET = new Set(VERIFY_VERDICTS);
175
+ VERDICT_MARKS = [
176
+ ["not_reproduced", ["\u041D\u0415 \u0412\u041E\u0421\u041F\u0420\u041E\u0418\u0417\u0412\u041E\u0414\u0418\u0422\u0421\u042F", "NOT REPRODUCED", "\u041D\u0415 \u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u042E", "NOT CONFIRMED"]],
177
+ ["found_issues", ["\u041D\u0410\u0428\u0401\u041B", "\u041D\u0410\u0428\u0415\u041B", "FOUND"]],
178
+ ["confirmed", ["\u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u042E", "CONFIRMED"]]
179
+ ];
180
+ }
181
+ });
182
+
183
+ // ../swarm-core/src/reviewPhase.ts
184
+ function reviewPhaseKindOf(label2) {
185
+ if (!label2) return null;
186
+ return PHASE_BY_LENGTH.find((k) => label2.startsWith(REVIEW_PHASE_TEXT[k])) ?? null;
187
+ }
188
+ function computeReviewPhase(input) {
189
+ const { reviewVerdict, ciRed } = input;
190
+ const verifyVerdict = input.verifyVerdict ?? null;
191
+ if (!ciRed && isNegativeVerifyVerdict(verifyVerdict)) {
192
+ const kind = verifyVerdict === "found_issues" ? "verify_found" : "verify_not_reproduced";
193
+ return { kind, label: REVIEW_PHASE_TEXT[kind], ready: false };
194
+ }
195
+ if (reviewVerdict === "approve" && !ciRed) {
196
+ if (verifyVerdict === "confirmed" || input.verifyExpected === false) {
197
+ if (input.ciGreen !== true) {
198
+ return { kind: "ready_awaiting_ci", label: REVIEW_PHASE_TEXT.ready_awaiting_ci, ready: false };
199
+ }
200
+ return { kind: "ready", label: REVIEW_PHASE_TEXT.ready, ready: true };
201
+ }
202
+ if (input.verifierDead) {
203
+ return { kind: "verify_dead", label: REVIEW_PHASE_TEXT.verify_dead, ready: true };
204
+ }
205
+ return { kind: "awaiting_verify", label: REVIEW_PHASE_TEXT.awaiting_verify, ready: false };
206
+ }
207
+ if (ciRed) {
208
+ const jobs = (input.ciFailing ?? []).filter((j) => j && j !== "?");
209
+ const label3 = jobs.length ? `${REVIEW_PHASE_TEXT.ci_fixing}: ${jobs.join(", ")}` : REVIEW_PHASE_TEXT.ci_fixing;
210
+ return { kind: "ci_fixing", label: label3, ready: false };
211
+ }
212
+ if (reviewVerdict === "changes") {
213
+ return { kind: "changes", label: REVIEW_PHASE_TEXT.changes, ready: false };
214
+ }
215
+ if (input.reviewerName) {
216
+ return { kind: "in_review", label: `\u041D\u0430 \u0440\u0435\u0432\u044C\u044E \u0443 ${input.reviewerName}`, ready: false };
217
+ }
218
+ if (input.reviewerDead) {
219
+ return { kind: "reviewer_dead", label: REVIEW_PHASE_TEXT.reviewer_dead, ready: true };
220
+ }
221
+ const p = input.ciProgress;
222
+ if (p && p.total > 0 && p.pass >= p.total) {
223
+ return { kind: "awaiting_reviewer", label: REVIEW_PHASE_TEXT.awaiting_reviewer, ready: false };
224
+ }
225
+ const label2 = p && p.total > 0 ? `${REVIEW_PHASE_TEXT.checks} \u2014 CI ${p.pass}/${p.total}` : REVIEW_PHASE_TEXT.checks;
226
+ return { kind: "checks", label: label2, ready: false };
227
+ }
228
+ var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
229
+ var init_reviewPhase = __esm({
230
+ "../swarm-core/src/reviewPhase.ts"() {
231
+ "use strict";
232
+ init_verifyVerdict();
233
+ REVIEW_PHASE_TEXT = {
234
+ checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
235
+ awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
236
+ ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
237
+ in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
238
+ changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
239
+ 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",
240
+ 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",
241
+ 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",
242
+ verify_dead: "\u0412\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043D\u0435 \u0434\u0430\u043B \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
243
+ ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
244
+ ready_awaiting_ci: "\u0413\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443 \u2014 \u0436\u0434\u0451\u0442 CI",
245
+ reviewer_dead: "\u0420\u0435\u0432\u044C\u044E\u0435\u0440 \u0443\u043C\u0435\u0440 \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A"
246
+ };
247
+ PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
248
+ (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
249
+ );
250
+ }
251
+ });
252
+
136
253
  // ../swarm-core/src/timeline.ts
137
254
  function resolveDepRef(ref, lk) {
138
255
  const mapped = lk.slugByCuid.get(ref);
@@ -304,7 +421,8 @@ function computeCounters(lanes) {
304
421
  if (l.dependsOn.some((d) => !d.met)) c.waitingDep++;
305
422
  else c.waitingSlot++;
306
423
  } else if (l.status === "review") {
307
- if (l.blocker.tone === "merge" || l.reviewVerdict === "approve") c.waitingMerge++;
424
+ if (reviewPhaseKindOf(l.reviewPhase) === "ready_awaiting_ci") c.waitingCI++;
425
+ else if (l.blocker.tone === "merge" || l.reviewVerdict === "approve") c.waitingMerge++;
308
426
  else c.waitingCI++;
309
427
  }
310
428
  }
@@ -314,119 +432,11 @@ var DEFAULT_RUN_MS;
314
432
  var init_timeline = __esm({
315
433
  "../swarm-core/src/timeline.ts"() {
316
434
  "use strict";
435
+ init_reviewPhase();
317
436
  DEFAULT_RUN_MS = 25 * 6e4;
318
437
  }
319
438
  });
320
439
 
321
- // ../swarm-core/src/verifyVerdict.ts
322
- function isVerifyVerdict(v) {
323
- return typeof v === "string" && VERIFY_VERDICT_SET.has(v);
324
- }
325
- function toVerifyVerdict(v) {
326
- return isVerifyVerdict(v) ? v : null;
327
- }
328
- function isNegativeVerifyVerdict(v) {
329
- return v === "not_reproduced" || v === "found_issues";
330
- }
331
- function verifyVerdictLabel(v) {
332
- switch (v) {
333
- case "confirmed":
334
- return "\u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u042E";
335
- case "not_reproduced":
336
- return "\u041D\u0415 \u0412\u041E\u0421\u041F\u0420\u041E\u0418\u0417\u0412\u041E\u0414\u0418\u0422\u0421\u042F";
337
- case "found_issues":
338
- return "\u041D\u0410\u0428\u0401\u041B";
339
- }
340
- }
341
- function parseVerifyVerdictComment(text) {
342
- const line = (text ?? "").split("\n").find((l) => l.trim().length > 0);
343
- if (!line) return null;
344
- let best = null;
345
- for (const [verdict, marks] of VERDICT_MARKS) {
346
- for (const mark of marks) {
347
- const at = line.indexOf(mark);
348
- if (at < 0) continue;
349
- if (!best || at < best.at) best = { verdict, at };
350
- }
351
- }
352
- return best?.verdict ?? null;
353
- }
354
- var VERIFY_VERDICTS, VERIFY_VERDICT_SET, VERDICT_MARKS;
355
- var init_verifyVerdict = __esm({
356
- "../swarm-core/src/verifyVerdict.ts"() {
357
- "use strict";
358
- VERIFY_VERDICTS = ["confirmed", "not_reproduced", "found_issues"];
359
- VERIFY_VERDICT_SET = new Set(VERIFY_VERDICTS);
360
- VERDICT_MARKS = [
361
- ["not_reproduced", ["\u041D\u0415 \u0412\u041E\u0421\u041F\u0420\u041E\u0418\u0417\u0412\u041E\u0414\u0418\u0422\u0421\u042F", "NOT REPRODUCED", "\u041D\u0415 \u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u042E", "NOT CONFIRMED"]],
362
- ["found_issues", ["\u041D\u0410\u0428\u0401\u041B", "\u041D\u0410\u0428\u0415\u041B", "FOUND"]],
363
- ["confirmed", ["\u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u042E", "CONFIRMED"]]
364
- ];
365
- }
366
- });
367
-
368
- // ../swarm-core/src/reviewPhase.ts
369
- function computeReviewPhase(input) {
370
- const { reviewVerdict, ciRed } = input;
371
- const verifyVerdict = input.verifyVerdict ?? null;
372
- if (!ciRed && isNegativeVerifyVerdict(verifyVerdict)) {
373
- const kind = verifyVerdict === "found_issues" ? "verify_found" : "verify_not_reproduced";
374
- return { kind, label: REVIEW_PHASE_TEXT[kind], ready: false };
375
- }
376
- if (reviewVerdict === "approve" && !ciRed) {
377
- if (verifyVerdict === "confirmed" || input.verifyExpected === false) {
378
- return { kind: "ready", label: REVIEW_PHASE_TEXT.ready, ready: true };
379
- }
380
- if (input.verifierDead) {
381
- return { kind: "verify_dead", label: REVIEW_PHASE_TEXT.verify_dead, ready: true };
382
- }
383
- return { kind: "awaiting_verify", label: REVIEW_PHASE_TEXT.awaiting_verify, ready: false };
384
- }
385
- if (ciRed) {
386
- const jobs = (input.ciFailing ?? []).filter((j) => j && j !== "?");
387
- const label3 = jobs.length ? `${REVIEW_PHASE_TEXT.ci_fixing}: ${jobs.join(", ")}` : REVIEW_PHASE_TEXT.ci_fixing;
388
- return { kind: "ci_fixing", label: label3, ready: false };
389
- }
390
- if (reviewVerdict === "changes") {
391
- return { kind: "changes", label: REVIEW_PHASE_TEXT.changes, ready: false };
392
- }
393
- if (input.reviewerName) {
394
- return { kind: "in_review", label: `\u041D\u0430 \u0440\u0435\u0432\u044C\u044E \u0443 ${input.reviewerName}`, ready: false };
395
- }
396
- if (input.reviewerDead) {
397
- return { kind: "reviewer_dead", label: REVIEW_PHASE_TEXT.reviewer_dead, ready: true };
398
- }
399
- const p = input.ciProgress;
400
- if (p && p.total > 0 && p.pass >= p.total) {
401
- return { kind: "awaiting_reviewer", label: REVIEW_PHASE_TEXT.awaiting_reviewer, ready: false };
402
- }
403
- const label2 = p && p.total > 0 ? `${REVIEW_PHASE_TEXT.checks} \u2014 CI ${p.pass}/${p.total}` : REVIEW_PHASE_TEXT.checks;
404
- return { kind: "checks", label: label2, ready: false };
405
- }
406
- var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
407
- var init_reviewPhase = __esm({
408
- "../swarm-core/src/reviewPhase.ts"() {
409
- "use strict";
410
- init_verifyVerdict();
411
- REVIEW_PHASE_TEXT = {
412
- checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
413
- awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
414
- ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
415
- in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
416
- changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
417
- 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",
418
- 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",
419
- 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",
420
- 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",
421
- ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
422
- 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"
423
- };
424
- PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
425
- (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
426
- );
427
- }
428
- });
429
-
430
440
  // ../swarm-core/src/mergeGate.ts
431
441
  function computeMergeGate(input) {
432
442
  if (!input.hasRun) {
@@ -1934,7 +1944,7 @@ var init_effort = __esm({
1934
1944
  });
1935
1945
 
1936
1946
  // ../swarm-core/src/queuePlan.ts
1937
- var MIN_MS, QUEUE_NOMINAL_RUN_MS, QUEUE_MIN_TAIL_MS, QUEUE_REVIEW_TAIL_MS;
1947
+ var MIN_MS, QUEUE_NOMINAL_RUN_MS, QUEUE_MIN_TAIL_MS;
1938
1948
  var init_queuePlan = __esm({
1939
1949
  "../swarm-core/src/queuePlan.ts"() {
1940
1950
  "use strict";
@@ -1945,7 +1955,6 @@ var init_queuePlan = __esm({
1945
1955
  MIN_MS = 6e4;
1946
1956
  QUEUE_NOMINAL_RUN_MS = 25 * MIN_MS;
1947
1957
  QUEUE_MIN_TAIL_MS = 3 * MIN_MS;
1948
- QUEUE_REVIEW_TAIL_MS = 10 * MIN_MS;
1949
1958
  }
1950
1959
  });
1951
1960
 
@@ -12480,7 +12489,7 @@ function parkReasonUnretriable(reason) {
12480
12489
  // package.json
12481
12490
  var package_default = {
12482
12491
  name: "@taskless-app/swarm",
12483
- version: "0.6.50",
12492
+ version: "0.6.52",
12484
12493
  type: "module",
12485
12494
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
12486
12495
  keywords: [
@@ -12531,7 +12540,7 @@ var package_default = {
12531
12540
  execa: "^10.0.0",
12532
12541
  zod: "3.25.76"
12533
12542
  },
12534
- gitHead: "859ffe2d91bf4c791af067147515b14a41b96739"
12543
+ gitHead: "f77e0473fd2f80e4418ceadd1b29b9a70241fc04"
12535
12544
  };
12536
12545
 
12537
12546
  // src/version.ts
@@ -14932,6 +14941,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
14932
14941
  if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
14933
14942
  if (!run.pr_url) continue;
14934
14943
  if (run.review_verdict === "approve") {
14944
+ if (reviewPhaseKindOf(run.review_phase) === "ready_awaiting_ci") continue;
14935
14945
  candidates.push({
14936
14946
  key: `pr_stuck_merge:${run.id}`,
14937
14947
  minDurationMs: thresholds.prStuckMergeMs,
@@ -18093,6 +18103,14 @@ function compareQueuePriority(a, b) {
18093
18103
  function orderQueue(runs) {
18094
18104
  return [...runs].sort(compareQueuePriority);
18095
18105
  }
18106
+ function placeReturners(returners, queued, unmet) {
18107
+ const awaited = new Set(queued.flatMap(unmet));
18108
+ const first = (r) => r.entry?.headPassed === true || awaited.has(r.run.task_slug);
18109
+ return {
18110
+ beforeHead: returners.filter(first),
18111
+ afterHead: returners.filter((r) => !first(r))
18112
+ };
18113
+ }
18096
18114
 
18097
18115
  // src/watchdog.ts
18098
18116
  init_src();
@@ -18476,6 +18494,7 @@ var CONFLICT_RESOLVE_COOLDOWN_MS = Number(process.env.SWARM_CONFLICT_COOLDOWN_MS
18476
18494
  var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
18477
18495
  var BASE_MERGED_MARK = "\u0431\u0430\u0437\u0430 \u0432\u043B\u0438\u0442\u0430 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438";
18478
18496
  var BASE_NOOP_MARK = "\u0432\u043B\u0438\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E";
18497
+ var RESOLVER_RESOLVED_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 main \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D \u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440\u043E\u043C";
18479
18498
  var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
18480
18499
  var REVIEW_CYCLE_LIMIT = Number(process.env.SWARM_REVIEW_CYCLE_LIMIT) || 3;
18481
18500
  var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
@@ -18602,6 +18621,26 @@ function lastCiCheckMs(db, runId) {
18602
18621
  if (!evs.length) return null;
18603
18622
  return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
18604
18623
  }
18624
+ function headMovedBy(e) {
18625
+ if (e.kind !== "conflict") return false;
18626
+ const text = e.text ?? "";
18627
+ return text.startsWith(BASE_MERGED_MARK) || text.includes(RESOLVER_RESOLVED_MARK);
18628
+ }
18629
+ function ciGreenForHead(db, run, events) {
18630
+ if (ciWatchCloseKind(db, run) != null) return true;
18631
+ for (let i = events.length - 1; i >= 0; i -= 1) {
18632
+ const e = events[i];
18633
+ if (headMovedBy(e)) return false;
18634
+ if (e.kind === "ci_wake") {
18635
+ if ((e.text ?? "").startsWith("\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C")) continue;
18636
+ return false;
18637
+ }
18638
+ if (e.kind === "ci_checked") {
18639
+ return /^CI (pass|зелёный) /.test(e.text ?? "");
18640
+ }
18641
+ }
18642
+ return false;
18643
+ }
18605
18644
  function reviewPhaseInputFor(deps, run, projectWantsVerify = true) {
18606
18645
  const events = deps.db.events(run.id);
18607
18646
  let ciRed = false;
@@ -18632,6 +18671,7 @@ function reviewPhaseInputFor(deps, run, projectWantsVerify = true) {
18632
18671
  return {
18633
18672
  reviewVerdict: run.review_verdict,
18634
18673
  ciRed,
18674
+ ciGreen: ciGreenForHead(deps.db, run, events),
18635
18675
  ciFailing,
18636
18676
  ciProgress,
18637
18677
  reviewerName: reviewer?.name ?? null,
@@ -19604,6 +19644,7 @@ async function handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap)
19604
19644
  return;
19605
19645
  }
19606
19646
  const ci = await deps.ci.checks(run.pr_url).catch(() => null);
19647
+ if (ci?.bucket !== "fail" && deps.returners?.get(run.id)?.kind === "ci") deps.returners.delete(run.id);
19607
19648
  if (ci?.bucket === "denied") {
19608
19649
  const seen = ciDeniedStreak(deps.db, run.id) + 1;
19609
19650
  if (seen < CI_DENIED_LIMIT) {
@@ -19637,7 +19678,11 @@ async function handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap)
19637
19678
  return;
19638
19679
  }
19639
19680
  if (ci?.bucket === "fail") {
19640
- if (!await wakeAuthorOnRedCi(deps, run, now, ci, worktreeExists, hasTranscript, droneCap)) {
19681
+ const capFull = countActiveDrones(deps.db) >= droneCap;
19682
+ if (await wakeAuthorOnRedCi(deps, run, now, ci, worktreeExists, hasTranscript, droneCap)) {
19683
+ deps.returners?.delete(run.id);
19684
+ } else {
19685
+ if (capFull || deps.returners?.get(run.id)?.kind === "ci") noteReturner(deps, run, now, { kind: "ci", ci, seenAt: now, since: now });
19641
19686
  deps.db.addEvent(run.id, "ci_checked", `CI fail (${ci.pass}/${ci.total}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u0430\u0432\u0442\u043E\u0440\u0430 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0447\u0435\u043C/\u0440\u0430\u043D\u043E, \u0436\u0434\u0443 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043E\u043A\u043D\u0430`);
19642
19687
  }
19643
19688
  return;
@@ -20373,10 +20418,57 @@ async function failBeforeStart(deps, run, reason, opts = {}) {
20373
20418
  }
20374
20419
  return "failed";
20375
20420
  }
20376
- async function batchSpawnQueue(deps, droneCap) {
20377
- for (const run of orderQueue(deps.db.listRuns("queued"))) {
20421
+ async function batchSpawnQueue(deps, droneCap, now) {
20422
+ const queue = orderQueue(deps.db.listRuns("queued"));
20423
+ const { beforeHead, afterHead } = placeReturners(pendingReturners(deps), queue, (q) => deps.db.unmetDeps(q.depends_on));
20424
+ await liftReturners(deps, beforeHead, now, droneCap);
20425
+ let afterHeadDue = afterHead.length > 0;
20426
+ for (const run of queue) {
20378
20427
  if (countActiveDrones(deps.db) >= droneCap) break;
20379
- await trySpawnQueuedRun(deps, run);
20428
+ if (await trySpawnQueuedRun(deps, run) === "spawned" && afterHeadDue) {
20429
+ afterHeadDue = false;
20430
+ for (const { entry } of afterHead) entry.headPassed = true;
20431
+ await liftReturners(deps, afterHead, now, droneCap);
20432
+ }
20433
+ }
20434
+ if (afterHeadDue) await liftReturners(deps, afterHead, now, droneCap);
20435
+ }
20436
+ var RETURNER_CI_TTL_MS = 3 * CI_CHECK_COOLDOWN_MS;
20437
+ function noteReturner(deps, run, now, entry) {
20438
+ const pool = deps.returners ??= /* @__PURE__ */ new Map();
20439
+ const prev = pool.get(run.id);
20440
+ pool.set(run.id, { ...entry, since: prev?.since ?? entry.since, ...prev?.headPassed ? { headPassed: true } : {} });
20441
+ }
20442
+ function pendingReturners(deps) {
20443
+ const pool = deps.returners;
20444
+ if (!pool?.size) return [];
20445
+ const out = [];
20446
+ for (const [id, entry] of pool) {
20447
+ const run = deps.db.getRun(id);
20448
+ if (!run || run.status !== "review" || !run.pr_url) pool.delete(id);
20449
+ else out.push({ run, entry });
20450
+ }
20451
+ return out.sort((a, b) => a.entry.since - b.entry.since || a.run.id - b.run.id);
20452
+ }
20453
+ async function liftReturners(deps, returners, now, droneCap) {
20454
+ for (const { run, entry } of returners) {
20455
+ if (countActiveDrones(deps.db) >= droneCap) return;
20456
+ if (!deps.respawn || deps.db.activeResolverFor(run.id)) continue;
20457
+ let lifted;
20458
+ if (entry.kind === "changes") {
20459
+ if (!needsFixIteration(run)) {
20460
+ deps.returners?.delete(run.id);
20461
+ continue;
20462
+ }
20463
+ lifted = await wakeAuthorOnChanges(deps, run, now, droneCap);
20464
+ } else {
20465
+ if (!deps.ci || now - entry.seenAt > RETURNER_CI_TTL_MS) {
20466
+ deps.returners?.delete(run.id);
20467
+ continue;
20468
+ }
20469
+ lifted = await wakeAuthorOnRedCi(deps, run, now, entry.ci, deps.worktreeExists ?? existsSync20, deps.hasTranscript ?? executorHasSession, droneCap);
20470
+ }
20471
+ if (lifted) deps.returners?.delete(run.id);
20380
20472
  }
20381
20473
  }
20382
20474
  async function resumeWatchdogParked(deps, droneCap) {
@@ -20668,7 +20760,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
20668
20760
  const pausedForRelief = deps.db.listRuns("paused").filter((r) => isReliefPause(r.stop_reason));
20669
20761
  const recovering = pausedForRelief.length > 0 || state.overloadedAt != null && now - state.overloadedAt < cfg.rampWindowMs;
20670
20762
  if (!recovering) {
20671
- await batchSpawnQueue(deps, droneCap);
20763
+ await batchSpawnQueue(deps, droneCap, now);
20672
20764
  return;
20673
20765
  }
20674
20766
  if (!rampDue(state.lastRampAt ?? null, now, cfg)) return;
@@ -21158,7 +21250,7 @@ async function tickOnce(deps) {
21158
21250
  if (deps.resourcesOk) {
21159
21251
  await manageRelief(deps, now, droneCap, stormActive);
21160
21252
  } else if (!stormActive) {
21161
- await batchSpawnQueue(deps, droneCap);
21253
+ await batchSpawnQueue(deps, droneCap, now);
21162
21254
  }
21163
21255
  if (!stormActive) {
21164
21256
  await resumeWatchdogParked(deps, droneCap);
@@ -21175,7 +21267,7 @@ async function tickOnce(deps) {
21175
21267
  });
21176
21268
  deps.db.updateRun(run.id, { status: "done" });
21177
21269
  deps.db.addEvent(run.id, "done", "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D, main \u0432\u043B\u0438\u0442, PR \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D");
21178
- deps.db.addEvent(run.resolves_run_id, "conflict", `\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 main \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D \u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440\u043E\u043C ${run.name} \u2014 PR \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D`);
21270
+ deps.db.addEvent(run.resolves_run_id, "conflict", `${RESOLVER_RESOLVED_MARK} ${run.name} \u2014 PR \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D`);
21179
21271
  continue;
21180
21272
  }
21181
21273
  if (run.reviews_run_id != null) {
@@ -21228,6 +21320,7 @@ async function tickOnce(deps) {
21228
21320
  const prUrl = run.pr_url;
21229
21321
  const st = await deps.prStatus(prUrl);
21230
21322
  run = syncBranchFromPr(deps.db, run, st.headBranch);
21323
+ if (st.conflicting) deps.returners?.delete(run.id);
21231
21324
  if (st.merged) {
21232
21325
  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.`);
21233
21326
  continue;
@@ -21334,8 +21427,13 @@ async function tickOnce(deps) {
21334
21427
  } else if (!st.conflicting && deps.ci && deps.respawn && !stormActive) {
21335
21428
  await handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap);
21336
21429
  }
21337
- if (needsFixIteration(run) && !stormActive && !st.conflicting && !deps.db.activeResolverFor(run.id) && await wakeAuthorOnChanges(deps, run, now, droneCap)) {
21338
- continue;
21430
+ if (needsFixIteration(run) && !stormActive && !st.conflicting && !deps.db.activeResolverFor(run.id)) {
21431
+ const capFull = countActiveDrones(deps.db) >= droneCap;
21432
+ if (await wakeAuthorOnChanges(deps, run, now, droneCap)) {
21433
+ deps.returners?.delete(run.id);
21434
+ continue;
21435
+ }
21436
+ if (capFull) noteReturner(deps, run, now, { kind: "changes", since: now });
21339
21437
  }
21340
21438
  const reviewerGatesOk = !st.conflicting && !st.isDraft && !!deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt) && deps.db.reviewerCountFor(run.id) === 0 && // ГЕЙТ ПЕРВЫМ: живой/успешный ревьюер уже есть — ни капа, ни спавна не трогаем
21341
21439
  // …и НИ ОДНОГО живого/ждущего ревьюера ЛЮБОГО цикла (TASWM-299). reviewerCountFor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.50",
3
+ "version": "0.6.52",
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": "859ffe2d91bf4c791af067147515b14a41b96739",
42
+ "gitHead": "f77e0473fd2f80e4418ceadd1b29b9a70241fc04",
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",