@taskless-app/swarm 0.6.49 → 0.6.51

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 +326 -116
  2. package/dist/overmind.js +630 -158
  3. package/package.json +2 -2
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) {
@@ -1723,13 +1733,61 @@ function similarTasks(model, text, limit = 5) {
1723
1733
  }
1724
1734
  return scored.sort((a, b) => b.score - a.score).slice(0, limit);
1725
1735
  }
1736
+ function historyFiles(model) {
1737
+ let idx = historyIndex.get(model);
1738
+ if (!idx) {
1739
+ const files = new Set(model.entries.flatMap((e) => e.files));
1740
+ const byBase = /* @__PURE__ */ new Map();
1741
+ for (const f of files) {
1742
+ const base3 = f.slice(f.lastIndexOf("/") + 1);
1743
+ byBase.set(base3, [...byBase.get(base3) ?? [], f]);
1744
+ }
1745
+ idx = { files, byBase };
1746
+ historyIndex.set(model, idx);
1747
+ }
1748
+ return idx;
1749
+ }
1750
+ function namedPaths(model, text, fileExists) {
1751
+ const { files, byBase } = historyFiles(model);
1752
+ const known = (p) => files.has(p) || (fileExists?.(p) ?? false);
1753
+ const raw = [...new Set((text.match(PATH_CANDIDATE) ?? []).map((c) => c.replace(/^\.?\/+/, "").replace(/\.+$/, "")))].filter((c) => c && !c.split("/").includes(".."));
1754
+ raw.sort((a, b) => b.split("/").length - a.split("/").length);
1755
+ const out = [];
1756
+ const dirs = /* @__PURE__ */ new Set();
1757
+ const take = (p) => {
1758
+ if (out.includes(p)) return;
1759
+ out.push(p);
1760
+ dirs.add(dirOf(p));
1761
+ };
1762
+ for (const c of raw) {
1763
+ if (known(c)) {
1764
+ take(c);
1765
+ continue;
1766
+ }
1767
+ const base3 = c.slice(c.lastIndexOf("/") + 1);
1768
+ let matches = (byBase.get(base3) ?? []).filter((f) => f === c || f.endsWith(`/${c}`));
1769
+ if (matches.length > 1) matches = matches.filter((f) => dirs.has(dirOf(f)));
1770
+ if (matches.length === 1) {
1771
+ take(matches[0]);
1772
+ continue;
1773
+ }
1774
+ if (!c.includes("/") && fileExists) {
1775
+ const sibling = [...dirs].map((d) => d ? `${d}/${c}` : c).filter(fileExists);
1776
+ if (sibling.length === 1) take(sibling[0]);
1777
+ }
1778
+ }
1779
+ return out;
1780
+ }
1726
1781
  function predictFiles(model, opts) {
1727
1782
  if (!hasZoneModel(model)) return [];
1728
1783
  const limit = opts.limit ?? 40;
1729
1784
  const best = /* @__PURE__ */ new Map();
1785
+ const named = opts.text ? namedPaths(model, opts.text, opts.fileExists) : [];
1786
+ for (const path of named) best.set(path, { path, weight: 1, why: "\u043D\u0430\u0437\u0432\u0430\u043D \u0432 \u0437\u0430\u0434\u0430\u0447\u0435", named: true });
1787
+ const factor = named.length ? NAMED_BACKGROUND_FACTOR : 1;
1730
1788
  const put = (path, weight, why) => {
1731
1789
  const cur = best.get(path);
1732
- if (!cur || cur.weight < weight) best.set(path, { path, weight, why });
1790
+ if (!cur || cur.weight < weight * factor) best.set(path, { path, weight: weight * factor, why });
1733
1791
  };
1734
1792
  if (opts.text) {
1735
1793
  const sim = similarTasks(model, opts.text, opts.similar ?? 5);
@@ -1746,12 +1804,18 @@ function predictFiles(model, opts) {
1746
1804
  }
1747
1805
  return [...best.values()].sort((a, b) => b.weight - a.weight || a.path.localeCompare(b.path)).slice(0, limit);
1748
1806
  }
1807
+ function heldByPrediction(predicted) {
1808
+ const named = predicted.filter((p) => p.named);
1809
+ return (named.length ? named : predicted).map((p) => p.path);
1810
+ }
1749
1811
  function overlapRisk(predicted, actual, opts = {}) {
1750
1812
  const o = { ...OVERLAP_DEFAULTS, ...opts };
1751
1813
  const actualSet = new Set(actual);
1752
1814
  const actualDirs = new Set([...actualSet].map(dirOf).filter(Boolean));
1753
1815
  const totalWeight = predicted.reduce((a, p) => a + p.weight, 0);
1754
1816
  if (!totalWeight) return { overlap: false, score: 0, sharedFiles: [], sharedDirs: [] };
1817
+ const hasNamed = predicted.some((p) => p.named);
1818
+ let namedHit = false;
1755
1819
  let hit = 0;
1756
1820
  let strong = false;
1757
1821
  const sharedFiles = [];
@@ -1760,24 +1824,26 @@ function overlapRisk(predicted, actual, opts = {}) {
1760
1824
  if (actualSet.has(p.path)) {
1761
1825
  hit += p.weight;
1762
1826
  sharedFiles.push(p.path);
1763
- if (p.weight >= o.strongFile) strong = true;
1827
+ if (p.named) namedHit = true;
1828
+ if (p.weight >= o.strongFile && (p.named || !hasNamed)) strong = true;
1764
1829
  continue;
1765
1830
  }
1766
1831
  const d = dirOf(p.path);
1767
1832
  if (d && actualDirs.has(d)) {
1768
1833
  hit += p.weight * 0.25;
1769
1834
  sharedDirs.add(d);
1835
+ if (p.named) namedHit = true;
1770
1836
  }
1771
1837
  }
1772
1838
  const score = Math.min(1, hit / totalWeight);
1773
1839
  return {
1774
- overlap: strong || score >= o.threshold,
1840
+ overlap: strong || score >= o.threshold && (!hasNamed || namedHit),
1775
1841
  score,
1776
1842
  sharedFiles: sharedFiles.slice(0, 10),
1777
1843
  sharedDirs: [...sharedDirs].slice(0, 5)
1778
1844
  };
1779
1845
  }
1780
- var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS, OVERLAP_DEFAULTS;
1846
+ var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS, PATH_CANDIDATE, historyIndex, NAMED_BACKGROUND_FACTOR, OVERLAP_DEFAULTS;
1781
1847
  var init_codeZones = __esm({
1782
1848
  "../swarm-core/src/codeZones.ts"() {
1783
1849
  "use strict";
@@ -1856,6 +1922,9 @@ var init_codeZones = __esm({
1856
1922
  "\u043F\u0435\u0440\u0444",
1857
1923
  "wip"
1858
1924
  ]);
1925
+ PATH_CANDIDATE = /[\w@.\-/]+\.[A-Za-z][A-Za-z0-9]{0,5}/g;
1926
+ historyIndex = /* @__PURE__ */ new WeakMap();
1927
+ NAMED_BACKGROUND_FACTOR = 0.1;
1859
1928
  OVERLAP_DEFAULTS = { threshold: 0.2, strongFile: 0.35 };
1860
1929
  }
1861
1930
  });
@@ -1875,7 +1944,7 @@ var init_effort = __esm({
1875
1944
  });
1876
1945
 
1877
1946
  // ../swarm-core/src/queuePlan.ts
1878
- 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;
1879
1948
  var init_queuePlan = __esm({
1880
1949
  "../swarm-core/src/queuePlan.ts"() {
1881
1950
  "use strict";
@@ -1886,7 +1955,6 @@ var init_queuePlan = __esm({
1886
1955
  MIN_MS = 6e4;
1887
1956
  QUEUE_NOMINAL_RUN_MS = 25 * MIN_MS;
1888
1957
  QUEUE_MIN_TAIL_MS = 3 * MIN_MS;
1889
- QUEUE_REVIEW_TAIL_MS = 10 * MIN_MS;
1890
1958
  }
1891
1959
  });
1892
1960
 
@@ -6253,6 +6321,10 @@ function rowToRun(row) {
6253
6321
  // вычисляемая фаза ревью (TASWM-230)
6254
6322
  review_cycle: row.review_cycle ?? 0,
6255
6323
  // старые прогоны без колонки → 0
6324
+ // TASWM-577: намерение смержить (человек отметил «пора влить»). В sqlite-бэкенде оно
6325
+ // живёт только здесь — ставить его некому, кроме самого демона, зато читает его
6326
+ // реконсайл, и колонка нужна, чтобы отметка пережила рестарт.
6327
+ merge_requested_at: row.merge_requested_at ?? null,
6256
6328
  task_title: row.task_title ?? null,
6257
6329
  task_desc: row.task_desc ?? null,
6258
6330
  // TASWM-556: когда заведена сама задача — по ней проверка дубля отличает свой
@@ -6328,6 +6400,7 @@ CREATE TABLE IF NOT EXISTS runs (
6328
6400
  verifies_run_id INTEGER,
6329
6401
  verify_verdict TEXT,
6330
6402
  review_cycle INTEGER NOT NULL DEFAULT 0,
6403
+ merge_requested_at TEXT,
6331
6404
  task_title TEXT,
6332
6405
  task_desc TEXT,
6333
6406
  task_created_at TEXT,
@@ -6422,6 +6495,8 @@ CREATE TABLE IF NOT EXISTS imports (
6422
6495
  "ALTER TABLE runs ADD COLUMN verify_verdict TEXT",
6423
6496
  // Авто-ревью v2 (TASWM-162): номер цикла ревью — пере-ревью после фикс-итерации.
6424
6497
  "ALTER TABLE runs ADD COLUMN review_cycle INTEGER NOT NULL DEFAULT 0",
6498
+ // TASWM-577: намерение смержить — по нему рой вливает PR сам, по очереди мержа.
6499
+ "ALTER TABLE runs ADD COLUMN merge_requested_at TEXT",
6425
6500
  // Вычисляемая фаза ревью (TASWM-230): человеко-фраза «чего ждёт PR», её считает демон.
6426
6501
  "ALTER TABLE runs ADD COLUMN review_phase TEXT",
6427
6502
  // Приоритетная очередь (TASWM-269): явный приоритет прогона, выше — раньше из queued.
@@ -7207,7 +7282,7 @@ var init_droneStore = __esm({
7207
7282
 
7208
7283
  // src/overmind.ts
7209
7284
  import { homedir as homedir7 } from "os";
7210
- import { existsSync as existsSync20, readFileSync as readFileSync25, readdirSync as readdirSync14, renameSync as renameSync12 } from "fs";
7285
+ import { existsSync as existsSync20, readFileSync as readFileSync25, readdirSync as readdirSync14, renameSync as renameSync12, statSync as statSync13 } from "fs";
7211
7286
  import { basename as basename6, dirname as dirname5, join as join34 } from "path";
7212
7287
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
7213
7288
  import { spawn as spawn4 } from "child_process";
@@ -7555,6 +7630,12 @@ function depsFailureRetriable(code) {
7555
7630
  function runRef(runKey) {
7556
7631
  return { __runRef: runKey };
7557
7632
  }
7633
+ function isoOrNull(v) {
7634
+ if (v == null) return null;
7635
+ const raw = v instanceof Date ? v.toISOString() : typeof v === "string" ? v : "";
7636
+ const at = Date.parse(raw);
7637
+ return Number.isNaN(at) ? null : new Date(at).toISOString();
7638
+ }
7558
7639
  function isRunRef(v) {
7559
7640
  return typeof v === "object" && v !== null && "__runRef" in v;
7560
7641
  }
@@ -8022,6 +8103,17 @@ var SwarmClient = class _SwarmClient {
8022
8103
  runKeyOf(id) {
8023
8104
  return this.runs.get(id)?.runKey ?? null;
8024
8105
  }
8106
+ /**
8107
+ * Cuid прогона В ПРОДЕ — им адресуются прод-РУЧКИ, а не MCP-тулы (TASWM-577: демон
8108
+ * вливает PR тем же `POST /swarm/merge`, что и кнопка, и снимает намерение тем же
8109
+ * `DELETE /swarm/merge-request/:runId`). null — прогон до прода ещё не долетел
8110
+ * (создан этим демоном, флаш не прошёл): адресовать его там нечем, и вызывающий
8111
+ * обязан прочитать это как «сейчас нельзя», а не как ошибку.
8112
+ */
8113
+ prodRunId(id) {
8114
+ const key = this.runs.get(id)?.runKey;
8115
+ return key ? this.prodIdFor(key) ?? null : null;
8116
+ }
8025
8117
  runByKey(key) {
8026
8118
  const direct = [...this.runs.values()].find((r) => r.runKey === key);
8027
8119
  if (direct) return this.mat(direct);
@@ -9568,6 +9660,9 @@ var SwarmClient = class _SwarmClient {
9568
9660
  verify_verdict: null,
9569
9661
  review_phase: null,
9570
9662
  // вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
9663
+ // TASWM-577: намерение смержить — прод-поле (его ставит человек ручкой
9664
+ // /swarm/merge-request), заполняется ниже из ответа.
9665
+ merge_requested_at: null,
9571
9666
  // Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
9572
9667
  // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
9573
9668
  // в отличие от respawns, — авто-ревью v2, TASWM-162.
@@ -9603,6 +9698,7 @@ var SwarmClient = class _SwarmClient {
9603
9698
  local.task_title = pr.taskTitle ?? local.task_title;
9604
9699
  local.task_desc = pr.taskDesc ?? local.task_desc;
9605
9700
  local.task_created_at = pr.taskCreatedAt ?? local.task_created_at;
9701
+ if ("mergeRequestedAt" in pr) local.merge_requested_at = isoOrNull(pr.mergeRequestedAt);
9606
9702
  local.name = pr.droneName ?? local.name;
9607
9703
  local.kind = pr.role ?? local.kind;
9608
9704
  const heroProd = pr.heroId ?? null;
@@ -12393,7 +12489,7 @@ function parkReasonUnretriable(reason) {
12393
12489
  // package.json
12394
12490
  var package_default = {
12395
12491
  name: "@taskless-app/swarm",
12396
- version: "0.6.49",
12492
+ version: "0.6.51",
12397
12493
  type: "module",
12398
12494
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
12399
12495
  keywords: [
@@ -12444,7 +12540,7 @@ var package_default = {
12444
12540
  execa: "^10.0.0",
12445
12541
  zod: "3.25.76"
12446
12542
  },
12447
- gitHead: "27600667531d4f65761c74793e8c4ee4859dc8ae"
12543
+ gitHead: "24aad254ba4f3f71904310b73435e647a08f22f5"
12448
12544
  };
12449
12545
 
12450
12546
  // src/version.ts
@@ -12762,18 +12858,48 @@ async function prHeadBranch(prUrl, run = realRunner) {
12762
12858
  return null;
12763
12859
  }
12764
12860
  }
12765
- async function prBaseSync(prUrl, run = realRunner) {
12861
+ function prRepoPath(prUrl) {
12766
12862
  const m = prUrl.trim().match(/github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/\d+/i);
12767
- if (!isGithubPrUrl(prUrl) || !m) return null;
12768
- const [, owner, repo] = m;
12863
+ return isGithubPrUrl(prUrl) && m ? `${m[1]}/${m[2]}` : null;
12864
+ }
12865
+ async function prBaseSync(prUrl, run = realRunner) {
12866
+ const repoPath = prRepoPath(prUrl);
12867
+ if (!repoPath) return null;
12769
12868
  try {
12770
12869
  const j = JSON.parse(await run(["pr", "view", prUrl, "--json", "baseRefName,headRefOid"]));
12771
12870
  const base3 = j.baseRefName;
12772
12871
  const head = j.headRefOid;
12773
12872
  if (!base3 || !head) return null;
12774
- const out = await run(["api", `repos/${owner}/${repo}/compare/${base3}...${head}?per_page=1`, "--jq", ".behind_by"]);
12873
+ const out = await run(["api", `repos/${repoPath}/compare/${base3}...${head}?per_page=1`, "--jq", ".behind_by"]);
12775
12874
  const behindBy = Number(out.trim());
12776
- return Number.isFinite(behindBy) && behindBy >= 0 ? { behindBy, base: base3 } : null;
12875
+ return Number.isFinite(behindBy) && behindBy >= 0 ? { behindBy, base: base3, headSha: head } : null;
12876
+ } catch {
12877
+ return null;
12878
+ }
12879
+ }
12880
+ var ACTIONS_RUN_OK = /* @__PURE__ */ new Set(["success", "skipped", "neutral"]);
12881
+ async function prActionsRuns(prUrl, headSha2, run = realRunner) {
12882
+ const repoPath = prRepoPath(prUrl);
12883
+ if (!repoPath || !/^[0-9a-f]{7,40}$/i.test(headSha2)) return null;
12884
+ try {
12885
+ const j = JSON.parse(
12886
+ await run(["api", `repos/${repoPath}/actions/runs?head_sha=${headSha2}&per_page=100&exclude_pull_requests=true`])
12887
+ );
12888
+ const runs = j.workflow_runs;
12889
+ if (!Array.isArray(runs)) return null;
12890
+ if (typeof j.total_count === "number" && j.total_count > runs.length) return null;
12891
+ const latest = /* @__PURE__ */ new Map();
12892
+ for (const r of runs) {
12893
+ const key = `${r.workflow_id ?? r.name ?? "?"}:${r.event ?? "?"}`;
12894
+ const seen = latest.get(key);
12895
+ if (!seen || (r.id ?? 0) > (seen.id ?? 0)) latest.set(key, r);
12896
+ }
12897
+ const label2 = (r, what) => `${r.name || "workflow"} (${what || "?"})`;
12898
+ return {
12899
+ total: runs.length,
12900
+ active: runs.filter((r) => r.status !== "completed").map((r) => label2(r, r.status)),
12901
+ failed: [...latest.values()].filter((r) => r.status === "completed" && !ACTIONS_RUN_OK.has(r.conclusion ?? "")).map((r) => label2(r, r.conclusion))
12902
+ };
12777
12903
  } catch {
12778
12904
  return null;
12779
12905
  }
@@ -14815,6 +14941,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
14815
14941
  if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
14816
14942
  if (!run.pr_url) continue;
14817
14943
  if (run.review_verdict === "approve") {
14944
+ if (reviewPhaseKindOf(run.review_phase) === "ready_awaiting_ci") continue;
14818
14945
  candidates.push({
14819
14946
  key: `pr_stuck_merge:${run.id}`,
14820
14947
  minDurationMs: thresholds.prStuckMergeMs,
@@ -15337,22 +15464,10 @@ function mergeQueueCandidates(runs, repo, repoFallback) {
15337
15464
  }
15338
15465
  var BASE_TTL_MS = 2e4;
15339
15466
  function makeMergeQueueResolver(deps) {
15340
- const memo = /* @__PURE__ */ new Map();
15467
+ const read = deps.headState ?? makeHeadStateReader(deps);
15341
15468
  const evaluate = async (e, prUrl, ready) => {
15342
- const now = Date.now();
15343
- let cached = memo.get(prUrl);
15344
- const ttl = deps.ttlMs ?? BASE_TTL_MS;
15345
- if (!cached || now - cached.at >= ttl) {
15346
- const base3 = await deps.baseSync(prUrl).catch(() => null);
15347
- let checks = "none";
15348
- if (base3?.behindBy === 0 && deps.checks) {
15349
- const ci = await deps.checks(prUrl).catch(() => null);
15350
- checks = ci ? ci.bucket : null;
15351
- }
15352
- cached = { at: now, base: base3, checks };
15353
- memo.set(prUrl, cached);
15354
- }
15355
- return computeMergeQueue({ entry: e, ready, behindBy: cached.base ? cached.base.behindBy : null, checks: cached.checks });
15469
+ const st = await read(prUrl);
15470
+ return computeMergeQueue({ entry: e, ready, behindBy: st.behindBy, checks: st.checks });
15356
15471
  };
15357
15472
  return async (run) => {
15358
15473
  if (!deps.baseSync || !run.pr_url) return null;
@@ -15387,6 +15502,124 @@ function makeMergeQueueResolver(deps) {
15387
15502
  function entryOf(r) {
15388
15503
  return { runId: r.id, prNumber: prNumberOf(r.pr_url), slug: r.task_slug };
15389
15504
  }
15505
+ function makeHeadStateReader(deps) {
15506
+ const memo = /* @__PURE__ */ new Map();
15507
+ return async (prUrl) => {
15508
+ const now = Date.now();
15509
+ const ttl = deps.ttlMs ?? BASE_TTL_MS;
15510
+ const cached = memo.get(prUrl);
15511
+ if (cached && now - cached.at < ttl) return { behindBy: cached.behindBy, checks: cached.checks, headSha: cached.headSha };
15512
+ const base3 = deps.baseSync ? await deps.baseSync(prUrl).catch(() => null) : null;
15513
+ let checks = "none";
15514
+ if (base3?.behindBy === 0 && deps.checks) {
15515
+ const ci = await deps.checks(prUrl).catch(() => null);
15516
+ checks = ci ? ci.bucket : null;
15517
+ }
15518
+ const fresh = { behindBy: base3 ? base3.behindBy : null, checks, headSha: base3?.headSha ?? null };
15519
+ memo.set(prUrl, { at: now, ...fresh });
15520
+ return fresh;
15521
+ };
15522
+ }
15523
+ function hasMergeIntent(r) {
15524
+ return !!(r.merge_requested_at ?? "").trim();
15525
+ }
15526
+ var NO_CI_GRACE_MS = 10 * 6e4;
15527
+ var shortSha = (sha) => sha.slice(0, 7);
15528
+ function makeMergeIntentResolver(deps) {
15529
+ const read = deps.headState ?? makeHeadStateReader(deps);
15530
+ const firstSeen = /* @__PURE__ */ new Map();
15531
+ const ciSettled = async (prUrl, st, now) => {
15532
+ const sha = st.headSha;
15533
+ if (!sha) {
15534
+ return { action: "wait", code: "checks_unknown", reason: "gh \u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043B HEAD PR \u2014 \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C, \u0434\u043E\u0431\u0435\u0436\u0430\u043B \u043B\u0438 \u043D\u0430 \u043D\u0451\u043C CI. \u0421\u043F\u0440\u043E\u0448\u0443 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C" };
15535
+ }
15536
+ if (!deps.actionsRuns) {
15537
+ return { action: "wait", code: "checks_unknown", reason: "\u0440\u043E\u044E \u043D\u0435\u0447\u0435\u043C \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D\u044B GitHub Actions \u043D\u0430 HEAD PR \u2014 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 \u0430\u0433\u0440\u0435\u0433\u0430\u0442\u0443 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u0432\u0441\u043B\u0435\u043F\u0443\u044E \u043D\u0435 \u0432\u043B\u0438\u0432\u0430\u044E" };
15538
+ }
15539
+ const runs = await deps.actionsRuns(prUrl, sha).catch(() => null);
15540
+ if (!runs) {
15541
+ return { action: "wait", code: "checks_unknown", reason: `\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D\u044B GitHub Actions \u043D\u0430 ${shortSha(sha)} \u2014 \u0441\u043F\u0440\u043E\u0448\u0443 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C` };
15542
+ }
15543
+ if (runs.failed.length > 0) {
15544
+ return { action: "clear", code: "checks_red", reason: `\u043D\u0430 ${shortSha(sha)} \u0443\u043F\u0430\u043B \u043F\u0440\u043E\u0433\u043E\u043D GitHub Actions: ${runs.failed.join(", ")} \u2014 \u0432\u043B\u0438\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E, \u043D\u0443\u0436\u043D\u0430 \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0430` };
15545
+ }
15546
+ if (runs.active.length > 0) {
15547
+ return {
15548
+ action: "wait",
15549
+ code: "checks_pending",
15550
+ reason: `\u043D\u0430 ${shortSha(sha)} \u0435\u0449\u0451 \u0438\u0434\u0443\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u044B GitHub Actions: ${runs.active.join(", ")} \u2014 \u0432\u0438\u0434\u0438\u043C\u044B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043C\u043E\u0433\u0443\u0442 \u0431\u044B\u0442\u044C \u0437\u0435\u043B\u0451\u043D\u044B\u043C\u0438, \u043F\u043E\u043A\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0430\u044F \u0432\u043E\u043B\u043D\u0430 \u0434\u0436\u043E\u0431 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430`
15551
+ };
15552
+ }
15553
+ if (st.checks === "none") {
15554
+ if (runs.total > 0) {
15555
+ return { action: "wait", code: "checks_pending", reason: `\u043F\u0440\u043E\u0433\u043E\u043D\u044B GitHub Actions \u043D\u0430 ${shortSha(sha)} \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0438\u0441\u044C, \u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u0443 PR \u0435\u0449\u0451 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E \u2014 \u0436\u0434\u0443, \u043F\u043E\u043A\u0430 GitHub \u0438\u0445 \u043F\u043E\u043A\u0430\u0436\u0435\u0442` };
15556
+ }
15557
+ const seen = firstSeen.get(prUrl);
15558
+ const waited = seen && seen.sha === sha ? now - seen.at : 0;
15559
+ const graceMin = NO_CI_GRACE_MS / 6e4;
15560
+ if (waited < NO_CI_GRACE_MS) {
15561
+ return {
15562
+ action: "wait",
15563
+ code: "checks_pending",
15564
+ reason: `\u043D\u0430 ${shortSha(sha)} \u043F\u043E\u043A\u0430 \u043D\u0438 \u043E\u0434\u043D\u043E\u0439 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0438 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 GitHub Actions \u2014 CI \u043C\u043E\u0433 \u0435\u0449\u0451 \u043D\u0435 \u0437\u0430\u0432\u0435\u0441\u0442\u0438 \u0434\u0436\u043E\u0431\u044B \u043F\u043E\u0441\u043B\u0435 \u043F\u0443\u0448\u0430. \u0416\u0434\u0443 \u0434\u043E ${graceMin} \u043C\u0438\u043D (\u043F\u0440\u043E\u0448\u043B\u043E ${Math.floor(waited / 6e4)})`
15565
+ };
15566
+ }
15567
+ return { action: "merge", code: "queue_head", reason: `\u0435\u0433\u043E \u0447\u0435\u0440\u0451\u0434 \u0438 \u0431\u0430\u0437\u0430 \u0441\u0432\u0435\u0436\u0430\u044F, \u0430 ${shortSha(sha)} ${graceMin} \u043C\u0438\u043D \u043F\u0440\u043E\u0441\u0442\u043E\u044F\u043B \u0431\u0435\u0437 \u0435\u0434\u0438\u043D\u043E\u0439 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0438 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 GitHub Actions \u2014 CI \u0443 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u043D\u0435\u0442` };
15568
+ }
15569
+ return { action: "merge", code: "queue_head", reason: `\u0435\u0433\u043E \u0447\u0435\u0440\u0451\u0434, \u0431\u0430\u0437\u0430 \u0441\u0432\u0435\u0436\u0430\u044F, \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043D\u0430 ${shortSha(sha)} \u0437\u0435\u043B\u0451\u043D\u044B\u0435 \u0438 \u0432\u0441\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B GitHub Actions \u043D\u0430 \u043D\u0451\u043C \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u044B` };
15570
+ };
15571
+ const confirmLive = async (prUrl, sha, verdict) => {
15572
+ if (!sha) {
15573
+ return { action: "wait", code: "checks_unknown", reason: "gh \u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043B HEAD PR \u2014 \u043D\u0435 \u043F\u043E\u043D\u044F\u0442\u044C, \u043F\u0440\u043E \u043A\u0430\u043A\u043E\u0439 \u043A\u043E\u043C\u043C\u0438\u0442 \u044D\u0442\u043E\u0442 \u0432\u0435\u0440\u0434\u0438\u043A\u0442; \u0441\u043F\u0440\u043E\u0448\u0443 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C" };
15574
+ }
15575
+ const live = deps.baseSync ? await deps.baseSync(prUrl).catch(() => null) : null;
15576
+ if (!live || !live.headSha) {
15577
+ return { action: "wait", code: "base_unknown", reason: "\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0435\u0440\u0435\u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u0436\u0438\u0432\u043E\u0439 HEAD PR \u2014 \u0441\u043F\u0440\u043E\u0448\u0443 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C" };
15578
+ }
15579
+ if (verdict.action === "merge" && live.behindBy > 0) {
15580
+ return {
15581
+ action: "refresh_base",
15582
+ code: "base_stale",
15583
+ reason: `\u0431\u0430\u0437\u0430 \u0443\u0448\u043B\u0430 \u0432\u043F\u0435\u0440\u0451\u0434 \u043D\u0430 ${live.behindBy} \u043A\u043E\u043C\u043C\u0438\u0442(\u043E\u0432) \u043F\u043E\u0441\u043B\u0435 \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0433\u043E \u043C\u0435\u0440\u0436\u0430 \u2014 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u043B\u0438\u0442\u044C main \u0438 \u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F CI \u043D\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D\u043D\u043E\u0439 \u0431\u0430\u0437\u0435`
15584
+ };
15585
+ }
15586
+ if (live.headSha !== sha) {
15587
+ return { action: "wait", code: "checks_pending", reason: `\u043D\u0430 PR \u043D\u043E\u0432\u044B\u0439 \u043A\u043E\u043C\u043C\u0438\u0442 ${shortSha(live.headSha)} \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044E \u0437\u0430\u043D\u043E\u0432\u043E` };
15588
+ }
15589
+ return verdict;
15590
+ };
15591
+ return async (run) => {
15592
+ if (!deps.baseSync || !run.pr_url || !hasMergeIntent(run)) return null;
15593
+ const num = prNumberOf(run.pr_url);
15594
+ if (num == null) return null;
15595
+ const marked = mergeQueueCandidates(deps.listRuns(), effectiveRepoOf(run.repo, deps.repoFallback), deps.repoFallback).filter(hasMergeIntent);
15596
+ const mine = marked.find((c) => prNumberOf(c.pr_url) === num);
15597
+ if (!mine) return null;
15598
+ const queue = orderMergeQueue(marked.map(entryOf));
15599
+ const entry = entryOf(mine);
15600
+ const position = queue.findIndex((e) => e.runId === entry.runId) + 1;
15601
+ const head = queue[0];
15602
+ if (head.runId !== entry.runId) {
15603
+ return {
15604
+ action: "wait",
15605
+ code: "queue_wait",
15606
+ reason: `\u0440\u043E\u0439 \u0432\u043B\u0438\u0432\u0430\u0435\u0442 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443: \u0441\u0435\u0439\u0447\u0430\u0441 \u043E\u0447\u0435\u0440\u0435\u0434\u044C PR #${head.prNumber}${head.slug ? ` (${head.slug})` : ""}, \u044D\u0442\u043E\u0442 \u2014 ${position}-\u0439 \u0438\u0437 ${queue.length}`,
15607
+ position,
15608
+ size: queue.length
15609
+ };
15610
+ }
15611
+ const st = await read(mine.pr_url);
15612
+ const now = deps.now ? deps.now() : Date.now();
15613
+ if (st.headSha) {
15614
+ const seen = firstSeen.get(mine.pr_url);
15615
+ if (!seen || seen.sha !== st.headSha) firstSeen.set(mine.pr_url, { sha: st.headSha, at: now });
15616
+ }
15617
+ const d = computeMergeQueue({ entry, ready: queue, behindBy: st.behindBy, checks: st.checks });
15618
+ const verdict = d.code === "queue_head" ? await ciSettled(mine.pr_url, st, now) : { action: d.code === "base_stale" ? "refresh_base" : d.code === "checks_red" ? "clear" : "wait", code: d.code, reason: d.reason };
15619
+ const final = verdict.action === "merge" || verdict.action === "clear" ? await confirmLive(mine.pr_url, st.headSha, verdict) : verdict;
15620
+ return { ...final, position, size: queue.length, ...st.headSha ? { headSha: st.headSha } : {} };
15621
+ };
15622
+ }
15390
15623
  function needsBaseRefresh(d) {
15391
15624
  return d?.code === "base_stale";
15392
15625
  }
@@ -15418,9 +15651,15 @@ function makeMergeGateParts(deps) {
15418
15651
  });
15419
15652
  };
15420
15653
  const repoOfRun = deps.repoFullNameOf ? (r) => deps.repoFullNameOf(effectiveRepoOf(r.repo, deps.repoFallback)) : void 0;
15654
+ const headState = makeHeadStateReader({
15655
+ ...deps.baseSync ? { baseSync: deps.baseSync } : {},
15656
+ ...deps.checks ? { checks: deps.checks } : {},
15657
+ ...deps.ttlMs != null ? { ttlMs: deps.ttlMs } : {}
15658
+ });
15421
15659
  const queueFor = makeMergeQueueResolver({
15422
15660
  listRuns: deps.listRuns,
15423
15661
  gateAllows: async (r) => (await gateFor(r)).allowed,
15662
+ headState,
15424
15663
  ...deps.repoFallback ? { repoFallback: deps.repoFallback } : {},
15425
15664
  ...deps.baseSync ? { baseSync: deps.baseSync } : {},
15426
15665
  ...deps.checks ? { checks: deps.checks } : {},
@@ -15446,7 +15685,68 @@ function makeMergeGateParts(deps) {
15446
15685
  }
15447
15686
  };
15448
15687
  };
15449
- return { gate, queue: queueFor };
15688
+ const intentFor = makeMergeIntentResolver({
15689
+ listRuns: deps.listRuns,
15690
+ headState,
15691
+ ...deps.repoFallback ? { repoFallback: deps.repoFallback } : {},
15692
+ ...deps.baseSync ? { baseSync: deps.baseSync } : {},
15693
+ ...deps.checks ? { checks: deps.checks } : {},
15694
+ ...deps.actionsRuns ? { actionsRuns: deps.actionsRuns } : {},
15695
+ ...deps.ttlMs != null ? { ttlMs: deps.ttlMs } : {}
15696
+ });
15697
+ return { gate, queue: queueFor, intent: intentFor };
15698
+ }
15699
+
15700
+ // src/prodMerge.ts
15701
+ init_config();
15702
+ init_taskless();
15703
+ var TIMEOUT_MS = 3e4;
15704
+ async function tokenFor(api, deps) {
15705
+ return deps.token ? deps.token() : resolveApiToken(api);
15706
+ }
15707
+ async function prodMergePr(runId, sha, deps = {}) {
15708
+ const api = deps.api ?? TASKLESS_API;
15709
+ const doFetch = deps.fetchFn ?? fetch;
15710
+ try {
15711
+ const token = await tokenFor(api, deps);
15712
+ if (!token) return { ok: false, error: "\u043D\u0435\u0447\u0435\u043C \u0445\u043E\u0434\u0438\u0442\u044C \u0432 \u043F\u0440\u043E\u0434 (\u043D\u0435\u0442 \u0442\u043E\u043A\u0435\u043D\u0430)" };
15713
+ const res = await doFetch(restEndpoint(api, "/swarm/merge"), {
15714
+ method: "POST",
15715
+ headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
15716
+ body: JSON.stringify(sha ? { runId, sha } : { runId }),
15717
+ signal: AbortSignal.timeout(TIMEOUT_MS)
15718
+ });
15719
+ if (!res.ok) {
15720
+ const text = await res.text().catch(() => "");
15721
+ let code;
15722
+ try {
15723
+ code = JSON.parse(text).error;
15724
+ } catch {
15725
+ code = void 0;
15726
+ }
15727
+ if (code === "head_moved") return { ok: false, error: "HEAD PR \u0441\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438", headMoved: true };
15728
+ return { ok: false, error: `\u043F\u0440\u043E\u0434 \u043E\u0442\u043A\u0430\u0437\u0430\u043B (${res.status}): ${text.slice(0, 300)}` };
15729
+ }
15730
+ return { ok: true };
15731
+ } catch (e) {
15732
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
15733
+ }
15734
+ }
15735
+ async function prodClearMergeIntent(runId, deps = {}) {
15736
+ const api = deps.api ?? TASKLESS_API;
15737
+ const doFetch = deps.fetchFn ?? fetch;
15738
+ try {
15739
+ const token = await tokenFor(api, deps);
15740
+ if (!token) return false;
15741
+ const res = await doFetch(restEndpoint(api, `/swarm/merge-request/${encodeURIComponent(runId)}`), {
15742
+ method: "DELETE",
15743
+ headers: { Authorization: `Bearer ${token}` },
15744
+ signal: AbortSignal.timeout(TIMEOUT_MS)
15745
+ });
15746
+ return res.ok;
15747
+ } catch {
15748
+ return false;
15749
+ }
15450
15750
  }
15451
15751
 
15452
15752
  // src/envGate.ts
@@ -17803,6 +18103,14 @@ function compareQueuePriority(a, b) {
17803
18103
  function orderQueue(runs) {
17804
18104
  return [...runs].sort(compareQueuePriority);
17805
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
+ }
17806
18114
 
17807
18115
  // src/watchdog.ts
17808
18116
  init_src();
@@ -18034,8 +18342,8 @@ async function headSha(repo) {
18034
18342
  function runText(run) {
18035
18343
  return [run.task_title ?? "", run.task_desc ?? ""].join(" ").trim();
18036
18344
  }
18037
- function predictForRun(model, run) {
18038
- return predictFiles(model, { zone: run.area, text: runText(run) });
18345
+ function predictForRun(model, run, fileExists) {
18346
+ return predictFiles(model, { zone: run.area, text: runText(run), ...fileExists ? { fileExists } : {} });
18039
18347
  }
18040
18348
  function short(files, n = 3) {
18041
18349
  return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
@@ -18050,17 +18358,17 @@ var SPINE_PREDICTION_WEIGHT = 0.7;
18050
18358
  function spineOfPrediction(spine, predicted) {
18051
18359
  return predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
18052
18360
  }
18053
- function decideConflict({ model, candidate, active }) {
18361
+ function decideConflict({ model, candidate, active, fileExists }) {
18054
18362
  if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
18055
18363
  if (!hasZoneModel(model)) return { kind: "unknown" };
18056
- const predicted = predictForRun(model, candidate);
18364
+ const predicted = predictForRun(model, candidate, fileExists);
18057
18365
  if (!predicted.length) return { kind: "unknown" };
18058
18366
  if (!active.length) return { kind: "clear" };
18059
18367
  const spine = new Set(model.spine);
18060
18368
  const candidateSpine = new Set(spineOfPrediction(spine, predicted));
18061
18369
  if (candidateSpine.size) {
18062
18370
  for (const w of active) {
18063
- const theirSpine = w.actual ? w.files.filter((f) => spine.has(f)) : spineOfPrediction(spine, predictForRun(model, w.run));
18371
+ const theirSpine = w.actual ? w.files.filter((f) => spine.has(f)) : spineOfPrediction(spine, predictForRun(model, w.run, fileExists));
18064
18372
  const shared = theirSpine.filter((f) => candidateSpine.has(f));
18065
18373
  if (!shared.length) continue;
18066
18374
  return {
@@ -18186,6 +18494,7 @@ var CONFLICT_RESOLVE_COOLDOWN_MS = Number(process.env.SWARM_CONFLICT_COOLDOWN_MS
18186
18494
  var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
18187
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";
18188
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";
18189
18498
  var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
18190
18499
  var REVIEW_CYCLE_LIMIT = Number(process.env.SWARM_REVIEW_CYCLE_LIMIT) || 3;
18191
18500
  var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
@@ -18282,11 +18591,56 @@ async function needsBaseRefreshNow(deps, run, stormActive, now) {
18282
18591
  if (last != null && now - last < CONFLICT_RESOLVE_COOLDOWN_MS) return false;
18283
18592
  return needsBaseRefresh(await deps.mergeQueue(run).catch(() => null));
18284
18593
  }
18594
+ async function dropMergeIntent(deps, run, reason) {
18595
+ if (!deps.clearMergeIntent) return false;
18596
+ if (!await deps.clearMergeIntent(run).catch(() => false)) return false;
18597
+ deps.db.updateRun(run.id, { merge_requested_at: null });
18598
+ deps.db.addEvent(run.id, "step", `\u043C\u0435\u0440\u0436 \u0441\u043D\u044F\u0442: ${reason}`);
18599
+ narrate(deps, `\u0421\u043D\u044F\u043B \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u0438\u0435 \u0441\u043C\u0435\u0440\u0436\u0438\u0442\u044C PR ${droneLabel(run)} \u2014 ${reason}.`);
18600
+ return true;
18601
+ }
18602
+ function mergeIntentDropReason(deps, run, conflicting) {
18603
+ if (needsFixIteration(run)) return "\u043F\u043E PR \u043F\u0440\u0438\u0448\u0451\u043B \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB \u2014 \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0430";
18604
+ if (conflicting && resolveEscalated(deps.db, run.id)) return "\u0440\u043E\u0439 \u043D\u0435 \u0441\u043C\u043E\u0433 \u0441\u0432\u0435\u0441\u0442\u0438 \u0432\u0435\u0442\u043A\u0443 \u0441 \u0431\u0430\u0437\u043E\u0439 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A";
18605
+ return null;
18606
+ }
18607
+ var MERGE_INTENT_FAIL_MARK = "\u043C\u0435\u0440\u0436 \u043F\u043E \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u0438\u044E \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B";
18608
+ var MERGE_INTENT_FAIL_NOTE_MS = 60 * 6e4;
18609
+ function noteMergeIntentFailure(deps, run, error, now) {
18610
+ const text = `${MERGE_INTENT_FAIL_MARK}: ${error} \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C`;
18611
+ const prev = deps.db.events(run.id).filter((e) => (e.text ?? "").startsWith(MERGE_INTENT_FAIL_MARK));
18612
+ const last = prev[prev.length - 1];
18613
+ if (last && last.text === text) {
18614
+ const at = Date.parse(last.at.replace(" ", "T") + "Z");
18615
+ if (!Number.isNaN(at) && now - at < MERGE_INTENT_FAIL_NOTE_MS) return;
18616
+ }
18617
+ deps.db.addEvent(run.id, "step", text);
18618
+ }
18285
18619
  function lastCiCheckMs(db, runId) {
18286
18620
  const evs = db.events(runId).filter((e) => e.kind === "ci_wake" || e.kind === "ci_checked");
18287
18621
  if (!evs.length) return null;
18288
18622
  return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
18289
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
+ }
18290
18644
  function reviewPhaseInputFor(deps, run, projectWantsVerify = true) {
18291
18645
  const events = deps.db.events(run.id);
18292
18646
  let ciRed = false;
@@ -18317,6 +18671,7 @@ function reviewPhaseInputFor(deps, run, projectWantsVerify = true) {
18317
18671
  return {
18318
18672
  reviewVerdict: run.review_verdict,
18319
18673
  ciRed,
18674
+ ciGreen: ciGreenForHead(deps.db, run, events),
18320
18675
  ciFailing,
18321
18676
  ciProgress,
18322
18677
  reviewerName: reviewer?.name ?? null,
@@ -19289,6 +19644,7 @@ async function handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap)
19289
19644
  return;
19290
19645
  }
19291
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);
19292
19648
  if (ci?.bucket === "denied") {
19293
19649
  const seen = ciDeniedStreak(deps.db, run.id) + 1;
19294
19650
  if (seen < CI_DENIED_LIMIT) {
@@ -19322,7 +19678,11 @@ async function handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap)
19322
19678
  return;
19323
19679
  }
19324
19680
  if (ci?.bucket === "fail") {
19325
- 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 });
19326
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`);
19327
19687
  }
19328
19688
  return;
@@ -19712,6 +20072,13 @@ async function zoneConflictFor(db, models, activeFiles, run, repo, repoFallback)
19712
20072
  if (!hasZoneModel(model)) return { kind: "unknown" };
19713
20073
  const holders = db.listRuns().filter((r) => holdsFiles(r));
19714
20074
  activeFiles.prune(holders.map((r) => r.id));
20075
+ const fileExists = (p) => {
20076
+ try {
20077
+ return statSync13(join34(repo, p), { throwIfNoEntry: false })?.isFile() ?? false;
20078
+ } catch {
20079
+ return false;
20080
+ }
20081
+ };
19715
20082
  const active = [];
19716
20083
  for (const r of holders) {
19717
20084
  if (r.id === run.id) continue;
@@ -19720,10 +20087,10 @@ async function zoneConflictFor(db, models, activeFiles, run, repo, repoFallback)
19720
20087
  const files = snapshot.length ? snapshot : await activeFiles.filesOf(r);
19721
20088
  const prUrl = r.status === "review" ? r.pr_url : null;
19722
20089
  active.push(
19723
- files.length ? { run: r, files, actual: true, prUrl } : { run: r, files: predictForRun(model, r).map((p) => p.path), actual: false, prUrl }
20090
+ files.length ? { run: r, files, actual: true, prUrl } : { run: r, files: heldByPrediction(predictForRun(model, r, fileExists)), actual: false, prUrl }
19724
20091
  );
19725
20092
  }
19726
- return decideConflict({ model, candidate: run, active });
20093
+ return decideConflict({ model, candidate: run, active, fileExists });
19727
20094
  }
19728
20095
  function activeDronesOnRepo(deps, repo, exceptRunId) {
19729
20096
  return deps.db.listRuns().filter(
@@ -20051,10 +20418,57 @@ async function failBeforeStart(deps, run, reason, opts = {}) {
20051
20418
  }
20052
20419
  return "failed";
20053
20420
  }
20054
- async function batchSpawnQueue(deps, droneCap) {
20055
- 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) {
20056
20427
  if (countActiveDrones(deps.db) >= droneCap) break;
20057
- 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);
20058
20472
  }
20059
20473
  }
20060
20474
  async function resumeWatchdogParked(deps, droneCap) {
@@ -20346,7 +20760,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
20346
20760
  const pausedForRelief = deps.db.listRuns("paused").filter((r) => isReliefPause(r.stop_reason));
20347
20761
  const recovering = pausedForRelief.length > 0 || state.overloadedAt != null && now - state.overloadedAt < cfg.rampWindowMs;
20348
20762
  if (!recovering) {
20349
- await batchSpawnQueue(deps, droneCap);
20763
+ await batchSpawnQueue(deps, droneCap, now);
20350
20764
  return;
20351
20765
  }
20352
20766
  if (!rampDue(state.lastRampAt ?? null, now, cfg)) return;
@@ -20836,7 +21250,7 @@ async function tickOnce(deps) {
20836
21250
  if (deps.resourcesOk) {
20837
21251
  await manageRelief(deps, now, droneCap, stormActive);
20838
21252
  } else if (!stormActive) {
20839
- await batchSpawnQueue(deps, droneCap);
21253
+ await batchSpawnQueue(deps, droneCap, now);
20840
21254
  }
20841
21255
  if (!stormActive) {
20842
21256
  await resumeWatchdogParked(deps, droneCap);
@@ -20853,7 +21267,7 @@ async function tickOnce(deps) {
20853
21267
  });
20854
21268
  deps.db.updateRun(run.id, { status: "done" });
20855
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");
20856
- 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`);
20857
21271
  continue;
20858
21272
  }
20859
21273
  if (run.reviews_run_id != null) {
@@ -20906,11 +21320,13 @@ async function tickOnce(deps) {
20906
21320
  const prUrl = run.pr_url;
20907
21321
  const st = await deps.prStatus(prUrl);
20908
21322
  run = syncBranchFromPr(deps.db, run, st.headBranch);
21323
+ if (st.conflicting) deps.returners?.delete(run.id);
20909
21324
  if (st.merged) {
20910
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.`);
20911
21326
  continue;
20912
21327
  }
20913
21328
  if (prLiveness(st) === "closed") {
21329
+ if (hasMergeIntent(run)) await dropMergeIntent(deps, run, "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430");
20914
21330
  await finalizeClosedPrReview(deps, run, "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E, \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442");
20915
21331
  narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 ${run.task_slug} \u0441\u043D\u0438\u043C\u0430\u0435\u0442\u0441\u044F \u0441 \u0440\u0435\u0432\u044C\u044E, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
20916
21332
  continue;
@@ -20940,7 +21356,37 @@ async function tickOnce(deps) {
20940
21356
  continue;
20941
21357
  }
20942
21358
  await retireOutdatedReviewers(deps, run);
20943
- const baseStale = !st.conflicting && await needsBaseRefreshNow(deps, run, stormActive, now);
21359
+ let intentBaseStale = false;
21360
+ if (hasMergeIntent(run)) {
21361
+ const drop = mergeIntentDropReason(deps, run, st.conflicting === true);
21362
+ if (drop) {
21363
+ if (await dropMergeIntent(deps, run, drop)) run = deps.db.getRun(run.id) ?? run;
21364
+ } else if (deps.mergeIntent && !stormActive) {
21365
+ const decision = await deps.mergeIntent(run).catch(() => null);
21366
+ if (decision?.action === "clear") {
21367
+ if (await dropMergeIntent(deps, run, decision.reason)) run = deps.db.getRun(run.id) ?? run;
21368
+ } else if (decision?.action === "refresh_base") {
21369
+ if (resolveEscalated(deps.db, run.id)) {
21370
+ if (await dropMergeIntent(deps, run, "\u0440\u043E\u0439 \u043D\u0435 \u0441\u043C\u043E\u0433 \u043F\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u0432\u0435\u0442\u043A\u0443 \u043A \u0441\u0432\u0435\u0436\u0435\u0439 \u0431\u0430\u0437\u0435 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A")) run = deps.db.getRun(run.id) ?? run;
21371
+ } else {
21372
+ intentBaseStale = true;
21373
+ }
21374
+ } else if (decision?.action === "merge" && deps.mergePr && !st.conflicting) {
21375
+ const res = await deps.mergePr(run, decision.headSha).catch((e) => ({ ok: false, error: describeError(e) }));
21376
+ if (res.ok) {
21377
+ deps.db.addEvent(run.id, "step", `\u0440\u043E\u0439 \u0432\u043B\u0438\u043B PR \u043F\u043E \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u043C\u0435\u0440\u0436\u0430: ${decision.reason} (TASWM-577)`);
21378
+ narrate(deps, `\u0412\u043B\u0438\u043B PR ${droneLabel(run)} \u043F\u043E \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u043C\u0435\u0440\u0436\u0430: ${decision.reason}.`);
21379
+ continue;
21380
+ }
21381
+ if ("headMoved" in res && res.headMoved) {
21382
+ deps.db.addEvent(run.id, "step", `\u043C\u0435\u0440\u0436 \u043F\u043E \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u0438\u044E \u043E\u0442\u043B\u043E\u0436\u0435\u043D: HEAD PR \u0441\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 ${(decision.headSha ?? "").slice(0, 7)} \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044E \u043D\u043E\u0432\u044B\u0439 \u043A\u043E\u043C\u043C\u0438\u0442 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C (TASWM-580)`);
21383
+ } else {
21384
+ noteMergeIntentFailure(deps, run, res.error, now);
21385
+ }
21386
+ }
21387
+ }
21388
+ }
21389
+ const baseStale = !st.conflicting && (intentBaseStale || await needsBaseRefreshNow(deps, run, stormActive, now));
20944
21390
  if ((st.conflicting || baseStale) && deps.resolveConflict && !stormActive) {
20945
21391
  if (deps.db.activeResolverFor(run.id)) continue;
20946
21392
  if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) {
@@ -20981,8 +21427,13 @@ async function tickOnce(deps) {
20981
21427
  } else if (!st.conflicting && deps.ci && deps.respawn && !stormActive) {
20982
21428
  await handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap);
20983
21429
  }
20984
- if (needsFixIteration(run) && !stormActive && !st.conflicting && !deps.db.activeResolverFor(run.id) && await wakeAuthorOnChanges(deps, run, now, droneCap)) {
20985
- 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 });
20986
21437
  }
20987
21438
  const reviewerGatesOk = !st.conflicting && !st.isDraft && !!deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt) && deps.db.reviewerCountFor(run.id) === 0 && // ГЕЙТ ПЕРВЫМ: живой/успешный ревьюер уже есть — ни капа, ни спавна не трогаем
20988
21439
  // …и НИ ОДНОГО живого/ждущего ревьюера ЛЮБОГО цикла (TASWM-299). reviewerCountFor
@@ -22458,7 +22909,10 @@ async function runDaemon(opts) {
22458
22909
  const ci = await prCiStatus(url);
22459
22910
  if (ci.bucket === "denied" || ci.bucket === "unavailable") return null;
22460
22911
  return { bucket: ci.bucket };
22461
- }
22912
+ },
22913
+ // TASWM-577: прогоны GitHub Actions на HEAD — только вливанию по намерению: рой
22914
+ // вливает сам и обязан дождаться, пока CI на этом коммите добежит целиком.
22915
+ actionsRuns: (url, sha) => prActionsRuns(url, sha)
22462
22916
  } : {}
22463
22917
  });
22464
22918
  const deps = {
@@ -22468,6 +22922,24 @@ async function runDaemon(opts) {
22468
22922
  worktreesDir,
22469
22923
  // TASWM-472: реконсайлу от очереди нужен ровно исход `base_stale` (см. needsBaseRefreshNow).
22470
22924
  ...SWARM_MERGE_QUEUE ? { mergeQueue: mergeParts.queue } : {},
22925
+ // TASWM-577: вливание ПО НАМЕРЕНИЮ — та же ось, тот же вход (mergeParts), но очередь из
22926
+ // одних отмеченных PR. Выключается тем же ключом, что и очередь: SWARM_MERGE_QUEUE=0 —
22927
+ // рой не вливает ничего, мержит человек кнопкой, как до этой задачи.
22928
+ ...SWARM_MERGE_QUEUE ? { mergeIntent: mergeParts.intent } : {},
22929
+ // Сам мерж и снятие намерения идут в ПРОД-ручки (`POST /swarm/merge` — GitHub App
22930
+ // воркспейса, squash; `DELETE /swarm/merge-request/:runId`), а адресуются они cuid'ом
22931
+ // прогона в проде. Он есть только у клиентского бэкенда: в sqlite-режиме прогонов в
22932
+ // проде нет вовсе, и рой там по-прежнему не мержит — мержит человек.
22933
+ ...db instanceof SwarmClient ? {
22934
+ mergePr: async (run, sha) => {
22935
+ const prodId = db.prodRunId(run.id);
22936
+ return prodId ? prodMergePr(prodId, sha) : { ok: false, error: "\u043F\u0440\u043E\u0433\u043E\u043D \u0435\u0449\u0451 \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B \u0434\u043E \u043F\u0440\u043E\u0434\u0430 \u2014 \u0432\u043B\u0438\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u043C" };
22937
+ },
22938
+ clearMergeIntent: async (run) => {
22939
+ const prodId = db.prodRunId(run.id);
22940
+ return prodId ? prodClearMergeIntent(prodId) : false;
22941
+ }
22942
+ } : {},
22471
22943
  narrate: narrator.narrate,
22472
22944
  control,
22473
22945
  spawn: async (run) => {