@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/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) {
@@ -1288,6 +1295,10 @@ function pathZoneCandidate(path) {
1288
1295
  if (parts[0] === "src" && parts.length >= 3 && !GENERIC_DIRS.has(parts[1])) return parts[1];
1289
1296
  return parts[0];
1290
1297
  }
1298
+ function dirOf(path) {
1299
+ const i = path.lastIndexOf("/");
1300
+ return i < 0 ? "" : path.slice(0, i);
1301
+ }
1291
1302
  function tokenize(text) {
1292
1303
  const out = [];
1293
1304
  for (const raw of text.split(/[^\p{L}\p{N}]+/u)) {
@@ -1521,13 +1532,61 @@ function similarTasks(model, text, limit = 5) {
1521
1532
  }
1522
1533
  return scored.sort((a, b) => b.score - a.score).slice(0, limit);
1523
1534
  }
1535
+ function historyFiles(model) {
1536
+ let idx = historyIndex.get(model);
1537
+ if (!idx) {
1538
+ const files = new Set(model.entries.flatMap((e) => e.files));
1539
+ const byBase = /* @__PURE__ */ new Map();
1540
+ for (const f of files) {
1541
+ const base3 = f.slice(f.lastIndexOf("/") + 1);
1542
+ byBase.set(base3, [...byBase.get(base3) ?? [], f]);
1543
+ }
1544
+ idx = { files, byBase };
1545
+ historyIndex.set(model, idx);
1546
+ }
1547
+ return idx;
1548
+ }
1549
+ function namedPaths(model, text, fileExists) {
1550
+ const { files, byBase } = historyFiles(model);
1551
+ const known = (p) => files.has(p) || (fileExists?.(p) ?? false);
1552
+ const raw = [...new Set((text.match(PATH_CANDIDATE) ?? []).map((c) => c.replace(/^\.?\/+/, "").replace(/\.+$/, "")))].filter((c) => c && !c.split("/").includes(".."));
1553
+ raw.sort((a, b) => b.split("/").length - a.split("/").length);
1554
+ const out = [];
1555
+ const dirs = /* @__PURE__ */ new Set();
1556
+ const take = (p) => {
1557
+ if (out.includes(p)) return;
1558
+ out.push(p);
1559
+ dirs.add(dirOf(p));
1560
+ };
1561
+ for (const c of raw) {
1562
+ if (known(c)) {
1563
+ take(c);
1564
+ continue;
1565
+ }
1566
+ const base3 = c.slice(c.lastIndexOf("/") + 1);
1567
+ let matches = (byBase.get(base3) ?? []).filter((f) => f === c || f.endsWith(`/${c}`));
1568
+ if (matches.length > 1) matches = matches.filter((f) => dirs.has(dirOf(f)));
1569
+ if (matches.length === 1) {
1570
+ take(matches[0]);
1571
+ continue;
1572
+ }
1573
+ if (!c.includes("/") && fileExists) {
1574
+ const sibling = [...dirs].map((d) => d ? `${d}/${c}` : c).filter(fileExists);
1575
+ if (sibling.length === 1) take(sibling[0]);
1576
+ }
1577
+ }
1578
+ return out;
1579
+ }
1524
1580
  function predictFiles(model, opts) {
1525
1581
  if (!hasZoneModel(model)) return [];
1526
1582
  const limit = opts.limit ?? 40;
1527
1583
  const best = /* @__PURE__ */ new Map();
1584
+ const named = opts.text ? namedPaths(model, opts.text, opts.fileExists) : [];
1585
+ 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 });
1586
+ const factor = named.length ? NAMED_BACKGROUND_FACTOR : 1;
1528
1587
  const put = (path, weight, why) => {
1529
1588
  const cur = best.get(path);
1530
- if (!cur || cur.weight < weight) best.set(path, { path, weight, why });
1589
+ if (!cur || cur.weight < weight * factor) best.set(path, { path, weight: weight * factor, why });
1531
1590
  };
1532
1591
  if (opts.text) {
1533
1592
  const sim = similarTasks(model, opts.text, opts.similar ?? 5);
@@ -1564,7 +1623,7 @@ function checkZoneAgreement(model, entries = model.entries) {
1564
1623
  const n = rows.length;
1565
1624
  return { checked: n, matched, compatible, rate: n ? matched / n : 0, looseRate: n ? (matched + compatible) / n : 0, rows };
1566
1625
  }
1567
- var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS;
1626
+ var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS, PATH_CANDIDATE, historyIndex, NAMED_BACKGROUND_FACTOR;
1568
1627
  var init_codeZones = __esm({
1569
1628
  "../swarm-core/src/codeZones.ts"() {
1570
1629
  "use strict";
@@ -1643,6 +1702,9 @@ var init_codeZones = __esm({
1643
1702
  "\u043F\u0435\u0440\u0444",
1644
1703
  "wip"
1645
1704
  ]);
1705
+ PATH_CANDIDATE = /[\w@.\-/]+\.[A-Za-z][A-Za-z0-9]{0,5}/g;
1706
+ historyIndex = /* @__PURE__ */ new WeakMap();
1707
+ NAMED_BACKGROUND_FACTOR = 0.1;
1646
1708
  }
1647
1709
  });
1648
1710
 
@@ -1661,7 +1723,7 @@ var init_effort = __esm({
1661
1723
  });
1662
1724
 
1663
1725
  // ../swarm-core/src/queuePlan.ts
1664
- 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;
1665
1727
  var init_queuePlan = __esm({
1666
1728
  "../swarm-core/src/queuePlan.ts"() {
1667
1729
  "use strict";
@@ -1672,7 +1734,6 @@ var init_queuePlan = __esm({
1672
1734
  MIN_MS = 6e4;
1673
1735
  QUEUE_NOMINAL_RUN_MS = 25 * MIN_MS;
1674
1736
  QUEUE_MIN_TAIL_MS = 3 * MIN_MS;
1675
- QUEUE_REVIEW_TAIL_MS = 10 * MIN_MS;
1676
1737
  }
1677
1738
  });
1678
1739
 
@@ -3431,7 +3492,7 @@ var init_package = __esm({
3431
3492
  "package.json"() {
3432
3493
  package_default = {
3433
3494
  name: "@taskless-app/swarm",
3434
- version: "0.6.49",
3495
+ version: "0.6.51",
3435
3496
  type: "module",
3436
3497
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
3437
3498
  keywords: [
@@ -3482,7 +3543,7 @@ var init_package = __esm({
3482
3543
  execa: "^10.0.0",
3483
3544
  zod: "3.25.76"
3484
3545
  },
3485
- gitHead: "27600667531d4f65761c74793e8c4ee4859dc8ae"
3546
+ gitHead: "24aad254ba4f3f71904310b73435e647a08f22f5"
3486
3547
  };
3487
3548
  }
3488
3549
  });
@@ -4817,6 +4878,10 @@ function rowToRun(row) {
4817
4878
  // вычисляемая фаза ревью (TASWM-230)
4818
4879
  review_cycle: row.review_cycle ?? 0,
4819
4880
  // старые прогоны без колонки → 0
4881
+ // TASWM-577: намерение смержить (человек отметил «пора влить»). В sqlite-бэкенде оно
4882
+ // живёт только здесь — ставить его некому, кроме самого демона, зато читает его
4883
+ // реконсайл, и колонка нужна, чтобы отметка пережила рестарт.
4884
+ merge_requested_at: row.merge_requested_at ?? null,
4820
4885
  task_title: row.task_title ?? null,
4821
4886
  task_desc: row.task_desc ?? null,
4822
4887
  // TASWM-556: когда заведена сама задача — по ней проверка дубля отличает свой
@@ -4892,6 +4957,7 @@ CREATE TABLE IF NOT EXISTS runs (
4892
4957
  verifies_run_id INTEGER,
4893
4958
  verify_verdict TEXT,
4894
4959
  review_cycle INTEGER NOT NULL DEFAULT 0,
4960
+ merge_requested_at TEXT,
4895
4961
  task_title TEXT,
4896
4962
  task_desc TEXT,
4897
4963
  task_created_at TEXT,
@@ -4986,6 +5052,8 @@ CREATE TABLE IF NOT EXISTS imports (
4986
5052
  "ALTER TABLE runs ADD COLUMN verify_verdict TEXT",
4987
5053
  // Авто-ревью v2 (TASWM-162): номер цикла ревью — пере-ревью после фикс-итерации.
4988
5054
  "ALTER TABLE runs ADD COLUMN review_cycle INTEGER NOT NULL DEFAULT 0",
5055
+ // TASWM-577: намерение смержить — по нему рой вливает PR сам, по очереди мержа.
5056
+ "ALTER TABLE runs ADD COLUMN merge_requested_at TEXT",
4989
5057
  // Вычисляемая фаза ревью (TASWM-230): человеко-фраза «чего ждёт PR», её считает демон.
4990
5058
  "ALTER TABLE runs ADD COLUMN review_phase TEXT",
4991
5059
  // Приоритетная очередь (TASWM-269): явный приоритет прогона, выше — раньше из queued.
@@ -7317,6 +7385,12 @@ function depsFailureRetriable(code) {
7317
7385
  function runRef(runKey) {
7318
7386
  return { __runRef: runKey };
7319
7387
  }
7388
+ function isoOrNull(v) {
7389
+ if (v == null) return null;
7390
+ const raw = v instanceof Date ? v.toISOString() : typeof v === "string" ? v : "";
7391
+ const at = Date.parse(raw);
7392
+ return Number.isNaN(at) ? null : new Date(at).toISOString();
7393
+ }
7320
7394
  function isRunRef(v) {
7321
7395
  return typeof v === "object" && v !== null && "__runRef" in v;
7322
7396
  }
@@ -7784,6 +7858,17 @@ var SwarmClient = class _SwarmClient {
7784
7858
  runKeyOf(id) {
7785
7859
  return this.runs.get(id)?.runKey ?? null;
7786
7860
  }
7861
+ /**
7862
+ * Cuid прогона В ПРОДЕ — им адресуются прод-РУЧКИ, а не MCP-тулы (TASWM-577: демон
7863
+ * вливает PR тем же `POST /swarm/merge`, что и кнопка, и снимает намерение тем же
7864
+ * `DELETE /swarm/merge-request/:runId`). null — прогон до прода ещё не долетел
7865
+ * (создан этим демоном, флаш не прошёл): адресовать его там нечем, и вызывающий
7866
+ * обязан прочитать это как «сейчас нельзя», а не как ошибку.
7867
+ */
7868
+ prodRunId(id) {
7869
+ const key = this.runs.get(id)?.runKey;
7870
+ return key ? this.prodIdFor(key) ?? null : null;
7871
+ }
7787
7872
  runByKey(key) {
7788
7873
  const direct = [...this.runs.values()].find((r) => r.runKey === key);
7789
7874
  if (direct) return this.mat(direct);
@@ -9330,6 +9415,9 @@ var SwarmClient = class _SwarmClient {
9330
9415
  verify_verdict: null,
9331
9416
  review_phase: null,
9332
9417
  // вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
9418
+ // TASWM-577: намерение смержить — прод-поле (его ставит человек ручкой
9419
+ // /swarm/merge-request), заполняется ниже из ответа.
9420
+ merge_requested_at: null,
9333
9421
  // Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
9334
9422
  // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
9335
9423
  // в отличие от respawns, — авто-ревью v2, TASWM-162.
@@ -9365,6 +9453,7 @@ var SwarmClient = class _SwarmClient {
9365
9453
  local.task_title = pr.taskTitle ?? local.task_title;
9366
9454
  local.task_desc = pr.taskDesc ?? local.task_desc;
9367
9455
  local.task_created_at = pr.taskCreatedAt ?? local.task_created_at;
9456
+ if ("mergeRequestedAt" in pr) local.merge_requested_at = isoOrNull(pr.mergeRequestedAt);
9368
9457
  local.name = pr.droneName ?? local.name;
9369
9458
  local.kind = pr.role ?? local.kind;
9370
9459
  const heroProd = pr.heroId ?? null;
@@ -10397,22 +10486,10 @@ function mergeQueueCandidates(runs, repo, repoFallback) {
10397
10486
  }
10398
10487
  var BASE_TTL_MS = 2e4;
10399
10488
  function makeMergeQueueResolver(deps) {
10400
- const memo = /* @__PURE__ */ new Map();
10489
+ const read = deps.headState ?? makeHeadStateReader(deps);
10401
10490
  const evaluate = async (e, prUrl, ready) => {
10402
- const now = Date.now();
10403
- let cached = memo.get(prUrl);
10404
- const ttl = deps.ttlMs ?? BASE_TTL_MS;
10405
- if (!cached || now - cached.at >= ttl) {
10406
- const base3 = await deps.baseSync(prUrl).catch(() => null);
10407
- let checks = "none";
10408
- if (base3?.behindBy === 0 && deps.checks) {
10409
- const ci = await deps.checks(prUrl).catch(() => null);
10410
- checks = ci ? ci.bucket : null;
10411
- }
10412
- cached = { at: now, base: base3, checks };
10413
- memo.set(prUrl, cached);
10414
- }
10415
- return computeMergeQueue({ entry: e, ready, behindBy: cached.base ? cached.base.behindBy : null, checks: cached.checks });
10491
+ const st = await read(prUrl);
10492
+ return computeMergeQueue({ entry: e, ready, behindBy: st.behindBy, checks: st.checks });
10416
10493
  };
10417
10494
  return async (run) => {
10418
10495
  if (!deps.baseSync || !run.pr_url) return null;
@@ -10447,6 +10524,124 @@ function makeMergeQueueResolver(deps) {
10447
10524
  function entryOf(r) {
10448
10525
  return { runId: r.id, prNumber: prNumberOf(r.pr_url), slug: r.task_slug };
10449
10526
  }
10527
+ function makeHeadStateReader(deps) {
10528
+ const memo = /* @__PURE__ */ new Map();
10529
+ return async (prUrl) => {
10530
+ const now = Date.now();
10531
+ const ttl = deps.ttlMs ?? BASE_TTL_MS;
10532
+ const cached = memo.get(prUrl);
10533
+ if (cached && now - cached.at < ttl) return { behindBy: cached.behindBy, checks: cached.checks, headSha: cached.headSha };
10534
+ const base3 = deps.baseSync ? await deps.baseSync(prUrl).catch(() => null) : null;
10535
+ let checks = "none";
10536
+ if (base3?.behindBy === 0 && deps.checks) {
10537
+ const ci = await deps.checks(prUrl).catch(() => null);
10538
+ checks = ci ? ci.bucket : null;
10539
+ }
10540
+ const fresh = { behindBy: base3 ? base3.behindBy : null, checks, headSha: base3?.headSha ?? null };
10541
+ memo.set(prUrl, { at: now, ...fresh });
10542
+ return fresh;
10543
+ };
10544
+ }
10545
+ function hasMergeIntent(r) {
10546
+ return !!(r.merge_requested_at ?? "").trim();
10547
+ }
10548
+ var NO_CI_GRACE_MS = 10 * 6e4;
10549
+ var shortSha = (sha) => sha.slice(0, 7);
10550
+ function makeMergeIntentResolver(deps) {
10551
+ const read = deps.headState ?? makeHeadStateReader(deps);
10552
+ const firstSeen = /* @__PURE__ */ new Map();
10553
+ const ciSettled = async (prUrl, st, now) => {
10554
+ const sha = st.headSha;
10555
+ if (!sha) {
10556
+ 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" };
10557
+ }
10558
+ if (!deps.actionsRuns) {
10559
+ 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" };
10560
+ }
10561
+ const runs = await deps.actionsRuns(prUrl, sha).catch(() => null);
10562
+ if (!runs) {
10563
+ 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` };
10564
+ }
10565
+ if (runs.failed.length > 0) {
10566
+ 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` };
10567
+ }
10568
+ if (runs.active.length > 0) {
10569
+ return {
10570
+ action: "wait",
10571
+ code: "checks_pending",
10572
+ 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`
10573
+ };
10574
+ }
10575
+ if (st.checks === "none") {
10576
+ if (runs.total > 0) {
10577
+ 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` };
10578
+ }
10579
+ const seen = firstSeen.get(prUrl);
10580
+ const waited = seen && seen.sha === sha ? now - seen.at : 0;
10581
+ const graceMin = NO_CI_GRACE_MS / 6e4;
10582
+ if (waited < NO_CI_GRACE_MS) {
10583
+ return {
10584
+ action: "wait",
10585
+ code: "checks_pending",
10586
+ 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)})`
10587
+ };
10588
+ }
10589
+ 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` };
10590
+ }
10591
+ 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` };
10592
+ };
10593
+ const confirmLive = async (prUrl, sha, verdict) => {
10594
+ if (!sha) {
10595
+ 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" };
10596
+ }
10597
+ const live = deps.baseSync ? await deps.baseSync(prUrl).catch(() => null) : null;
10598
+ if (!live || !live.headSha) {
10599
+ 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" };
10600
+ }
10601
+ if (verdict.action === "merge" && live.behindBy > 0) {
10602
+ return {
10603
+ action: "refresh_base",
10604
+ code: "base_stale",
10605
+ 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`
10606
+ };
10607
+ }
10608
+ if (live.headSha !== sha) {
10609
+ 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` };
10610
+ }
10611
+ return verdict;
10612
+ };
10613
+ return async (run) => {
10614
+ if (!deps.baseSync || !run.pr_url || !hasMergeIntent(run)) return null;
10615
+ const num = prNumberOf(run.pr_url);
10616
+ if (num == null) return null;
10617
+ const marked = mergeQueueCandidates(deps.listRuns(), effectiveRepoOf(run.repo, deps.repoFallback), deps.repoFallback).filter(hasMergeIntent);
10618
+ const mine = marked.find((c) => prNumberOf(c.pr_url) === num);
10619
+ if (!mine) return null;
10620
+ const queue = orderMergeQueue(marked.map(entryOf));
10621
+ const entry = entryOf(mine);
10622
+ const position = queue.findIndex((e) => e.runId === entry.runId) + 1;
10623
+ const head = queue[0];
10624
+ if (head.runId !== entry.runId) {
10625
+ return {
10626
+ action: "wait",
10627
+ code: "queue_wait",
10628
+ 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}`,
10629
+ position,
10630
+ size: queue.length
10631
+ };
10632
+ }
10633
+ const st = await read(mine.pr_url);
10634
+ const now = deps.now ? deps.now() : Date.now();
10635
+ if (st.headSha) {
10636
+ const seen = firstSeen.get(mine.pr_url);
10637
+ if (!seen || seen.sha !== st.headSha) firstSeen.set(mine.pr_url, { sha: st.headSha, at: now });
10638
+ }
10639
+ const d = computeMergeQueue({ entry, ready: queue, behindBy: st.behindBy, checks: st.checks });
10640
+ 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 };
10641
+ const final = verdict.action === "merge" || verdict.action === "clear" ? await confirmLive(mine.pr_url, st.headSha, verdict) : verdict;
10642
+ return { ...final, position, size: queue.length, ...st.headSha ? { headSha: st.headSha } : {} };
10643
+ };
10644
+ }
10450
10645
 
10451
10646
  // src/mergeGate.ts
10452
10647
  function findRunByPr(runs, ref, scope = {}) {
@@ -10475,9 +10670,15 @@ function makeMergeGateParts(deps) {
10475
10670
  });
10476
10671
  };
10477
10672
  const repoOfRun = deps.repoFullNameOf ? (r) => deps.repoFullNameOf(effectiveRepoOf(r.repo, deps.repoFallback)) : void 0;
10673
+ const headState = makeHeadStateReader({
10674
+ ...deps.baseSync ? { baseSync: deps.baseSync } : {},
10675
+ ...deps.checks ? { checks: deps.checks } : {},
10676
+ ...deps.ttlMs != null ? { ttlMs: deps.ttlMs } : {}
10677
+ });
10478
10678
  const queueFor = makeMergeQueueResolver({
10479
10679
  listRuns: deps.listRuns,
10480
10680
  gateAllows: async (r) => (await gateFor(r)).allowed,
10681
+ headState,
10481
10682
  ...deps.repoFallback ? { repoFallback: deps.repoFallback } : {},
10482
10683
  ...deps.baseSync ? { baseSync: deps.baseSync } : {},
10483
10684
  ...deps.checks ? { checks: deps.checks } : {},
@@ -10503,7 +10704,16 @@ function makeMergeGateParts(deps) {
10503
10704
  }
10504
10705
  };
10505
10706
  };
10506
- return { gate, queue: queueFor };
10707
+ const intentFor = makeMergeIntentResolver({
10708
+ listRuns: deps.listRuns,
10709
+ headState,
10710
+ ...deps.repoFallback ? { repoFallback: deps.repoFallback } : {},
10711
+ ...deps.baseSync ? { baseSync: deps.baseSync } : {},
10712
+ ...deps.checks ? { checks: deps.checks } : {},
10713
+ ...deps.actionsRuns ? { actionsRuns: deps.actionsRuns } : {},
10714
+ ...deps.ttlMs != null ? { ttlMs: deps.ttlMs } : {}
10715
+ });
10716
+ return { gate, queue: queueFor, intent: intentFor };
10507
10717
  }
10508
10718
 
10509
10719
  // src/web.ts