@taskless-app/swarm 0.6.11 → 0.6.13

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 +1220 -356
  2. package/dist/overmind.js +1153 -304
  3. package/package.json +2 -2
package/dist/overmind.js CHANGED
@@ -13,7 +13,7 @@ function isMilestoneKind(kind) {
13
13
  return MILESTONE_KIND_SET.has(kind);
14
14
  }
15
15
  function toRunKind(role) {
16
- return role === "hero" || role === "reviewer" ? role : "drone";
16
+ return role === "hero" || role === "reviewer" || role === "verifier" ? role : "drone";
17
17
  }
18
18
  var ACTIVE_DRONE_STATUSES, EVENT_KINDS, MILESTONE_KINDS, MILESTONE_KIND_SET;
19
19
  var init_types = __esm({
@@ -335,6 +335,53 @@ var init_reviewPhase = __esm({
335
335
  }
336
336
  });
337
337
 
338
+ // ../swarm-core/src/verifyVerdict.ts
339
+ function isVerifyVerdict(v) {
340
+ return typeof v === "string" && VERIFY_VERDICT_SET.has(v);
341
+ }
342
+ function toVerifyVerdict(v) {
343
+ return isVerifyVerdict(v) ? v : null;
344
+ }
345
+ function isNegativeVerifyVerdict(v) {
346
+ return v === "not_reproduced" || v === "found_issues";
347
+ }
348
+ function verifyVerdictLabel(v) {
349
+ switch (v) {
350
+ case "confirmed":
351
+ return "\u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u042E";
352
+ case "not_reproduced":
353
+ return "\u041D\u0415 \u0412\u041E\u0421\u041F\u0420\u041E\u0418\u0417\u0412\u041E\u0414\u0418\u0422\u0421\u042F";
354
+ case "found_issues":
355
+ return "\u041D\u0410\u0428\u0401\u041B";
356
+ }
357
+ }
358
+ function parseVerifyVerdictComment(text) {
359
+ const line = (text ?? "").split("\n").find((l) => l.trim().length > 0);
360
+ if (!line) return null;
361
+ let best = null;
362
+ for (const [verdict, marks] of VERDICT_MARKS) {
363
+ for (const mark of marks) {
364
+ const at = line.indexOf(mark);
365
+ if (at < 0) continue;
366
+ if (!best || at < best.at) best = { verdict, at };
367
+ }
368
+ }
369
+ return best?.verdict ?? null;
370
+ }
371
+ var VERIFY_VERDICTS, VERIFY_VERDICT_SET, VERDICT_MARKS;
372
+ var init_verifyVerdict = __esm({
373
+ "../swarm-core/src/verifyVerdict.ts"() {
374
+ "use strict";
375
+ VERIFY_VERDICTS = ["confirmed", "not_reproduced", "found_issues"];
376
+ VERIFY_VERDICT_SET = new Set(VERIFY_VERDICTS);
377
+ VERDICT_MARKS = [
378
+ ["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"]],
379
+ ["found_issues", ["\u041D\u0410\u0428\u0401\u041B", "\u041D\u0410\u0428\u0415\u041B", "FOUND"]],
380
+ ["confirmed", ["\u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u042E", "CONFIRMED"]]
381
+ ];
382
+ }
383
+ });
384
+
338
385
  // ../swarm-core/src/mergeGate.ts
339
386
  function computeMergeGate(input) {
340
387
  if (!input.hasRun) {
@@ -343,21 +390,38 @@ function computeMergeGate(input) {
343
390
  if (!input.autoReview) {
344
391
  return { allowed: true, code: "auto_review_off", reason: "\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u0440\u0435\u0432\u044C\u044E \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F" };
345
392
  }
346
- if (input.reviewVerdict === "approve") {
347
- return { allowed: true, code: "approved", reason: "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u0430\u043B approve \u2014 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043C\u043E\u0436\u043D\u043E" };
348
- }
349
393
  if (input.reviewVerdict === "changes") {
350
394
  return { allowed: false, code: "changes_requested", reason: "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u043B \u043F\u0440\u0430\u0432\u043A\u0438 (REQUEST CHANGES) \u2014 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F, \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0438 \u0438 \u043D\u043E\u0432\u043E\u0433\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430" };
351
395
  }
352
- return {
353
- allowed: false,
354
- code: "no_verdict",
355
- reason: "\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u0440\u0435\u0432\u044C\u044E, \u0430 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043F\u043E \u044D\u0442\u043E\u043C\u0443 PR \u0435\u0449\u0451 \u043D\u0435\u0442 \u2014 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F (TASWM-355)"
356
- };
396
+ if (input.reviewVerdict !== "approve") {
397
+ return {
398
+ allowed: false,
399
+ code: "no_verdict",
400
+ reason: "\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u0430\u0432\u0442\u043E\u0440\u0435\u0432\u044C\u044E, \u0430 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043F\u043E \u044D\u0442\u043E\u043C\u0443 PR \u0435\u0449\u0451 \u043D\u0435\u0442 \u2014 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F (TASWM-355)"
401
+ };
402
+ }
403
+ if (input.autoVerify) {
404
+ if (input.verifyVerdict == null) {
405
+ return {
406
+ allowed: false,
407
+ code: "no_verify_verdict",
408
+ reason: "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u0430\u043B approve, \u043D\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0433\u043E \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440\u0430 \u043F\u043E \u044D\u0442\u043E\u043C\u0443 PR \u0435\u0449\u0451 \u043D\u0435\u0442 \u2014 \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F \u043F\u0440\u043E\u0448\u0435\u0434\u0448\u0435\u0439 \u043F\u0440\u0438\u0451\u043C\u043A\u0443 (TASWM-344)"
409
+ };
410
+ }
411
+ if (isNegativeVerifyVerdict(input.verifyVerdict)) {
412
+ return {
413
+ allowed: false,
414
+ code: "verify_negative",
415
+ reason: `\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0432\u044B\u043D\u0435\u0441 \xAB${verifyVerdictLabel(input.verifyVerdict)}\xBB \u2014 \u0437\u0430\u044F\u0432\u043B\u0435\u043D\u043D\u043E\u0435 \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B\u043E\u0441\u044C, \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F (TASWM-344)`
416
+ };
417
+ }
418
+ }
419
+ return { allowed: true, code: "approved", reason: "\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u0430\u043B approve \u2014 \u043C\u0435\u0440\u0436\u0438\u0442\u044C \u043C\u043E\u0436\u043D\u043E" };
357
420
  }
358
421
  var init_mergeGate = __esm({
359
422
  "../swarm-core/src/mergeGate.ts"() {
360
423
  "use strict";
424
+ init_verifyVerdict();
361
425
  }
362
426
  });
363
427
 
@@ -1383,6 +1447,7 @@ var init_src = __esm({
1383
1447
  init_timeline();
1384
1448
  init_reviewPhase();
1385
1449
  init_mergeGate();
1450
+ init_verifyVerdict();
1386
1451
  init_status();
1387
1452
  init_deps();
1388
1453
  init_time();
@@ -1532,11 +1597,18 @@ var init_swarmFiles = __esm({
1532
1597
  });
1533
1598
 
1534
1599
  // src/pricing.ts
1535
- function costFromUsage(usage, model, table = MODEL_PRICING) {
1536
- const pricing = model ? table[model] : void 0;
1537
- if (!pricing) {
1600
+ function pricingAt(pricing, at) {
1601
+ const intro = pricing.intro;
1602
+ if (!intro) return pricing;
1603
+ const day = at.toISOString().slice(0, 10);
1604
+ return day <= intro.until ? { inputPerMTok: intro.inputPerMTok, outputPerMTok: intro.outputPerMTok } : pricing;
1605
+ }
1606
+ function costFromUsage(usage, model, table = MODEL_PRICING, at = /* @__PURE__ */ new Date()) {
1607
+ const entry = model ? table[model] : void 0;
1608
+ if (!entry) {
1538
1609
  return { costUsd: 0, warning: `\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u0430\u0439\u0441\u0438\u043D\u0433\u0430: "${model ?? ""}" \u2014 cost \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D` };
1539
1610
  }
1611
+ const pricing = pricingAt(entry, at);
1540
1612
  const input = usage.input_tokens ?? 0;
1541
1613
  const output = usage.output_tokens ?? 0;
1542
1614
  const cacheWrite = usage.cache_creation_input_tokens ?? 0;
@@ -1549,10 +1621,12 @@ var init_pricing = __esm({
1549
1621
  "src/pricing.ts"() {
1550
1622
  "use strict";
1551
1623
  MODEL_PRICING = {
1552
- // Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
1553
- // Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
1554
- // ветвление по времени внутри расчёта стоимости.
1555
- "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
1624
+ // Sonnet 5: вводные 2/10 по 31.08.2026 включительно, дальше базовые 3/15.
1625
+ "claude-sonnet-5": {
1626
+ inputPerMTok: 3,
1627
+ outputPerMTok: 15,
1628
+ intro: { inputPerMTok: 2, outputPerMTok: 10, until: "2026-08-31" }
1629
+ },
1556
1630
  "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
1557
1631
  // Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
1558
1632
  "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
@@ -1991,6 +2065,7 @@ __export(config_exports, {
1991
2065
  SWARM_ANOMALY_RESOURCE_GATE_STALL_MS: () => SWARM_ANOMALY_RESOURCE_GATE_STALL_MS,
1992
2066
  SWARM_ANOMALY_RUN_DOWN_WINDOW_MS: () => SWARM_ANOMALY_RUN_DOWN_WINDOW_MS,
1993
2067
  SWARM_AUTO_REVIEW: () => SWARM_AUTO_REVIEW,
2068
+ SWARM_AUTO_VERIFY: () => SWARM_AUTO_VERIFY,
1994
2069
  SWARM_BACKEND: () => SWARM_BACKEND,
1995
2070
  SWARM_CLAIM_INTERVAL_MS: () => SWARM_CLAIM_INTERVAL_MS,
1996
2071
  SWARM_CPU_SANITY_BUSY: () => SWARM_CPU_SANITY_BUSY,
@@ -2121,7 +2196,7 @@ function reviewCutoffAt() {
2121
2196
  }
2122
2197
  return at;
2123
2198
  }
2124
- var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_WATCHDOG_STALL_MS;
2199
+ var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_WATCHDOG_STALL_MS;
2125
2200
  var init_config = __esm({
2126
2201
  "src/config.ts"() {
2127
2202
  "use strict";
@@ -2192,6 +2267,7 @@ var init_config = __esm({
2192
2267
  TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
2193
2268
  SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
2194
2269
  SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
2270
+ SWARM_AUTO_VERIFY = process.env.SWARM_AUTO_VERIFY !== "0";
2195
2271
  REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join5(SWARM_HOME, "review-cutoff.json");
2196
2272
  SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
2197
2273
  SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
@@ -2631,6 +2707,105 @@ var init_heartbeat = __esm({
2631
2707
  }
2632
2708
  });
2633
2709
 
2710
+ // src/agentToken.ts
2711
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
2712
+ import { join as join8 } from "path";
2713
+ function loadAgentToken() {
2714
+ try {
2715
+ const s = JSON.parse(readFileSync7(STORE_PATH, "utf8"));
2716
+ return s.token ? s : null;
2717
+ } catch {
2718
+ return null;
2719
+ }
2720
+ }
2721
+ async function probeAgentToken(args, fetchFn = fetch) {
2722
+ try {
2723
+ const res = await fetchFn(`${base(args.api)}/api/auth/me`, {
2724
+ headers: { Authorization: `Bearer ${args.token}` },
2725
+ // Оборвётся по таймауту — попадём в catch ниже, то есть в "unreachable": это ровно
2726
+ // то, чем висящая сеть и является. Соседи по CLI (daemonLink) живут так же.
2727
+ signal: AbortSignal.timeout(args.timeoutMs ?? TOKEN_PROBE_TIMEOUT_MS)
2728
+ });
2729
+ if (res.status === 401 || res.status === 403) return "revoked";
2730
+ return res.ok ? "ok" : "unreachable";
2731
+ } catch {
2732
+ return "unreachable";
2733
+ }
2734
+ }
2735
+ function isRevokedTokenError(e) {
2736
+ if (e && typeof e === "object" && e.code === 401) return true;
2737
+ const text = e instanceof Error ? e.message : typeof e === "string" ? e : "";
2738
+ return /invalid_token|(?:http|status|code)[ =:]*401\b|\(401\)|401 unauthorized/i.test(text);
2739
+ }
2740
+ function revokedTokenLine() {
2741
+ return "[taskless] \u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u043E\u0437\u0432\u0430\u043D (401 invalid_token) \u2014 \u0440\u043E\u0439 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442 \u0440\u0430\u0431\u043E\u0442\u0443. \u041F\u043E\u0447\u0438\u043D\u0438 \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: swarm login";
2742
+ }
2743
+ function revokedTokenComplaint() {
2744
+ return [
2745
+ "",
2746
+ "\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E",
2747
+ "\u2502 \u0422\u041E\u041A\u0415\u041D \u0420\u041E\u042F \u041D\u0415 \u041F\u0420\u0418\u041D\u042F\u0422 (401). \u0414\u043E\u0441\u0442\u0443\u043F \u043A Taskless \u0440\u0430\u0437\u043E\u0440\u0432\u0430\u043D. \u2502",
2748
+ "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F",
2749
+ "\u041F\u0440\u0438\u0447\u0438\u043D\u0430 \u2014 \u043E\u0434\u043D\u0430 \u0438\u0437 \u0442\u0440\u0451\u0445, \u0432\u0441\u0435 \u0442\u0440\u0438 \u0440\u0435\u0448\u0430\u044E\u0442\u0441\u044F \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0435\u043C:",
2750
+ " \u2022 \xAB\u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u044D\u0442\u043E\u0442 \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\xBB \u0432 \u043F\u0430\u0440\u043A\u0435 \u043C\u0430\u0448\u0438\u043D \u2014 \u043E\u0442\u043E\u0437\u0432\u0430\u043D \u0442\u043E\u043A\u0435\u043D \u042D\u0422\u041E\u0419 \u043C\u0430\u0448\u0438\u043D\u044B;",
2751
+ " \u2022 \xAB\u0441\u0431\u0440\u043E\u0441\u0438\u0442\u044C \u0434\u043E\u0441\u0442\u0443\u043F \u0440\u043E\u044F\xBB \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 (\u041F\u0440\u043E\u0444\u0438\u043B\u044C \u2192 \u0420\u043E\u0439) \u2014 \u043E\u0442\u043E\u0437\u0432\u0430\u043D\u044B \u0432\u0441\u0435;",
2752
+ " \u2022 \u0440\u043E\u0439 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D \u043C\u0430\u0441\u0442\u0435\u0440-\u0442\u0443\u043C\u0431\u043B\u0435\u0440\u043E\u043C \u2014 \u0442\u043E\u043A\u0435\u043D \u0430\u0433\u0435\u043D\u0442\u0430 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u0451\u0442 \u0434\u0435\u0439\u0441\u0442\u0432\u043E\u0432\u0430\u0442\u044C.",
2753
+ "",
2754
+ "\u0412\u0445\u043E\u0434 \u043D\u0430 \u0414\u0420\u0423\u0413\u041E\u0419 \u043C\u0430\u0448\u0438\u043D\u0435 \u044D\u0442\u043E\u0442 \u0442\u043E\u043A\u0435\u043D \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0433\u0430\u0441\u0438\u0442 (TASWM-324): \u0443 \u043A\u0430\u0436\u0434\u043E\u0439",
2755
+ "\u043C\u0430\u0448\u0438\u043D\u044B \u0441\u0432\u043E\u0439 \u0442\u043E\u043A\u0435\u043D, \u043F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0439 \u043A \u0435\u0451 hostId.",
2756
+ "",
2757
+ "\u0414\u0435\u043C\u043E\u043D \u041D\u0415 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432\u0441\u043B\u0435\u043F\u0443\u044E: \u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442, \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043D\u0435 \u0431\u0435\u0440\u0443\u0442\u0441\u044F \u0438 \u043E\u0442\u0447\u0451\u0442\u044B",
2758
+ "\u043D\u0435 \u043F\u0438\u0448\u0443\u0442\u0441\u044F. \u041F\u043E\u0447\u0438\u043D\u0438 \u0432\u0445\u043E\u0434\u043E\u043C \u0437\u0430\u043D\u043E\u0432\u043E: swarm login",
2759
+ "\u041F\u0440\u043E\u0431\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435 \u043F\u0440\u043E\u0445\u043E\u0434\u0438\u0442, \u0430 \u0432\u043E\u0439\u0442\u0438 \u043C\u0435\u0448\u0430\u0435\u0442 \u0441\u0442\u0430\u0440\u044B\u0439 \u0444\u0430\u0439\u043B: swarm login --force",
2760
+ ""
2761
+ ].join("\n");
2762
+ }
2763
+ var STORE_PATH, base, TOKEN_PROBE_TIMEOUT_MS;
2764
+ var init_agentToken = __esm({
2765
+ "src/agentToken.ts"() {
2766
+ "use strict";
2767
+ init_config();
2768
+ STORE_PATH = join8(SWARM_HOME, "agent.json");
2769
+ base = (api) => api.replace(/\/$/, "");
2770
+ TOKEN_PROBE_TIMEOUT_MS = 8e3;
2771
+ }
2772
+ });
2773
+
2774
+ // src/linkFailure.ts
2775
+ function describeError(e, maxDepth = 4) {
2776
+ const parts = [];
2777
+ const seen = /* @__PURE__ */ new Set();
2778
+ let cur = e;
2779
+ for (let depth = 0; depth < maxDepth && cur != null && !seen.has(cur); depth++) {
2780
+ seen.add(cur);
2781
+ const msg = (cur instanceof Error ? cur.message : typeof cur === "string" ? cur : String(cur)).trim();
2782
+ const code = cur.code;
2783
+ const codeText = code === void 0 || code === null ? "" : String(code);
2784
+ const label = codeText && !msg.includes(codeText) ? `${msg} [code=${codeText}]` : msg;
2785
+ if (label && !parts.includes(label)) parts.push(label);
2786
+ cur = cur.cause;
2787
+ }
2788
+ return parts.join(" \u2190 ") || String(e);
2789
+ }
2790
+ function explainLinkFailure(e) {
2791
+ const detail = describeError(e);
2792
+ if (detail.includes(UNKNOWN_LINK_REASON)) return { kind: "unknown", detail };
2793
+ if (isRevokedTokenError(e) || isRevokedTokenError(detail)) return { kind: "unauthorized", detail };
2794
+ if (MISSING_TOOL_RE.test(detail)) return { kind: "missing_tool", detail };
2795
+ if (OFFLINE_RE.test(detail)) return { kind: "offline", detail };
2796
+ return { kind: "rejected", detail };
2797
+ }
2798
+ var UNKNOWN_LINK_REASON, OFFLINE_RE, MISSING_TOOL_RE;
2799
+ var init_linkFailure = __esm({
2800
+ "src/linkFailure.ts"() {
2801
+ "use strict";
2802
+ init_agentToken();
2803
+ UNKNOWN_LINK_REASON = "\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F \u043D\u0435\u0442, \u0430 \u043F\u0440\u0438\u0447\u0438\u043D\u0443 \u043A\u043E\u043D\u043D\u0435\u043A\u0442\u043E\u0440 \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043B";
2804
+ OFFLINE_RE = /fetch failed|econnreset|econnrefused|econnaborted|enotfound|eai_again|etimedout|ehostunreach|enetunreach|epipe|socket hang up|und_err|network error|connection (?:closed|refused|reset|error)|connect timeout/i;
2805
+ MISSING_TOOL_RE = /tool[^.]{0,40}not found|unknown tool|tool_not_found|method not found|-32601\b/i;
2806
+ }
2807
+ });
2808
+
2634
2809
  // src/procList.ts
2635
2810
  function parsePsRows(out) {
2636
2811
  const rows = [];
@@ -2952,7 +3127,7 @@ var init_procCleanup = __esm({
2952
3127
  // src/git.ts
2953
3128
  import { existsSync as existsSync9, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
2954
3129
  import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
2955
- import { isAbsolute as isAbsolute2, join as join15, sep as sep2 } from "path";
3130
+ import { isAbsolute as isAbsolute2, join as join16, sep as sep2 } from "path";
2956
3131
  import { $ as $2 } from "execa";
2957
3132
  function isUnrecoverableRefError(e) {
2958
3133
  if (e instanceof MissingRefError) return true;
@@ -2964,7 +3139,7 @@ async function reclaimStaleWorktree(repo, worktree) {
2964
3139
  }
2965
3140
  function isLinkedWorktree(dir) {
2966
3141
  try {
2967
- return statSync7(join15(dir, ".git")).isFile();
3142
+ return statSync7(join16(dir, ".git")).isFile();
2968
3143
  } catch {
2969
3144
  return false;
2970
3145
  }
@@ -3074,8 +3249,8 @@ async function ensureGitExcluded(worktree, pattern) {
3074
3249
  try {
3075
3250
  const commonDir = await gitPath(worktree, "--git-common-dir");
3076
3251
  if (!commonDir) return false;
3077
- const infoDir = join15(commonDir, "info");
3078
- const excludeFile = join15(infoDir, "exclude");
3252
+ const infoDir = join16(commonDir, "info");
3253
+ const excludeFile = join16(infoDir, "exclude");
3079
3254
  let current = "";
3080
3255
  try {
3081
3256
  current = await readFile(excludeFile, "utf8");
@@ -3095,7 +3270,7 @@ async function gitPath(worktree, flag) {
3095
3270
  const out = await $2({ reject: false })`git -C ${worktree} rev-parse ${flag}`;
3096
3271
  const raw = out.stdout.trim();
3097
3272
  if (out.exitCode !== 0 || !raw) return null;
3098
- return isAbsolute2(raw) ? raw : join15(worktree, raw);
3273
+ return isAbsolute2(raw) ? raw : join16(worktree, raw);
3099
3274
  }
3100
3275
  async function ensureWorktreeHygiene(worktree) {
3101
3276
  for (const pattern of SWARM_EXCLUDE_PATTERNS) await ensureGitExcluded(worktree, pattern);
@@ -3105,12 +3280,12 @@ async function installGuardHooks(worktree) {
3105
3280
  try {
3106
3281
  const gitDir = await gitPath(worktree, "--git-dir");
3107
3282
  if (!gitDir) return false;
3108
- const hooksDir = join15(gitDir, "swarm-hooks");
3283
+ const hooksDir = join16(gitDir, "swarm-hooks");
3109
3284
  const prev = await previousHooksPath(worktree);
3110
3285
  await mkdir(hooksDir, { recursive: true });
3111
3286
  for (const hook of GUARD_HOOKS) {
3112
- await writeFile(join15(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
3113
- await chmod(join15(hooksDir, hook), 493);
3287
+ await writeFile(join16(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
3288
+ await chmod(join16(hooksDir, hook), 493);
3114
3289
  }
3115
3290
  await $2`git -C ${worktree} config extensions.worktreeConfig true`;
3116
3291
  await $2`git -C ${worktree} config --worktree core.hooksPath ${posix(hooksDir)}`;
@@ -3129,7 +3304,7 @@ async function previousHooksPath(worktree) {
3129
3304
  if (out.exitCode === 0 && value) return value;
3130
3305
  }
3131
3306
  const commonDir = await gitPath(worktree, "--git-common-dir");
3132
- return commonDir ? join15(commonDir, "hooks") : null;
3307
+ return commonDir ? join16(commonDir, "hooks") : null;
3133
3308
  }
3134
3309
  async function removeWorktree(repo, path) {
3135
3310
  const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
@@ -3148,7 +3323,7 @@ async function worktreeDirty(path) {
3148
3323
  async function repairMovedWorktrees(targetDir) {
3149
3324
  const subdirs = (dir) => {
3150
3325
  try {
3151
- return readdirSync10(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join15(dir, d.name));
3326
+ return readdirSync10(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join16(dir, d.name));
3152
3327
  } catch {
3153
3328
  return [];
3154
3329
  }
@@ -3275,18 +3450,18 @@ __export(droneChannel_exports, {
3275
3450
  verifyLocalCliToken: () => verifyLocalCliToken
3276
3451
  });
3277
3452
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
3278
- import { chmodSync, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
3453
+ import { chmodSync, mkdirSync as mkdirSync7, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
3279
3454
  import { dirname as dirname3 } from "path";
3280
3455
  import { serve } from "@hono/node-server";
3281
3456
  function loadChannelSecret(path) {
3282
3457
  try {
3283
- const s = readFileSync13(path, "utf8").trim();
3458
+ const s = readFileSync14(path, "utf8").trim();
3284
3459
  if (s.length >= 32) return s;
3285
3460
  } catch {
3286
3461
  }
3287
3462
  const secret = randomBytes(32).toString("hex");
3288
3463
  mkdirSync7(dirname3(path), { recursive: true });
3289
- writeFileSync9(path, secret, { mode: 384 });
3464
+ writeFileSync10(path, secret, { mode: 384 });
3290
3465
  try {
3291
3466
  chmodSync(path, 384);
3292
3467
  } catch {
@@ -3325,6 +3500,7 @@ function bad(status, error) {
3325
3500
  function scopeOf(store, run) {
3326
3501
  const ids = /* @__PURE__ */ new Set([run.id]);
3327
3502
  if (run.reviews_run_id != null) ids.add(run.reviews_run_id);
3503
+ if (run.verifies_run_id != null) ids.add(run.verifies_run_id);
3328
3504
  if (run.kind === "hero") for (const r of store.squadRuns(run.id)) ids.add(r.id);
3329
3505
  return ids;
3330
3506
  }
@@ -3337,6 +3513,10 @@ function invalidPatchValue(field, v) {
3337
3513
  return typeof v === "string" && v in STATUS_META ? null : `\u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0439 \u0441\u0442\u0430\u0442\u0443\u0441: ${String(v)}`;
3338
3514
  case "review_verdict":
3339
3515
  return v === null || v === "approve" || v === "changes" ? null : `\u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0439 \u0432\u0435\u0440\u0434\u0438\u043A\u0442: ${String(v)}`;
3516
+ // TASWM-444: словарь исходов верификатора — один на весь продукт (swarm-core), поэтому
3517
+ // проверяем его же функцией, а не повторяем литералы третьим списком.
3518
+ case "verify_verdict":
3519
+ return v === null || isVerifyVerdict(v) ? null : `\u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0439 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440\u0430: ${String(v)}`;
3340
3520
  case "review_cycle":
3341
3521
  return typeof v === "number" && Number.isInteger(v) && v >= 0 ? null : `\u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0439 \u0446\u0438\u043A\u043B \u0440\u0435\u0432\u044C\u044E: ${String(v)}`;
3342
3522
  case "pr_url":
@@ -3600,7 +3780,16 @@ var init_droneChannel = __esm({
3600
3780
  "use strict";
3601
3781
  init_src();
3602
3782
  LOCAL_CLI_KEY = "cli";
3603
- PATCHABLE = /* @__PURE__ */ new Set(["status", "pr_url", "review_verdict", "review_cycle"]);
3783
+ PATCHABLE = /* @__PURE__ */ new Set([
3784
+ "status",
3785
+ "pr_url",
3786
+ "review_verdict",
3787
+ // TASWM-444: вердикт верификатора — такая же сдача работы, как review_verdict у ревьюера,
3788
+ // и пишется он тем же путём на ПРОВЕРЯЕМЫЙ прогон. Без него роль verifier была немой:
3789
+ // вердикт выносился комментарием на PR, а `done` отбивался каналом.
3790
+ "verify_verdict",
3791
+ "review_cycle"
3792
+ ]);
3604
3793
  EVENT_KIND_SET = new Set(EVENT_KINDS);
3605
3794
  ChannelUnavailableError = class extends Error {
3606
3795
  constructor(downMs, message) {
@@ -3616,7 +3805,7 @@ var init_droneChannel = __esm({
3616
3805
 
3617
3806
  // src/attachments.ts
3618
3807
  import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3619
- import { join as join18 } from "path";
3808
+ import { join as join19 } from "path";
3620
3809
  function attachmentsFromDoc(doc, origin) {
3621
3810
  const out = [];
3622
3811
  const walk2 = (node) => {
@@ -3707,7 +3896,7 @@ async function materializeAttachments(opts) {
3707
3896
  };
3708
3897
  const result = { files: [], skipped: [] };
3709
3898
  if (opts.refs.length === 0) return result;
3710
- const dir = join18(opts.worktree, ATTACHMENTS_DIR);
3899
+ const dir = join19(opts.worktree, ATTACHMENTS_DIR);
3711
3900
  await io.removeDir(dir);
3712
3901
  let dirReady = false;
3713
3902
  let total = 0;
@@ -3753,7 +3942,7 @@ async function materializeAttachments(opts) {
3753
3942
  await io.mkdir(dir);
3754
3943
  dirReady = true;
3755
3944
  }
3756
- await io.writeFile(join18(dir, name), file.bytes);
3945
+ await io.writeFile(join19(dir, name), file.bytes);
3757
3946
  } catch (e) {
3758
3947
  result.skipped.push(`${label} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText(e)})`);
3759
3948
  continue;
@@ -3906,7 +4095,7 @@ var init_attachments = __esm({
3906
4095
 
3907
4096
  // src/shotUpload.ts
3908
4097
  import { readFile as fsReadFile } from "fs/promises";
3909
- import { basename as basename2, isAbsolute as isAbsolute3, join as join19 } from "path";
4098
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join20 } from "path";
3910
4099
  function shotMimetype(path) {
3911
4100
  const dot = path.lastIndexOf(".");
3912
4101
  if (dot < 0) return null;
@@ -3932,7 +4121,7 @@ async function readShots(paths, opts) {
3932
4121
  }
3933
4122
  let bytes;
3934
4123
  try {
3935
- bytes = await read(isAbsolute3(path) ? path : join19(opts.worktree, path));
4124
+ bytes = await read(isAbsolute3(path) ? path : join20(opts.worktree, path));
3936
4125
  } catch (e) {
3937
4126
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
3938
4127
  continue;
@@ -4010,15 +4199,15 @@ var init_shotUpload = __esm({
4010
4199
 
4011
4200
  // src/oauth.ts
4012
4201
  import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
4013
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
4014
- import { join as join20 } from "path";
4202
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
4203
+ import { join as join21 } from "path";
4015
4204
  function makePkce() {
4016
4205
  const verifier = randomBytes2(48).toString("base64url");
4017
4206
  const challenge = createHash2("sha256").update(verifier).digest("base64url");
4018
4207
  return { verifier, challenge };
4019
4208
  }
4020
4209
  async function registerClient(args, fetchFn = fetch) {
4021
- const res = await fetchFn(`${base(args.api)}${REGISTER}`, {
4210
+ const res = await fetchFn(`${base2(args.api)}${REGISTER}`, {
4022
4211
  method: "POST",
4023
4212
  headers: { "Content-Type": "application/json" },
4024
4213
  body: JSON.stringify({
@@ -4033,7 +4222,7 @@ async function registerClient(args, fetchFn = fetch) {
4033
4222
  return j.client_id;
4034
4223
  }
4035
4224
  function authorizeUrl(args) {
4036
- const u = new URL(`${base(args.api)}${AUTHORIZE}`);
4225
+ const u = new URL(`${base2(args.api)}${AUTHORIZE}`);
4037
4226
  u.searchParams.set("response_type", "code");
4038
4227
  u.searchParams.set("client_id", args.clientId);
4039
4228
  u.searchParams.set("redirect_uri", args.redirectUri);
@@ -4044,7 +4233,7 @@ function authorizeUrl(args) {
4044
4233
  return u.toString();
4045
4234
  }
4046
4235
  async function postToken(api, params, fetchFn) {
4047
- const res = await fetchFn(`${base(api)}${TOKEN}`, {
4236
+ const res = await fetchFn(`${base2(api)}${TOKEN}`, {
4048
4237
  method: "POST",
4049
4238
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
4050
4239
  body: new URLSearchParams(params).toString()
@@ -4095,23 +4284,23 @@ function toStore(clientId, t, now) {
4095
4284
  }
4096
4285
  function loadStore() {
4097
4286
  try {
4098
- return JSON.parse(readFileSync14(STORE_PATH, "utf8"));
4287
+ return JSON.parse(readFileSync15(STORE_PATH2, "utf8"));
4099
4288
  } catch {
4100
4289
  return null;
4101
4290
  }
4102
4291
  }
4103
4292
  function saveStore(s) {
4104
- writeFileSync10(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
4293
+ writeFileSync11(STORE_PATH2, JSON.stringify(s, null, 2), { mode: 384 });
4105
4294
  }
4106
4295
  function clearStore() {
4107
4296
  try {
4108
- unlinkSync3(STORE_PATH);
4297
+ unlinkSync4(STORE_PATH2);
4109
4298
  } catch {
4110
4299
  }
4111
4300
  }
4112
4301
  function loadClientId() {
4113
4302
  try {
4114
- return JSON.parse(readFileSync14(CLIENT_PATH, "utf8")).clientId;
4303
+ return JSON.parse(readFileSync15(CLIENT_PATH, "utf8")).clientId;
4115
4304
  } catch {
4116
4305
  return null;
4117
4306
  }
@@ -4120,7 +4309,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
4120
4309
  const existing = loadClientId();
4121
4310
  if (existing) return existing;
4122
4311
  const clientId = await registerClient({ api, redirectUri }, fetchFn);
4123
- writeFileSync10(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
4312
+ writeFileSync11(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
4124
4313
  return clientId;
4125
4314
  }
4126
4315
  async function ensureAccess(store, opts = {}) {
@@ -4137,7 +4326,7 @@ async function ensureAccess(store, opts = {}) {
4137
4326
  return null;
4138
4327
  }
4139
4328
  }
4140
- var REGISTER, AUTHORIZE, TOKEN, SCOPE, STORE_PATH, CLIENT_PATH, base;
4329
+ var REGISTER, AUTHORIZE, TOKEN, SCOPE, STORE_PATH2, CLIENT_PATH, base2;
4141
4330
  var init_oauth = __esm({
4142
4331
  "src/oauth.ts"() {
4143
4332
  "use strict";
@@ -4146,64 +4335,45 @@ var init_oauth = __esm({
4146
4335
  AUTHORIZE = "/api/oauth/authorize";
4147
4336
  TOKEN = "/api/oauth/token";
4148
4337
  SCOPE = "openid profile email taskless:full";
4149
- STORE_PATH = join20(SWARM_HOME, "oauth.json");
4150
- CLIENT_PATH = join20(SWARM_HOME, "oauth-client.json");
4151
- base = (api) => api.replace(/\/$/, "");
4338
+ STORE_PATH2 = join21(SWARM_HOME, "oauth.json");
4339
+ CLIENT_PATH = join21(SWARM_HOME, "oauth-client.json");
4340
+ base2 = (api) => api.replace(/\/$/, "");
4152
4341
  }
4153
4342
  });
4154
4343
 
4155
- // src/agentToken.ts
4156
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
4157
- import { join as join21 } from "path";
4158
- function loadAgentToken() {
4159
- try {
4160
- const s = JSON.parse(readFileSync15(STORE_PATH2, "utf8"));
4161
- return s.token ? s : null;
4162
- } catch {
4163
- return null;
4164
- }
4344
+ // src/authState.ts
4345
+ function noteTokenRejected(nowMs = Date.now()) {
4346
+ if (!denial) {
4347
+ denial = { since: nowMs, lastShoutAt: nowMs, count: 1 };
4348
+ return revokedTokenComplaint();
4349
+ }
4350
+ denial.count += 1;
4351
+ if (nowMs - denial.lastShoutAt < SHOUT_EVERY_MS) return null;
4352
+ denial.lastShoutAt = nowMs;
4353
+ return `${revokedTokenComplaint()}
4354
+ ${deniedSinceLine(nowMs)}
4355
+ `;
4165
4356
  }
4166
- async function probeAgentToken(args, fetchFn = fetch) {
4167
- try {
4168
- const res = await fetchFn(`${base2(args.api)}/api/auth/me`, {
4169
- headers: { Authorization: `Bearer ${args.token}` }
4170
- });
4171
- if (res.status === 401 || res.status === 403) return "revoked";
4172
- return res.ok ? "ok" : "unreachable";
4173
- } catch {
4174
- return "unreachable";
4175
- }
4357
+ function noteTokenAccepted() {
4358
+ denial = null;
4176
4359
  }
4177
- function isRevokedTokenError(e) {
4178
- if (e && typeof e === "object" && e.code === 401) return true;
4179
- const text = e instanceof Error ? e.message : typeof e === "string" ? e : "";
4180
- return /invalid_token|(?:http|status|code)[ =:]*401\b|\(401\)|401 unauthorized/i.test(text);
4360
+ function authStateLine(nowMs = Date.now()) {
4361
+ if (!denial) return null;
4362
+ return `\u0414\u041E\u0421\u0422\u0423\u041F \u041A TASKLESS \u041E\u0422\u041E\u0417\u0412\u0410\u041D \u2014 ${deniedSinceLine(nowMs)}. \u041F\u043E\u0447\u0438\u043D\u0438 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: swarm login`;
4181
4363
  }
4182
- function revokedTokenLine() {
4183
- return "[taskless] \u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u043E\u0437\u0432\u0430\u043D (401 invalid_token) \u2014 \u0440\u043E\u0439 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442 \u0440\u0430\u0431\u043E\u0442\u0443. \u041F\u043E\u0447\u0438\u043D\u0438 \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: swarm login";
4364
+ function deniedSinceLine(nowMs) {
4365
+ const d = denial;
4366
+ if (!d) return "";
4367
+ const min = Math.max(1, Math.round((nowMs - d.since) / 6e4));
4368
+ return `\u043F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0440\u0433\u0430\u0435\u0442 \u0442\u043E\u043A\u0435\u043D ${min} \u043C\u0438\u043D, \u043E\u0442\u043A\u0430\u0437\u043E\u0432 ${d.count}`;
4184
4369
  }
4185
- function revokedTokenComplaint() {
4186
- return [
4187
- "",
4188
- "\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E",
4189
- "\u2502 \u0422\u041E\u041A\u0415\u041D \u0420\u041E\u042F \u041D\u0415 \u041F\u0420\u0418\u041D\u042F\u0422 (401). \u0414\u043E\u0441\u0442\u0443\u043F \u043A Taskless \u0440\u0430\u0437\u043E\u0440\u0432\u0430\u043D. \u2502",
4190
- "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F",
4191
- "\u041F\u0440\u0438\u0447\u0438\u043D\u0430 \u2014 \u043E\u0434\u043D\u0430 \u0438\u0437 \u0434\u0432\u0443\u0445, \u043E\u0431\u0435 \u0434\u0435\u043B\u0430\u044E\u0442\u0441\u044F \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0435\u043C \u0432 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435:",
4192
- " \u2022 \xAB\u0441\u0431\u0440\u043E\u0441\u0438\u0442\u044C \u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F\xBB \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 (\u041F\u0440\u043E\u0444\u0438\u043B\u044C \u2192 \u0420\u043E\u0439) \u2014 \u0442\u043E\u043A\u0435\u043D \u043E\u0442\u043E\u0437\u0432\u0430\u043D;",
4193
- " \u2022 \u0440\u043E\u0439 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D \u043C\u0430\u0441\u0442\u0435\u0440-\u0442\u0443\u043C\u0431\u043B\u0435\u0440\u043E\u043C \u2014 \u0442\u043E\u043A\u0435\u043D \u0430\u0433\u0435\u043D\u0442\u0430 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u0451\u0442 \u0434\u0435\u0439\u0441\u0442\u0432\u043E\u0432\u0430\u0442\u044C.",
4194
- "",
4195
- "\u0414\u0435\u043C\u043E\u043D \u041D\u0415 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432\u0441\u043B\u0435\u043F\u0443\u044E: \u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442, \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043D\u0435 \u0431\u0435\u0440\u0443\u0442\u0441\u044F \u0438 \u043E\u0442\u0447\u0451\u0442\u044B",
4196
- "\u043D\u0435 \u043F\u0438\u0448\u0443\u0442\u0441\u044F. \u041F\u043E\u0447\u0438\u043D\u0438 \u0432\u0445\u043E\u0434\u043E\u043C \u0437\u0430\u043D\u043E\u0432\u043E: swarm login",
4197
- ""
4198
- ].join("\n");
4199
- }
4200
- var STORE_PATH2, base2;
4201
- var init_agentToken = __esm({
4202
- "src/agentToken.ts"() {
4370
+ var SHOUT_EVERY_MS, denial;
4371
+ var init_authState = __esm({
4372
+ "src/authState.ts"() {
4203
4373
  "use strict";
4204
- init_config();
4205
- STORE_PATH2 = join21(SWARM_HOME, "agent.json");
4206
- base2 = (api) => api.replace(/\/$/, "");
4374
+ init_agentToken();
4375
+ SHOUT_EVERY_MS = 10 * 6e4;
4376
+ denial = null;
4207
4377
  }
4208
4378
  });
4209
4379
 
@@ -4244,6 +4414,7 @@ __export(taskless_exports, {
4244
4414
  extractText: () => extractText,
4245
4415
  fetchTaskContext: () => fetchTaskContext,
4246
4416
  isClosingTaskCategory: () => isClosingTaskCategory,
4417
+ lastSwarmConnectFailure: () => lastSwarmConnectFailure,
4247
4418
  makeFileFetcher: () => makeFileFetcher,
4248
4419
  makeTasklessClient: () => makeTasklessClient,
4249
4420
  parseWorkspaces: () => parseWorkspaces,
@@ -4588,12 +4759,14 @@ async function preflightAuth(opts = {}) {
4588
4759
  if (!agent?.token) return true;
4589
4760
  const probe = await probeAgentToken({ api, token: agent.token }, opts.fetchFn ?? fetch);
4590
4761
  if (probe === "revoked") {
4591
- out(revokedTokenComplaint());
4762
+ out(noteTokenRejected() ?? revokedTokenComplaint());
4592
4763
  return false;
4593
4764
  }
4594
4765
  if (probe === "unreachable") {
4595
4766
  out("[taskless] \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (\u0441\u0435\u0442\u044C/\u043F\u0440\u043E\u0434 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D) \u2014 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u044E, \u043D\u043E \u0441\u0432\u044F\u0437\u0438 \u043C\u043E\u0436\u0435\u0442 \u043D\u0435 \u0431\u044B\u0442\u044C.");
4767
+ return true;
4596
4768
  }
4769
+ noteTokenAccepted();
4597
4770
  return true;
4598
4771
  }
4599
4772
  async function connectTaskless(cfg) {
@@ -4610,11 +4783,15 @@ async function connectTaskless(cfg) {
4610
4783
  try {
4611
4784
  await client.connect(transport, { timeout: TASKLESS_CONNECT_TIMEOUT_MS });
4612
4785
  } catch (e) {
4613
- console.warn(
4614
- isRevokedTokenError(e) ? `${revokedTokenLine()} (\u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442 \u2014 slug-only \u0440\u0435\u0436\u0438\u043C)` : `Taskless: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 slug-only. \u0412\u043E\u0439\u0434\u0438 \u0437\u0430\u043D\u043E\u0432\u043E.`
4615
- );
4786
+ if (isRevokedTokenError(e)) {
4787
+ const shout = noteTokenRejected();
4788
+ if (shout) console.error(shout);
4789
+ } else {
4790
+ console.warn(`Taskless: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${describeError(e)}) \u2192 slug-only. \u0412\u043E\u0439\u0434\u0438 \u0437\u0430\u043D\u043E\u0432\u043E.`);
4791
+ }
4616
4792
  return null;
4617
4793
  }
4794
+ noteTokenAccepted();
4618
4795
  const call = async (name, args) => await client.callTool({ name, arguments: args });
4619
4796
  return makeTasklessClient(
4620
4797
  call,
@@ -4627,27 +4804,41 @@ async function connectSwarmMcp(cfg) {
4627
4804
  const api = cfg?.api ?? TASKLESS_API;
4628
4805
  const workspace = cfg?.workspace ?? resolvedWorkspaceId();
4629
4806
  const token = await resolveApiToken(api, cfg?.token);
4630
- if (!token) return null;
4807
+ if (!token) {
4808
+ swarmConnectFailure = "\u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 \u0442\u043E\u043A\u0435\u043D\u0430 \u0440\u043E\u044F \u2014 \u043D\u0443\u0436\u0435\u043D \u0432\u0445\u043E\u0434: swarm login";
4809
+ return null;
4810
+ }
4631
4811
  const url = new URL(mcpEndpoint(api));
4632
4812
  const transport = new StreamableHTTPClientTransport(url, {
4633
4813
  requestInit: { headers: { Authorization: `Bearer ${token}` } }
4634
4814
  });
4635
4815
  const client = new Client({ name: "taskless-swarm-client", version: "0.1.0" }, { capabilities: {} });
4636
4816
  try {
4637
- await client.connect(transport);
4817
+ await client.connect(transport, { timeout: TASKLESS_CONNECT_TIMEOUT_MS });
4638
4818
  } catch (e) {
4639
- console.warn(
4640
- isRevokedTokenError(e) ? `${revokedTokenLine()} (\u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442 \u2014 \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u043A\u043E\u043F\u044F\u0442\u0441\u044F \u0432 outbox)` : `Swarm MCP: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 \u0431\u0443\u0444\u0435\u0440 \u0432 outbox.`
4641
- );
4819
+ if (isRevokedTokenError(e)) {
4820
+ swarmConnectFailure = revokedTokenLine();
4821
+ const shout = noteTokenRejected();
4822
+ if (shout) console.error(`${shout}
4823
+ \u0421\u043E\u0431\u044B\u0442\u0438\u044F \u043F\u043E\u043A\u0430 \u043A\u043E\u043F\u044F\u0442\u0441\u044F \u0432 outbox \u0438 \u0443\u0435\u0434\u0443\u0442, \u043A\u043E\u0433\u0434\u0430 \u0434\u043E\u0441\u0442\u0443\u043F \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F.`);
4824
+ } else {
4825
+ swarmConnectFailure = describeError(e);
4826
+ console.warn(`Swarm MCP: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${swarmConnectFailure}) \u2192 \u0431\u0443\u0444\u0435\u0440 \u0432 outbox.`);
4827
+ }
4642
4828
  return null;
4643
4829
  }
4830
+ swarmConnectFailure = null;
4831
+ noteTokenAccepted();
4644
4832
  return async (name, args) => await client.callTool({ name, arguments: swarmToolArgs(name, args, workspace) });
4645
4833
  }
4834
+ function lastSwarmConnectFailure() {
4835
+ return swarmConnectFailure;
4836
+ }
4646
4837
  function swarmToolArgs(name, args, workspace) {
4647
4838
  if (!WS_SCOPED_SWARM_TOOLS.has(name) || !workspace) return args;
4648
4839
  return { workspaceId: workspace, ...args };
4649
4840
  }
4650
- var SLUG_RE, WS_FREE_TOOLS, TaskBodyUnavailableError, TASK_CONTEXT_MAX_DOCS, TASK_CONTEXT_MAX_DOC_CHARS, TASK_CONTEXT_MAX_COMMENTS, TASK_CONTEXT_MAX_COMMENT_CHARS, UNTRUSTED_TASK_INPUT_NOTICE, TASKLESS_CONNECT_TIMEOUT_MS, FILE_FETCH_TIMEOUT_MS, FILE_FETCH_MAX_REDIRECTS, legacyWarned, WS_SCOPED_SWARM_TOOLS;
4841
+ var SLUG_RE, WS_FREE_TOOLS, TaskBodyUnavailableError, TASK_CONTEXT_MAX_DOCS, TASK_CONTEXT_MAX_DOC_CHARS, TASK_CONTEXT_MAX_COMMENTS, TASK_CONTEXT_MAX_COMMENT_CHARS, UNTRUSTED_TASK_INPUT_NOTICE, TASKLESS_CONNECT_TIMEOUT_MS, FILE_FETCH_TIMEOUT_MS, FILE_FETCH_MAX_REDIRECTS, legacyWarned, swarmConnectFailure, WS_SCOPED_SWARM_TOOLS;
4651
4842
  var init_taskless = __esm({
4652
4843
  "src/taskless.ts"() {
4653
4844
  "use strict";
@@ -4657,6 +4848,8 @@ var init_taskless = __esm({
4657
4848
  init_config();
4658
4849
  init_oauth();
4659
4850
  init_agentToken();
4851
+ init_linkFailure();
4852
+ init_authState();
4660
4853
  init_workspace();
4661
4854
  SLUG_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
4662
4855
  WS_FREE_TOOLS = /* @__PURE__ */ new Set(["list_workspaces", "who_am_i", "finalize_upload", "report_acceptance_check"]);
@@ -4687,6 +4880,7 @@ var init_taskless = __esm({
4687
4880
  FILE_FETCH_TIMEOUT_MS = 15e3;
4688
4881
  FILE_FETCH_MAX_REDIRECTS = 3;
4689
4882
  legacyWarned = false;
4883
+ swarmConnectFailure = null;
4690
4884
  WS_SCOPED_SWARM_TOOLS = /* @__PURE__ */ new Set(["swarm_run_upsert", "get_task"]);
4691
4885
  }
4692
4886
  });
@@ -4748,6 +4942,10 @@ function rowToRun(row) {
4748
4942
  resolves_run_id: row.resolves_run_id ?? null,
4749
4943
  reviews_run_id: row.reviews_run_id ?? null,
4750
4944
  review_verdict: row.review_verdict ?? null,
4945
+ verifies_run_id: row.verifies_run_id ?? null,
4946
+ // TASWM-344: через узкий парсер, а не сырой строкой — в зеркале может лежать значение,
4947
+ // записанное демоном другой версии; непонятное здесь значит «вердикта нет».
4948
+ verify_verdict: toVerifyVerdict(row.verify_verdict),
4751
4949
  review_phase: row.review_phase ?? null,
4752
4950
  // вычисляемая фаза ревью (TASWM-230)
4753
4951
  review_cycle: row.review_cycle ?? 0,
@@ -4816,6 +5014,8 @@ CREATE TABLE IF NOT EXISTS runs (
4816
5014
  resolves_run_id INTEGER,
4817
5015
  reviews_run_id INTEGER,
4818
5016
  review_verdict TEXT,
5017
+ verifies_run_id INTEGER,
5018
+ verify_verdict TEXT,
4819
5019
  review_cycle INTEGER NOT NULL DEFAULT 0,
4820
5020
  task_title TEXT,
4821
5021
  task_desc TEXT,
@@ -4892,6 +5092,12 @@ CREATE TABLE IF NOT EXISTS imports (
4892
5092
  // вердикт (approve|changes), который ревьюер пишет на проверяемый прогон.
4893
5093
  "ALTER TABLE runs ADD COLUMN reviews_run_id INTEGER",
4894
5094
  "ALTER TABLE runs ADD COLUMN review_verdict TEXT",
5095
+ // Верификатор (TASWM-344): прогон, который пытается ОПРОВЕРГНУТЬ работу
5096
+ // #verifies_run_id, и его вердикт (confirmed|not_reproduced|found_issues) на
5097
+ // проверяемом прогоне. Своя пара колонок, а не переиспользование reviews_*:
5098
+ // ревью и верификация идут на одном PR одновременно и считаются раздельно.
5099
+ "ALTER TABLE runs ADD COLUMN verifies_run_id INTEGER",
5100
+ "ALTER TABLE runs ADD COLUMN verify_verdict TEXT",
4895
5101
  // Авто-ревью v2 (TASWM-162): номер цикла ревью — пере-ревью после фикс-итерации.
4896
5102
  "ALTER TABLE runs ADD COLUMN review_cycle INTEGER NOT NULL DEFAULT 0",
4897
5103
  // Вычисляемая фаза ревью (TASWM-230): человеко-фраза «чего ждёт PR», её считает демон.
@@ -4928,7 +5134,7 @@ CREATE TABLE IF NOT EXISTS imports (
4928
5134
  createRun(input) {
4929
5135
  const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
4930
5136
  const res = this.db.prepare(
4931
- "INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, effort, decision, resolves_run_id, reviews_run_id, review_cycle, priority, base_branch, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
5137
+ "INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, effort, decision, resolves_run_id, reviews_run_id, verifies_run_id, review_cycle, priority, base_branch, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
4932
5138
  ).run(
4933
5139
  input.task_slug,
4934
5140
  name,
@@ -4946,6 +5152,8 @@ CREATE TABLE IF NOT EXISTS imports (
4946
5152
  input.decision ? JSON.stringify(input.decision) : null,
4947
5153
  input.resolves_run_id ?? null,
4948
5154
  input.reviews_run_id ?? null,
5155
+ input.verifies_run_id ?? null,
5156
+ // прогон-верификатор (TASWM-344); обычный прогон — null
4949
5157
  input.review_cycle ?? 0,
4950
5158
  // цикл ревью для ревьюера (авто-ревью v2, TASWM-162); обычный прогон — 0
4951
5159
  input.priority ?? 0,
@@ -4956,7 +5164,7 @@ CREATE TABLE IF NOT EXISTS imports (
4956
5164
  // воркспейс задачи прогона (TASWM-288); null → глобальный пин
4957
5165
  );
4958
5166
  const id = Number(res.lastInsertRowid);
4959
- const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.reviews_run_id ? `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u043B\u044F #${input.reviews_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
5167
+ const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.reviews_run_id ? `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u043B\u044F #${input.reviews_run_id}` : input.verifies_run_id ? `\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0434\u043B\u044F #${input.verifies_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
4960
5168
  this.addEvent(id, "spawned", spawnNote);
4961
5169
  return this.getRun(id);
4962
5170
  }
@@ -4982,13 +5190,14 @@ CREATE TABLE IF NOT EXISTS imports (
4982
5190
  */
4983
5191
  ensureRun(input) {
4984
5192
  this.db.prepare(
4985
- "INSERT OR IGNORE INTO runs (id, task_slug, kind, status, name, reviews_run_id, hero_run_id) VALUES (?, ?, ?, 'running', ?, ?, ?)"
5193
+ "INSERT OR IGNORE INTO runs (id, task_slug, kind, status, name, reviews_run_id, verifies_run_id, hero_run_id) VALUES (?, ?, ?, 'running', ?, ?, ?, ?)"
4986
5194
  ).run(
4987
5195
  input.id,
4988
5196
  input.task_slug,
4989
5197
  input.kind ?? "drone",
4990
5198
  input.name || "(\u0438\u043C\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E)",
4991
5199
  input.reviews_run_id ?? null,
5200
+ input.verifies_run_id ?? null,
4992
5201
  input.hero_run_id ?? null
4993
5202
  );
4994
5203
  }
@@ -5133,6 +5342,31 @@ CREATE TABLE IF NOT EXISTS imports (
5133
5342
  const rows = this.db.prepare(`SELECT * FROM runs WHERE reviews_run_id = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC`).all(reviewRunId);
5134
5343
  return rows.map(rowToRun);
5135
5344
  }
5345
+ /**
5346
+ * ВЕРИФИКАЦИЯ (TASWM-344) — те же три счётчика, что у ревью выше, но по своей связи
5347
+ * `verifies_run_id` и со своим капом. Скоуп «текущий цикл ревью» тот же и по той же
5348
+ * причине: фикс-итерация бампит review_cycle и обнуляет вердикты (reviewTransitionPatch),
5349
+ * значит исправленную работу обязан проверить СВЕЖИЙ верификатор, а не считаться уже
5350
+ * полученным подтверждением по коду, которого больше нет.
5351
+ */
5352
+ verifierCountFor(verifyRunId) {
5353
+ const row = this.db.prepare(
5354
+ "SELECT COUNT(*) n FROM runs WHERE verifies_run_id = ? AND status NOT IN ('failed','stalled','stopped','superseded') AND review_cycle = COALESCE((SELECT review_cycle FROM runs WHERE id = ?), 0)"
5355
+ ).get(verifyRunId, verifyRunId);
5356
+ return row.n;
5357
+ }
5358
+ /** Плоский кап попыток верификации на цикл — любой статус, кроме сброшенных оператором. */
5359
+ verifierAttemptCountFor(verifyRunId) {
5360
+ const row = this.db.prepare(
5361
+ "SELECT COUNT(*) n FROM runs WHERE verifies_run_id = ? AND status != 'superseded' AND review_cycle = COALESCE((SELECT review_cycle FROM runs WHERE id = ?), 0)"
5362
+ ).get(verifyRunId, verifyRunId);
5363
+ return row.n;
5364
+ }
5365
+ /** Нетерминальные верификаторы прогона — закрыть зависших, когда PR уже смержен/закрыт. */
5366
+ activeVerifiersFor(verifyRunId) {
5367
+ const rows = this.db.prepare(`SELECT * FROM runs WHERE verifies_run_id = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC`).all(verifyRunId);
5368
+ return rows.map(rowToRun);
5369
+ }
5136
5370
  /**
5137
5371
  * Последний живой (не терминальный) прогон по slug — для swarm cancel. Служебные
5138
5372
  * сабраны (reviews_run_id/resolves_run_id — ревьюер/резолвер, TASWM-273) исключены:
@@ -5147,7 +5381,7 @@ CREATE TABLE IF NOT EXISTS imports (
5147
5381
  */
5148
5382
  activeRunForSlug(slug) {
5149
5383
  const row = this.db.prepare(
5150
- `SELECT * FROM runs WHERE task_slug = ? AND status NOT IN (${TERMINAL_SQL}) AND reviews_run_id IS NULL AND resolves_run_id IS NULL ORDER BY id DESC LIMIT 1`
5384
+ `SELECT * FROM runs WHERE task_slug = ? AND status NOT IN (${TERMINAL_SQL}) AND reviews_run_id IS NULL AND resolves_run_id IS NULL AND verifies_run_id IS NULL ORDER BY id DESC LIMIT 1`
5151
5385
  ).get(slug);
5152
5386
  return row ? rowToRun(row) : null;
5153
5387
  }
@@ -5187,7 +5421,7 @@ CREATE TABLE IF NOT EXISTS imports (
5187
5421
  if (!depends_on || depends_on.length === 0) return [];
5188
5422
  const slugs = [...new Set(depends_on)];
5189
5423
  const rows = this.db.prepare(
5190
- `SELECT task_slug, status, name, stop_reason FROM runs WHERE task_slug IN (${slugs.map(() => "?").join(",")}) AND resolves_run_id IS NULL AND reviews_run_id IS NULL ORDER BY id`
5424
+ `SELECT task_slug, status, name, stop_reason FROM runs WHERE task_slug IN (${slugs.map(() => "?").join(",")}) AND resolves_run_id IS NULL AND reviews_run_id IS NULL AND verifies_run_id IS NULL ORDER BY id`
5191
5425
  ).all(...slugs);
5192
5426
  const bySlug = /* @__PURE__ */ new Map();
5193
5427
  for (const r of rows) {
@@ -5935,7 +6169,16 @@ var SwarmClient = class _SwarmClient {
5935
6169
  static REJECT_LIMIT = 5;
5936
6170
  static BACKOFF_BASE = 500;
5937
6171
  static BACKOFF_MAX = 3e4;
6172
+ // TASWM-325: момент УДАЧНОГО пульса — «когда прод последний раз слышал эту машину»
6173
+ // (прод пишет SwarmHost.lastSeenAt ровно на swarm_heartbeat). Единственное, что про
6174
+ // связь знает КЛИЕНТ: жива ли она вообще — знает транспорт (mcpResilient.link), а
6175
+ // отвергнут ли токен — authState. Здесь их не угадываем: первая редакция считала
6176
+ // успехом любой возврат call и на боевом каллере (который не бросает, а возвращает
6177
+ // isError) всегда показывала «связь есть».
6178
+ heartbeatOkAt = null;
6179
+ now;
5938
6180
  constructor(opts) {
6181
+ this.now = opts.now ?? Date.now;
5939
6182
  this.call = opts.call;
5940
6183
  this.defaultRepo = normalizeRepo(opts.defaultRepo ?? "");
5941
6184
  this.autoFlush = opts.autoFlush ?? true;
@@ -6078,6 +6321,7 @@ var SwarmClient = class _SwarmClient {
6078
6321
  if ("decision" in patch) out.decision = patch.decision ? JSON.stringify(patch.decision) : null;
6079
6322
  if ("kind" in patch && patch.kind !== void 0) out.role = patch.kind;
6080
6323
  if ("review_verdict" in patch) out.reviewVerdict = patch.review_verdict ?? null;
6324
+ if ("verify_verdict" in patch) out.verifyVerdict = patch.verify_verdict ?? null;
6081
6325
  if ("host_id" in patch && patch.host_id) {
6082
6326
  out.hostId = patch.host_id;
6083
6327
  out.hostPinned = patch.host_pinned === true;
@@ -6088,6 +6332,7 @@ var SwarmClient = class _SwarmClient {
6088
6332
  this.setRunRefField(out, "resolvesRunId", patch, "resolves_run_id");
6089
6333
  this.setRunRefField(out, "supersededById", patch, "superseded_by");
6090
6334
  this.setRunRefField(out, "reviewsRunId", patch, "reviews_run_id");
6335
+ this.setRunRefField(out, "verifiesRunId", patch, "verifies_run_id");
6091
6336
  return out;
6092
6337
  }
6093
6338
  // Положить self-ref прогона в прод-патч: локальный number → отложенная ссылка
@@ -6150,6 +6395,9 @@ var SwarmClient = class _SwarmClient {
6150
6395
  resolves_run_id: input.resolves_run_id ?? null,
6151
6396
  reviews_run_id: input.reviews_run_id ?? null,
6152
6397
  review_verdict: null,
6398
+ verifies_run_id: input.verifies_run_id ?? null,
6399
+ // прогон-верификатор (TASWM-344)
6400
+ verify_verdict: null,
6153
6401
  review_phase: null,
6154
6402
  // вычисляемая фаза ревью (TASWM-230) — демон посчитает на review-тике
6155
6403
  review_cycle: input.review_cycle ?? 0,
@@ -6201,6 +6449,8 @@ var SwarmClient = class _SwarmClient {
6201
6449
  // резолвер конфликта → связь на review-прогон
6202
6450
  reviews_run_id: d.reviews_run_id,
6203
6451
  // ревьюер (авто-ревью, TASWM-148) → связь на review-прогон
6452
+ verifies_run_id: d.verifies_run_id,
6453
+ // верификатор (TASWM-344) → связь на проверяемый прогон
6204
6454
  host_id: d.host_id,
6205
6455
  // машина прогона (TASWM-281) — своя с рождения либо заданная --host
6206
6456
  host_pinned: d.host_pinned,
@@ -6208,7 +6458,7 @@ var SwarmClient = class _SwarmClient {
6208
6458
  base_branch: d.base_branch
6209
6459
  // базовая ветка PR (TASWM-285), если задана на спавне
6210
6460
  }));
6211
- const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.reviews_run_id ? `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u043B\u044F #${input.reviews_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
6461
+ const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.reviews_run_id ? `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u043B\u044F #${input.reviews_run_id}` : input.verifies_run_id ? `\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0434\u043B\u044F #${input.verifies_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
6212
6462
  this.addEvent(id, "spawned", spawnNote);
6213
6463
  return this.mat(d);
6214
6464
  }
@@ -6312,6 +6562,24 @@ var SwarmClient = class _SwarmClient {
6312
6562
  activeReviewersFor(reviewRunId) {
6313
6563
  return [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId && !TERMINAL_STATUSES.includes(r.status)).sort((a, b) => b.id - a.id).map((d) => this.mat(d));
6314
6564
  }
6565
+ // Верификация (TASWM-344) — зеркало трёх счётчиков ревью выше по своей связи
6566
+ // verifies_run_id; правила «кто не в счёт» те же (см. db.ts:verifierCountFor).
6567
+ verifierCountFor(verifyRunId) {
6568
+ const cycle = this.runs.get(verifyRunId)?.review_cycle ?? 0;
6569
+ const dead = /* @__PURE__ */ new Set(["failed", "stalled", "stopped", "superseded"]);
6570
+ return [...this.runs.values()].filter(
6571
+ (r) => r.verifies_run_id === verifyRunId && r.review_cycle === cycle && !dead.has(r.status)
6572
+ ).length;
6573
+ }
6574
+ verifierAttemptCountFor(verifyRunId) {
6575
+ const cycle = this.runs.get(verifyRunId)?.review_cycle ?? 0;
6576
+ return [...this.runs.values()].filter(
6577
+ (r) => r.verifies_run_id === verifyRunId && r.review_cycle === cycle && r.status !== "superseded"
6578
+ ).length;
6579
+ }
6580
+ activeVerifiersFor(verifyRunId) {
6581
+ return [...this.runs.values()].filter((r) => r.verifies_run_id === verifyRunId && !TERMINAL_STATUSES.includes(r.status)).sort((a, b) => b.id - a.id).map((d) => this.mat(d));
6582
+ }
6315
6583
  activeRunForSlug(slug) {
6316
6584
  const cands = [...this.runs.values()].filter((r) => r.task_slug === slug && !SLUG_TERMINAL.has(r.status)).sort((a, b) => b.id - a.id);
6317
6585
  return cands.length ? this.mat(cands[0]) : null;
@@ -6347,7 +6615,9 @@ var SwarmClient = class _SwarmClient {
6347
6615
  if (!depends_on || depends_on.length === 0) return [];
6348
6616
  const doneInMirror = /* @__PURE__ */ new Set();
6349
6617
  for (const r of this.runs.values()) {
6350
- if (r.status === "done" && r.resolves_run_id == null && r.reviews_run_id == null) doneInMirror.add(r.task_slug);
6618
+ if (r.status === "done" && r.resolves_run_id == null && r.reviews_run_id == null && r.verifies_run_id == null) {
6619
+ doneInMirror.add(r.task_slug);
6620
+ }
6351
6621
  }
6352
6622
  return depends_on.map((slug) => {
6353
6623
  if (doneInMirror.has(slug)) {
@@ -6393,7 +6663,7 @@ var SwarmClient = class _SwarmClient {
6393
6663
  // Служебный сабран под тем же слагом (резолвер конфликта / ревьюер): его финал —
6394
6664
  // не финал задачи. Резолвер вычёркивался и до TASWM-361, ревьюер добавлен вместе
6395
6665
  // с расширением критерия — иначе остановленный ревьюер закрывал бы зависимость.
6396
- service: Boolean(r.resolvesRunId) || Boolean(r.reviewsRunId),
6666
+ service: Boolean(r.resolvesRunId) || Boolean(r.reviewsRunId) || Boolean(r.verifiesRunId),
6397
6667
  // В проде имя дрона — колонка droneName (см. hydrateRun); `name` терпим как
6398
6668
  // запасной вариант ради фикстур и старых ответов.
6399
6669
  name: r.droneName ?? r.name ?? null,
@@ -6609,7 +6879,7 @@ var SwarmClient = class _SwarmClient {
6609
6879
  return this.eventsLog.filter((e) => {
6610
6880
  if (e.kind !== "failed" || e.run_id == null || e.at < cutoff) return false;
6611
6881
  const r = this.runs.get(e.run_id);
6612
- return !!r && r.resolves_run_id == null && r.reviews_run_id == null;
6882
+ return !!r && r.resolves_run_id == null && r.reviews_run_id == null && r.verifies_run_id == null;
6613
6883
  }).length;
6614
6884
  }
6615
6885
  recordStormProbe(probe, ext, reason, resetHint) {
@@ -6734,7 +7004,7 @@ var SwarmClient = class _SwarmClient {
6734
7004
  const workerWt = droneWorktreePath(opts.worktreesDir, d.repo || this.defaultRepo, d.task_slug);
6735
7005
  let matchWt = null;
6736
7006
  let matchPid = null;
6737
- if (d.resolves_run_id == null && d.reviews_run_id == null) {
7007
+ if (d.resolves_run_id == null && d.reviews_run_id == null && d.verifies_run_id == null) {
6738
7008
  const p = pidByWorktree.get(workerWt);
6739
7009
  if (p != null) {
6740
7010
  matchWt = workerWt;
@@ -6750,9 +7020,10 @@ var SwarmClient = class _SwarmClient {
6750
7020
  }
6751
7021
  }
6752
7022
  } else {
6753
- const reviewed = this.runs.get(d.reviews_run_id);
7023
+ const checkedId = d.reviews_run_id ?? d.verifies_run_id;
7024
+ const reviewed = this.runs.get(checkedId);
6754
7025
  const reviewedWt = reviewed ? droneWorktreePath(opts.worktreesDir, reviewed.repo || this.defaultRepo, reviewed.task_slug) : workerWt;
6755
- const prefix = `${reviewedWt}-review-`;
7026
+ const prefix = d.reviews_run_id != null ? `${reviewedWt}-review-` : `${reviewedWt}-verify-`;
6756
7027
  for (const [wt, p] of pidByWorktree) {
6757
7028
  if (wt.startsWith(prefix)) {
6758
7029
  matchWt = wt;
@@ -6765,7 +7036,7 @@ var SwarmClient = class _SwarmClient {
6765
7036
  this.runtime.set(d.id, { pid: matchPid, worktree_path: matchWt });
6766
7037
  alive++;
6767
7038
  } else {
6768
- const wt = d.resolves_run_id == null && d.reviews_run_id == null && wtExists(workerWt) ? workerWt : null;
7039
+ const wt = d.resolves_run_id == null && d.reviews_run_id == null && d.verifies_run_id == null && wtExists(workerWt) ? workerWt : null;
6769
7040
  this.runtime.set(d.id, { pid: null, worktree_path: wt });
6770
7041
  dead++;
6771
7042
  }
@@ -6786,6 +7057,8 @@ var SwarmClient = class _SwarmClient {
6786
7057
  if (sup) local.superseded_by = this.localIdForProdId(sup) ?? local.superseded_by;
6787
7058
  const rev = pr.reviewsRunId ?? null;
6788
7059
  if (rev) local.reviews_run_id = this.localIdForProdId(rev) ?? local.reviews_run_id;
7060
+ const ver = pr.verifiesRunId ?? null;
7061
+ if (ver) local.verifies_run_id = this.localIdForProdId(ver) ?? local.verifies_run_id;
6789
7062
  }
6790
7063
  /**
6791
7064
  * Авто-интейк (TASWM-100): взять следующую claimable-задачу владельца токена через
@@ -7282,9 +7555,16 @@ var SwarmClient = class _SwarmClient {
7282
7555
  * простоя, видимого только в логе на самой машине. Не передали (нечем измерить) → ключ
7283
7556
  * не шлём, прод прошлое не трогает — та же логика, что у capacity/repoFullNames.
7284
7557
  *
7558
+ * TASWM-333: тем же пульсом уходит ШТОРМ этой машины (`storm` — штормим ли сейчас, и с
7559
+ * какой формулировкой мы в него вошли). Состояние живёт в наших событиях и переживает
7560
+ * рестарт, а в проде его не было вовсе: рой стоял, а парк машин показывал живую свободную
7561
+ * машину — 10.08 это стоило десяти часов простоя, замеченных человеком вручную. Шлём
7562
+ * ВСЕГДА (в т.ч. `storm:false`), как паузу: это состояние, а не команда, и молчание тут
7563
+ * означало бы «прод помнит вчерашний шторм», когда рой давно работает.
7564
+ *
7285
7565
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
7286
7566
  */
7287
- async heartbeat(daemonVersion, pause, capacity, resources) {
7567
+ async heartbeat(daemonVersion, pause, capacity, resources, storm) {
7288
7568
  try {
7289
7569
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
7290
7570
  if (daemonVersion) args.daemonVersion = daemonVersion;
@@ -7294,12 +7574,17 @@ var SwarmClient = class _SwarmClient {
7294
7574
  args.paused = !!pause?.paused;
7295
7575
  if (pause?.reason) args.pauseReason = pause.reason;
7296
7576
  }
7577
+ if (storm !== void 0 && storm !== null) {
7578
+ args.storm = storm.active;
7579
+ if (storm.reason) args.stormReason = storm.reason;
7580
+ }
7297
7581
  if (this.host) {
7298
7582
  args.hostId = this.host.hostId;
7299
7583
  args.hostName = this.host.hostName;
7300
7584
  }
7301
7585
  const res = await this.call("swarm_heartbeat", args);
7302
7586
  if (res?.isError) return EMPTY_HEARTBEAT_REPLY;
7587
+ this.heartbeatOkAt = this.now();
7303
7588
  const data = res?.structuredContent ?? {};
7304
7589
  return {
7305
7590
  commands: Array.isArray(data.commands) ? data.commands.filter(isHostCommandShape) : [],
@@ -7511,6 +7796,9 @@ var SwarmClient = class _SwarmClient {
7511
7796
  resolves_run_id: null,
7512
7797
  reviews_run_id: null,
7513
7798
  review_verdict: null,
7799
+ verifies_run_id: null,
7800
+ // прогон-верификатор (TASWM-344) — прод-поле, заполняется ниже
7801
+ verify_verdict: null,
7514
7802
  review_phase: null,
7515
7803
  // вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
7516
7804
  // Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
@@ -7554,6 +7842,9 @@ var SwarmClient = class _SwarmClient {
7554
7842
  const revProd = pr.reviewsRunId ?? null;
7555
7843
  if (revProd) local.reviews_run_id = this.localIdForProdId(revProd) ?? local.reviews_run_id;
7556
7844
  local.review_verdict = pr.reviewVerdict ?? local.review_verdict;
7845
+ const verProd = pr.verifiesRunId ?? null;
7846
+ if (verProd) local.verifies_run_id = this.localIdForProdId(verProd) ?? local.verifies_run_id;
7847
+ local.verify_verdict = toVerifyVerdict(pr.verifyVerdict) ?? local.verify_verdict;
7557
7848
  local.review_phase = pr.reviewPhase ?? local.review_phase;
7558
7849
  local.repo = this.resolveRepo(pr.repo ?? null);
7559
7850
  local.host_id = pr.hostId ?? null;
@@ -7842,6 +8133,18 @@ var SwarmClient = class _SwarmClient {
7842
8133
  pendingCount() {
7843
8134
  return this.pending.length;
7844
8135
  }
8136
+ /**
8137
+ * Когда прод в последний раз ПРИНЯЛ пульс этой машины (ISO) — вход `swarm status`.
8138
+ * null — ни разу за жизнь процесса.
8139
+ *
8140
+ * Почему именно пульс, а не «последний удачный вызов»: в парке машин lastSeenAt прод
8141
+ * пишет ровно на swarm_heartbeat. Спрашивать за тем же самым swarm_list_hosts значило бы
8142
+ * завести второй источник правды и платить прод-вызовом в команде, которую зовут именно
8143
+ * тогда, когда прод не отвечает.
8144
+ */
8145
+ lastHeardAt() {
8146
+ return this.heartbeatOkAt == null ? null : new Date(this.heartbeatOkAt).toISOString();
8147
+ }
7845
8148
  close() {
7846
8149
  if (this.flushTimer) {
7847
8150
  clearTimeout(this.flushTimer);
@@ -7995,13 +8298,14 @@ function pickRepoPaths(args) {
7995
8298
  // src/repoDiscovery.ts
7996
8299
  init_src();
7997
8300
  init_config();
8301
+ init_linkFailure();
7998
8302
  import { homedir as homedir3 } from "os";
7999
- import { readFileSync as readFileSync8, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
8000
- import { isAbsolute, join as join9, resolve as resolve2 } from "path";
8303
+ import { readFileSync as readFileSync9, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
8304
+ import { isAbsolute, join as join10, resolve as resolve2 } from "path";
8001
8305
 
8002
8306
  // src/repoScan.ts
8003
- import { readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
8004
- import { join as join8 } from "path";
8307
+ import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
8308
+ import { join as join9 } from "path";
8005
8309
  var SKIP = /* @__PURE__ */ new Set([
8006
8310
  "node_modules",
8007
8311
  "vendor",
@@ -8031,7 +8335,7 @@ function parseOriginFromGitConfig(text) {
8031
8335
  }
8032
8336
  function originOf(dir) {
8033
8337
  try {
8034
- return parseOriginFromGitConfig(readFileSync7(join8(dir, ".git", "config"), "utf8"));
8338
+ return parseOriginFromGitConfig(readFileSync8(join9(dir, ".git", "config"), "utf8"));
8035
8339
  } catch {
8036
8340
  return null;
8037
8341
  }
@@ -8066,7 +8370,7 @@ function walk(dir, depth, maxDepth, found) {
8066
8370
  }
8067
8371
  for (const name of entries) {
8068
8372
  if (name.startsWith(".") || SKIP.has(name)) continue;
8069
- const child = join8(dir, name);
8373
+ const child = join9(dir, name);
8070
8374
  try {
8071
8375
  if (!statSync5(child).isDirectory()) continue;
8072
8376
  } catch {
@@ -8088,7 +8392,7 @@ function defaultScanRoots(home, knownPaths = []) {
8088
8392
  if (parent && parent !== p) add(parent);
8089
8393
  }
8090
8394
  for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
8091
- add(join8(home, name));
8395
+ add(join9(home, name));
8092
8396
  }
8093
8397
  return out;
8094
8398
  }
@@ -8102,7 +8406,7 @@ function resolveScanRoots(raw, home = homedir3(), knownPaths = []) {
8102
8406
  for (const part of v.split(",")) {
8103
8407
  const p = part.trim();
8104
8408
  if (!p) continue;
8105
- const abs = p === "~" ? home : p.startsWith("~/") ? join9(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
8409
+ const abs = p === "~" ? home : p.startsWith("~/") ? join10(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
8106
8410
  if (!out.includes(abs)) out.push(abs);
8107
8411
  }
8108
8412
  return out;
@@ -8115,16 +8419,24 @@ function normalizeFullNames(list) {
8115
8419
  }
8116
8420
  return out;
8117
8421
  }
8422
+ function errorText(res) {
8423
+ return (res.content ?? []).map((c) => c?.text ?? "").filter(Boolean).join(" ").trim();
8424
+ }
8118
8425
  async function fetchSwarmRepos(call) {
8119
8426
  let res;
8120
8427
  try {
8121
8428
  res = await call("swarm_my_repos", {});
8122
- } catch {
8123
- return null;
8429
+ } catch (e) {
8430
+ return { ok: false, ...explainLinkFailure(e) };
8431
+ }
8432
+ if (!res) return { ok: false, kind: "offline", detail: "\u0432\u044B\u0437\u043E\u0432 \u043D\u0435 \u0434\u0430\u043B \u043E\u0442\u0432\u0435\u0442\u0430" };
8433
+ if (res.isError) {
8434
+ return { ok: false, ...explainLinkFailure(errorText(res) || "\u0431\u0435\u0437 \u0442\u0435\u043A\u0441\u0442\u0430 \u043E\u0448\u0438\u0431\u043A\u0438") };
8124
8435
  }
8125
- if (!res || res.isError) return null;
8126
8436
  const raw = res.structuredContent?.repos;
8127
- if (!Array.isArray(raw)) return null;
8437
+ if (!Array.isArray(raw)) {
8438
+ return { ok: false, kind: "malformed", detail: `\u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442 \u0441\u043F\u0438\u0441\u043A\u0430 repos (${typeof raw})` };
8439
+ }
8128
8440
  const out = [];
8129
8441
  for (const item of raw) {
8130
8442
  if (!item || typeof item !== "object") continue;
@@ -8137,7 +8449,24 @@ async function fetchSwarmRepos(call) {
8137
8449
  workspaceSlug: typeof r.workspaceSlug === "string" ? r.workspaceSlug : ""
8138
8450
  });
8139
8451
  }
8140
- return out;
8452
+ return { ok: true, repos: out };
8453
+ }
8454
+ function describeReposFailure(kind, detail) {
8455
+ const head = "[overmind] \u043D\u0435 \u0443\u0437\u043D\u0430\u043B \u0443 \u043F\u0440\u043E\u0434\u0430, \u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443 \u0440\u043E\u044F (swarm_my_repos) \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u043F\u043E \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u043C\u0443 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435. ";
8456
+ switch (kind) {
8457
+ case "missing_tool":
8458
+ return `${head}\u041F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442, \u0447\u0442\u043E \u0442\u0430\u043A\u043E\u0439 \u0440\u0443\u0447\u043A\u0438 \u0443 \u043D\u0435\u0433\u043E \u043D\u0435\u0442 (${detail}) \u2014 \u0432\u043E\u0442 \u044D\u0442\u043E \u0438 \u0435\u0441\u0442\u044C \xAB\u0441\u0435\u0440\u0432\u0435\u0440 \u0441\u0442\u0430\u0440\u0435\u0435 \u0434\u0435\u043C\u043E\u043D\u0430\xBB, \u043B\u0435\u0447\u0438\u0442\u0441\u044F \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435\u043C \u043F\u0440\u043E\u0434\u0430.`;
8459
+ case "unauthorized":
8460
+ return `${head}\u041F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0438 \u041D\u0415 \u041F\u0423\u0421\u0422\u0418\u041B: \u0442\u043E\u043A\u0435\u043D \u043D\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u0435\u043D (${detail}). \u041B\u0435\u0447\u0438\u0442\u0441\u044F \u043F\u0435\u0440\u0435\u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0435\u0439: swarm login.`;
8461
+ case "offline":
8462
+ return `${head}\u0412\u042B\u0417\u041E\u0412 \u041D\u0415 \u0414\u041E\u0428\u0401\u041B \u0434\u043E \u043F\u0440\u043E\u0434\u0430 \u2014 \u0441\u0432\u044F\u0437\u044C \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C (${detail}). \u042D\u0442\u043E \u041D\u0415 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 \u0438 \u043D\u0435 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0449\u0430\u044F \u0440\u0443\u0447\u043A\u0430: \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C \u0442\u0438\u043A\u0435, \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043F\u0440\u043E\u0434 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`;
8463
+ case "malformed":
8464
+ return `${head}\u041F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0442\u0438\u043B, \u043D\u043E \u0432 \u043D\u0435\u043F\u043E\u043D\u044F\u0442\u043D\u043E\u043C \u0444\u043E\u0440\u043C\u0430\u0442\u0435 (${detail}).`;
8465
+ case "unknown":
8466
+ return `${head}\u041F\u041E\u0427\u0415\u041C\u0423 \u2014 \u041D\u0415\u0418\u0417\u0412\u0415\u0421\u0422\u041D\u041E (${detail}). \u041D\u0438 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439 \u0441\u0435\u0440\u0432\u0435\u0440, \u043D\u0438 \u043E\u0431\u0440\u044B\u0432 \u0441\u0432\u044F\u0437\u0438 \u043E\u0442\u0441\u044E\u0434\u0430 \u043D\u0435 \u0441\u043B\u0435\u0434\u0443\u044E\u0442: \u0441\u043C\u043E\u0442\u0440\u0438 \u0441\u0442\u0440\u043E\u043A\u0443 \xABSwarm MCP: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E \u2026\xBB \u0432\u044B\u0448\u0435 \u2014 \u0442\u0430\u043C \u043F\u0440\u0438\u0447\u0438\u043D\u0430, \u043A\u043E\u0442\u043E\u0440\u0443\u044E \u0432\u0438\u0434\u0435\u043B \u043A\u043E\u043D\u043D\u0435\u043A\u0442\u043E\u0440.`;
8467
+ default:
8468
+ return `${head}\u041F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0442\u043A\u0430\u0437\u043E\u043C: ${detail}.`;
8469
+ }
8141
8470
  }
8142
8471
  function emptyRepoMapState(roots) {
8143
8472
  return { clones: [], missing: [], roots: [...roots], at: null, unknown: true };
@@ -8147,7 +8476,8 @@ async function refreshRepoMap(opts) {
8147
8476
  });
8148
8477
  const scan = opts.scan ?? scanRootsAll;
8149
8478
  const repoAt = opts.repoAt ?? repoFullNameAt;
8150
- const wanted = await fetchSwarmRepos(opts.call);
8479
+ const answer = await fetchSwarmRepos(opts.call);
8480
+ const wanted = answer.ok ? answer.repos : null;
8151
8481
  const clones = /* @__PURE__ */ new Map();
8152
8482
  const take = (repoFullName, path, source, alternatives = []) => {
8153
8483
  if (clones.has(repoFullName)) return;
@@ -8182,10 +8512,8 @@ async function refreshRepoMap(opts) {
8182
8512
  opts.cachePath,
8183
8513
  [...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path }))
8184
8514
  );
8185
- if (!wanted) {
8186
- log(
8187
- "[overmind] \u043F\u0440\u043E\u0434 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B, \u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443 \u0440\u043E\u044F (swarm_my_repos) \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u043F\u043E \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u043C\u0443 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435. \u0421\u0435\u0440\u0432\u0435\u0440 \u0441\u0442\u0430\u0440\u0435\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u044D\u0442\u043E \u043E\u0436\u0438\u0434\u0430\u0435\u043C\u043E \u0438 \u043B\u0435\u0447\u0438\u0442\u0441\u044F \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435\u043C \u043F\u0440\u043E\u0434\u0430."
8188
- );
8515
+ if (!answer.ok) {
8516
+ log(describeReposFailure(answer.kind, answer.detail));
8189
8517
  return registry;
8190
8518
  }
8191
8519
  opts.state.unknown = false;
@@ -8199,7 +8527,7 @@ async function refreshRepoMap(opts) {
8199
8527
  }
8200
8528
  log(`[overmind] \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0440\u043E\u044F (${wantedNames.length}), \u043A\u0430\u0440\u0442\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435:`);
8201
8529
  for (const c of opts.state.clones) log(`[overmind] ${describeClone(c)}`);
8202
- const prefixOf = new Map(wanted.map((r) => [r.repoFullName, r.taskPrefix]));
8530
+ const prefixOf = new Map(answer.repos.map((r) => [r.repoFullName, r.taskPrefix]));
8203
8531
  for (const name of opts.state.missing) {
8204
8532
  const prefix = prefixOf.get(name);
8205
8533
  log(`[overmind] ${describeMissing(prefix ? `${name} (\u043F\u0440\u043E\u0435\u043A\u0442 ${prefix})` : name, opts.roots)}`);
@@ -8228,7 +8556,7 @@ function repoMapRows(state) {
8228
8556
  }
8229
8557
  function readRepoMapCache(path) {
8230
8558
  try {
8231
- const raw = JSON.parse(readFileSync8(path, "utf8"));
8559
+ const raw = JSON.parse(readFileSync9(path, "utf8"));
8232
8560
  if (!Array.isArray(raw?.entries)) return [];
8233
8561
  return raw.entries.filter(
8234
8562
  (e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
@@ -8240,7 +8568,7 @@ function readRepoMapCache(path) {
8240
8568
  function writeRepoMapCache(path, entries) {
8241
8569
  try {
8242
8570
  const tmp = `${path}.tmp`;
8243
- writeFileSync7(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
8571
+ writeFileSync8(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
8244
8572
  renameSync5(tmp, path);
8245
8573
  } catch {
8246
8574
  }
@@ -8248,17 +8576,17 @@ function writeRepoMapCache(path, entries) {
8248
8576
 
8249
8577
  // src/hostIdentity.ts
8250
8578
  import { hostname } from "os";
8251
- import { existsSync as existsSync7, readFileSync as readFileSync9, renameSync as renameSync6, writeFileSync as writeFileSync8 } from "fs";
8252
- import { join as join10 } from "path";
8579
+ import { existsSync as existsSync7, readFileSync as readFileSync10, renameSync as renameSync6, writeFileSync as writeFileSync9 } from "fs";
8580
+ import { join as join11 } from "path";
8253
8581
  import { randomUUID as randomUUID2 } from "crypto";
8254
8582
  function hostIdentityPath(home) {
8255
- return join10(home, "host.json");
8583
+ return join11(home, "host.json");
8256
8584
  }
8257
8585
  function loadHostIdentity(deps) {
8258
8586
  const exists = deps.exists ?? existsSync7;
8259
- const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
8587
+ const readFile2 = deps.readFile ?? ((p) => readFileSync10(p, "utf8"));
8260
8588
  const writeFile3 = deps.writeFile ?? ((p, data) => {
8261
- writeFileSync8(`${p}.tmp`, data);
8589
+ writeFileSync9(`${p}.tmp`, data);
8262
8590
  renameSync6(`${p}.tmp`, p);
8263
8591
  });
8264
8592
  const host = deps.hostname ?? hostname;
@@ -8444,12 +8772,12 @@ import { spawn } from "child_process";
8444
8772
  import { closeSync as closeSync2, existsSync as existsSync8, mkdirSync as mkdirSync6, openSync as openSync2, readdirSync as readdirSync7 } from "fs";
8445
8773
  import { createRequire } from "module";
8446
8774
  import { homedir as homedir4 } from "os";
8447
- import { dirname as dirname2, join as join12 } from "path";
8775
+ import { dirname as dirname2, join as join13 } from "path";
8448
8776
  import { fileURLToPath } from "url";
8449
8777
 
8450
8778
  // src/droneHome.ts
8451
8779
  import { lstatSync, mkdirSync as mkdirSync5, readdirSync as readdirSync6, rmSync, statSync as statSync6, symlinkSync as symlinkSync2 } from "fs";
8452
- import { join as join11, relative, sep } from "path";
8780
+ import { join as join12, relative, sep } from "path";
8453
8781
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
8454
8782
  function droneHomeDeny(env = process.env) {
8455
8783
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -8473,20 +8801,20 @@ function buildDroneHome(opts) {
8473
8801
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
8474
8802
  for (const existing of readdirSync6(droneHome)) {
8475
8803
  if (wanted.has(existing)) continue;
8476
- const p = join11(droneHome, existing);
8804
+ const p = join12(droneHome, existing);
8477
8805
  try {
8478
8806
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
8479
8807
  } catch {
8480
8808
  }
8481
8809
  }
8482
8810
  for (const name of wanted) {
8483
- const link = join11(droneHome, name);
8811
+ const link = join12(droneHome, name);
8484
8812
  try {
8485
8813
  lstatSync(link);
8486
8814
  continue;
8487
8815
  } catch {
8488
8816
  }
8489
- const target = join11(realHome, name);
8817
+ const target = join12(realHome, name);
8490
8818
  let isDir;
8491
8819
  try {
8492
8820
  isDir = statSync6(target).isDirectory();
@@ -8507,7 +8835,7 @@ var REAL_PATH_ENV_PINS = [
8507
8835
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
8508
8836
  ];
8509
8837
  function realPathEnvPins(realHome) {
8510
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join11(realHome, ...segments)]));
8838
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join12(realHome, ...segments)]));
8511
8839
  }
8512
8840
  function dronePrivatePaths(realHome, swarmHome) {
8513
8841
  return [
@@ -8605,7 +8933,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
8605
8933
  const env = { ...base3 };
8606
8934
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
8607
8935
  if (isolate && !base3.TURBO_CACHE_DIR) {
8608
- env.TURBO_CACHE_DIR = join12(worktree, ".turbo", "cache");
8936
+ env.TURBO_CACHE_DIR = join13(worktree, ".turbo", "cache");
8609
8937
  }
8610
8938
  Object.assign(env, droneTestEnv(droneCap, base3));
8611
8939
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -8633,16 +8961,16 @@ var DAEMON_STATE_ENV = [
8633
8961
  "TASKLESS_TOKEN"
8634
8962
  ];
8635
8963
  function bundledRoleDir(kind) {
8636
- return join12(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
8964
+ return join13(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
8637
8965
  }
8638
8966
  function roleDir(worktree, kind) {
8639
- return join12(worktree, ".claude", "roles", kind);
8967
+ return join13(worktree, ".claude", "roles", kind);
8640
8968
  }
8641
8969
  function roleDirs(worktree, kind) {
8642
8970
  return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync8(dir));
8643
8971
  }
8644
8972
  function nextRunLogPath(runLogsDir, runId) {
8645
- const dir = join12(runLogsDir, String(runId));
8973
+ const dir = join13(runLogsDir, String(runId));
8646
8974
  mkdirSync6(dir, { recursive: true });
8647
8975
  let files = [];
8648
8976
  try {
@@ -8654,7 +8982,7 @@ function nextRunLogPath(runLogsDir, runId) {
8654
8982
  const m = /^(\d+)\.json/.exec(f);
8655
8983
  if (m) next = Math.max(next, Number(m[1]) + 1);
8656
8984
  }
8657
- return join12(dir, `${next}.json`);
8985
+ return join13(dir, `${next}.json`);
8658
8986
  }
8659
8987
  function openNextRunLog(runLogsDir, runId) {
8660
8988
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -8722,6 +9050,9 @@ function makeLauncher(cfg) {
8722
9050
  SWARM_RUN_NAME: run.name,
8723
9051
  ...run.workspace_id ? { SWARM_WORKSPACE_ID: run.workspace_id } : {},
8724
9052
  ...run.reviews_run_id != null ? { SWARM_REVIEWS_RUN_ID: String(run.reviews_run_id) } : {},
9053
+ // TASWM-344: та же роль, что у SWARM_REVIEWS_RUN_ID выше, для верификатора — без неё
9054
+ // его done() в client-режиме пошёл бы веткой обычной сдачи PR, и вердикт пропал бы.
9055
+ ...run.verifies_run_id != null ? { SWARM_VERIFIES_RUN_ID: String(run.verifies_run_id) } : {},
8725
9056
  ...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
8726
9057
  }
8727
9058
  };
@@ -8734,9 +9065,9 @@ function makeLauncher(cfg) {
8734
9065
  }
8735
9066
  const realHome = cfg.droneIsolation?.realHome ?? homedir4();
8736
9067
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
8737
- if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join12(cfg.droneIsolation.swarmHome, "token"))) {
9068
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join13(cfg.droneIsolation.swarmHome, "token"))) {
8738
9069
  console.error(
8739
- `[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${join12(cfg.droneIsolation.swarmHome, "token")} \u043D\u0435\u0442. read_task \u0434\u0440\u043E\u043D\u0430 \u0431\u0443\u0434\u0435\u0442 slug-only. \u041F\u043E\u0447\u0438\u043D\u0438: \`swarm login\` (oauth.json) \u0438\u043B\u0438 \u043F\u043E\u043B\u043E\u0436\u0438 \u0442\u043E\u043A\u0435\u043D \u0432 \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B.`
9070
+ `[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${join13(cfg.droneIsolation.swarmHome, "token")} \u043D\u0435\u0442. read_task \u0434\u0440\u043E\u043D\u0430 \u0431\u0443\u0434\u0435\u0442 slug-only. \u041F\u043E\u0447\u0438\u043D\u0438: \`swarm login\` (oauth.json) \u0438\u043B\u0438 \u043F\u043E\u043B\u043E\u0436\u0438 \u0442\u043E\u043A\u0435\u043D \u0432 \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B.`
8740
9071
  );
8741
9072
  }
8742
9073
  const executor = cfg.executor ?? resolveExecutor();
@@ -8792,8 +9123,8 @@ init_procCleanup();
8792
9123
 
8793
9124
  // src/cost.ts
8794
9125
  init_executors();
8795
- import { readFileSync as readFileSync10, readdirSync as readdirSync8, renameSync as renameSync7 } from "fs";
8796
- import { join as join13 } from "path";
9126
+ import { readFileSync as readFileSync11, readdirSync as readdirSync8, renameSync as renameSync7 } from "fs";
9127
+ import { join as join14 } from "path";
8797
9128
  var UNPROCESSED_LOG_RE = /^\d+\.json$/;
8798
9129
  function unprocessedLogFiles(dir) {
8799
9130
  try {
@@ -8803,7 +9134,7 @@ function unprocessedLogFiles(dir) {
8803
9134
  }
8804
9135
  }
8805
9136
  function accountRunCost(deps, run) {
8806
- const dir = join13(deps.runLogsDir, String(run.id));
9137
+ const dir = join14(deps.runLogsDir, String(run.id));
8807
9138
  const files = unprocessedLogFiles(dir);
8808
9139
  if (files.length === 0) return;
8809
9140
  const model = resolveRunModel(run, deps.swarmModels?.() ?? null);
@@ -8814,10 +9145,10 @@ function accountRunCost(deps, run) {
8814
9145
  let durationMs = 0;
8815
9146
  const processed = [];
8816
9147
  for (const f of files) {
8817
- const full = join13(dir, f);
9148
+ const full = join14(dir, f);
8818
9149
  let raw = null;
8819
9150
  try {
8820
- raw = readFileSync10(full, "utf8");
9151
+ raw = readFileSync11(full, "utf8");
8821
9152
  } catch {
8822
9153
  continue;
8823
9154
  }
@@ -8854,14 +9185,14 @@ function makeCostAccountant(deps) {
8854
9185
 
8855
9186
  // src/runOutcome.ts
8856
9187
  init_executors();
8857
- import { readdirSync as readdirSync9, readFileSync as readFileSync11 } from "fs";
8858
- import { join as join14 } from "path";
9188
+ import { readdirSync as readdirSync9, readFileSync as readFileSync12 } from "fs";
9189
+ import { join as join15 } from "path";
8859
9190
  function endedItself(outcome) {
8860
9191
  return outcome === "graceful" || outcome === "budget";
8861
9192
  }
8862
9193
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
8863
9194
  function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
8864
- const dir = join14(runLogsDir, String(runId));
9195
+ const dir = join15(runLogsDir, String(runId));
8865
9196
  let files;
8866
9197
  try {
8867
9198
  files = readdirSync9(dir);
@@ -8878,7 +9209,7 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
8878
9209
  if (!latest) return "unknown";
8879
9210
  let raw;
8880
9211
  try {
8881
- raw = readFileSync11(join14(dir, latest.name), "utf8");
9212
+ raw = readFileSync12(join15(dir, latest.name), "utf8");
8882
9213
  } catch {
8883
9214
  return "unknown";
8884
9215
  }
@@ -8896,20 +9227,20 @@ init_src();
8896
9227
  init_git();
8897
9228
 
8898
9229
  // src/deps.ts
8899
- import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync12 } from "fs";
8900
- import { join as join17 } from "path";
9230
+ import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync13 } from "fs";
9231
+ import { join as join18 } from "path";
8901
9232
  import { execa } from "execa";
8902
9233
 
8903
9234
  // src/prepareLock.ts
8904
9235
  import { createHash } from "crypto";
8905
9236
  import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as statSync8, writeSync } from "fs";
8906
- import { join as join16 } from "path";
9237
+ import { join as join17 } from "path";
8907
9238
  import { tmpdir } from "os";
8908
9239
  var WAIT_TIMEOUT_MS = 5 * 6e4;
8909
9240
  var POLL_MS = 300;
8910
9241
  var STALE_MS = 20 * 6e4;
8911
9242
  function machineLockPath(key, dir = tmpdir()) {
8912
- return join16(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
9243
+ return join17(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
8913
9244
  }
8914
9245
  function defaultTryCreate(path, content) {
8915
9246
  try {
@@ -8983,7 +9314,7 @@ async function runCommand(cmd, worktree) {
8983
9314
  }
8984
9315
  function safeReadText(p) {
8985
9316
  try {
8986
- return readFileSync12(p, "utf8");
9317
+ return readFileSync13(p, "utf8");
8987
9318
  } catch {
8988
9319
  return null;
8989
9320
  }
@@ -9025,19 +9356,19 @@ function overridePlan(override) {
9025
9356
  function detectPackageManager(worktree, io = {}) {
9026
9357
  const exists = io.exists ?? existsSync10;
9027
9358
  const readText = io.readText ?? safeReadText;
9028
- const hasPkgJson = exists(join17(worktree, "package.json"));
9359
+ const hasPkgJson = exists(join18(worktree, "package.json"));
9029
9360
  if (!hasPkgJson) {
9030
- if (exists(join17(worktree, "deno.json")) || exists(join17(worktree, "deno.jsonc")) || exists(join17(worktree, "deno.lock"))) return "deno";
9361
+ if (exists(join18(worktree, "deno.json")) || exists(join18(worktree, "deno.jsonc")) || exists(join18(worktree, "deno.lock"))) return "deno";
9031
9362
  return null;
9032
9363
  }
9033
- const pkg = readJson(readText, join17(worktree, "package.json"));
9364
+ const pkg = readJson(readText, join18(worktree, "package.json"));
9034
9365
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
9035
9366
  if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
9036
- if (exists(join17(worktree, "bun.lock")) || exists(join17(worktree, "bun.lockb"))) return "bun";
9037
- if (exists(join17(worktree, "pnpm-lock.yaml"))) return "pnpm";
9038
- if (exists(join17(worktree, "yarn.lock"))) return "yarn";
9039
- if (exists(join17(worktree, "package-lock.json"))) return "npm";
9040
- if (exists(join17(worktree, "deno.lock"))) return "deno";
9367
+ if (exists(join18(worktree, "bun.lock")) || exists(join18(worktree, "bun.lockb"))) return "bun";
9368
+ if (exists(join18(worktree, "pnpm-lock.yaml"))) return "pnpm";
9369
+ if (exists(join18(worktree, "yarn.lock"))) return "yarn";
9370
+ if (exists(join18(worktree, "package-lock.json"))) return "npm";
9371
+ if (exists(join18(worktree, "deno.lock"))) return "deno";
9041
9372
  return "npm";
9042
9373
  }
9043
9374
  function jsInstallCommand(pm) {
@@ -9098,7 +9429,7 @@ function detectPlan(worktree, io) {
9098
9429
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
9099
9430
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
9100
9431
  // заново, но он дешёвый: тянет из того же общего кэша.
9101
- { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join17(worktree, "node_modules"), source: "detect" }
9432
+ { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join18(worktree, "node_modules"), source: "detect" }
9102
9433
  ];
9103
9434
  for (const target of workspaceBuildTargets(worktree, io)) {
9104
9435
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
@@ -9107,12 +9438,12 @@ function detectPlan(worktree, io) {
9107
9438
  return { source: "detect", ecosystem: `node/${pm}`, commands };
9108
9439
  }
9109
9440
  for (const marker of ECOSYSTEM_MARKERS) {
9110
- const hit = marker.files.some((f) => exists(join17(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
9441
+ const hit = marker.files.some((f) => exists(join18(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
9111
9442
  if (!hit) continue;
9112
9443
  let ecosystem = marker.ecosystem;
9113
9444
  let command = marker.command;
9114
9445
  for (const refine of marker.refine ?? []) {
9115
- if (!exists(join17(worktree, refine.file))) continue;
9446
+ if (!exists(join18(worktree, refine.file))) continue;
9116
9447
  ecosystem = refine.ecosystem;
9117
9448
  command = refine.command;
9118
9449
  break;
@@ -9169,12 +9500,12 @@ var CI_INSTALL_PATTERNS = [
9169
9500
  function ciWorkflowPlan(worktree, io) {
9170
9501
  const readText = io.readText ?? safeReadText;
9171
9502
  const listFiles = io.listFiles ?? safeListFiles;
9172
- const dir = join17(worktree, ".github", "workflows");
9503
+ const dir = join18(worktree, ".github", "workflows");
9173
9504
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
9174
9505
  const seen = /* @__PURE__ */ new Set();
9175
9506
  const commands = [];
9176
9507
  for (const name of files.sort()) {
9177
- const yaml = readText(join17(dir, name));
9508
+ const yaml = readText(join18(dir, name));
9178
9509
  if (yaml === null) continue;
9179
9510
  for (const raw of workflowRunLines(yaml)) {
9180
9511
  const line = raw.replace(/\s+/g, " ").trim();
@@ -9189,7 +9520,7 @@ function ciWorkflowPlan(worktree, io) {
9189
9520
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
9190
9521
  }
9191
9522
  function workspaceGlobs(worktree, readText) {
9192
- const yaml = readText(join17(worktree, "pnpm-workspace.yaml"));
9523
+ const yaml = readText(join18(worktree, "pnpm-workspace.yaml"));
9193
9524
  if (yaml !== null) {
9194
9525
  const globs = [];
9195
9526
  let inPackages = false;
@@ -9209,7 +9540,7 @@ function workspaceGlobs(worktree, readText) {
9209
9540
  }
9210
9541
  if (globs.length) return globs;
9211
9542
  }
9212
- const pkg = readJson(readText, join17(worktree, "package.json"));
9543
+ const pkg = readJson(readText, join18(worktree, "package.json"));
9213
9544
  const ws = pkg?.workspaces;
9214
9545
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
9215
9546
  return list.filter((g) => typeof g === "string");
@@ -9220,14 +9551,14 @@ function workspaceDirs(worktree, globs, io) {
9220
9551
  if (glob.startsWith("!")) continue;
9221
9552
  const star = glob.indexOf("*");
9222
9553
  if (star === -1) {
9223
- const dir = join17(worktree, glob);
9224
- if (io.exists(join17(dir, "package.json"))) dirs.add(dir);
9554
+ const dir = join18(worktree, glob);
9555
+ if (io.exists(join18(dir, "package.json"))) dirs.add(dir);
9225
9556
  continue;
9226
9557
  }
9227
- const base3 = join17(worktree, glob.slice(0, star).replace(/\/$/, ""));
9558
+ const base3 = join18(worktree, glob.slice(0, star).replace(/\/$/, ""));
9228
9559
  for (const name of io.listDirs(base3)) {
9229
- const dir = join17(base3, name);
9230
- if (io.exists(join17(dir, "package.json"))) dirs.add(dir);
9560
+ const dir = join18(base3, name);
9561
+ if (io.exists(join18(dir, "package.json"))) dirs.add(dir);
9231
9562
  }
9232
9563
  }
9233
9564
  return [...dirs];
@@ -9255,11 +9586,11 @@ function workspaceBuildTargets(worktree, io = {}) {
9255
9586
  if (!globs.length) return [];
9256
9587
  const pkgs = [];
9257
9588
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
9258
- const pkg = readJson(readText, join17(dir, "package.json"));
9589
+ const pkg = readJson(readText, join18(dir, "package.json"));
9259
9590
  if (!pkg || typeof pkg.name !== "string") continue;
9260
9591
  const scripts = pkg.scripts ?? {};
9261
9592
  const entries = entryPaths(pkg);
9262
- const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join17(dir, e)));
9593
+ const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join18(dir, e)));
9263
9594
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
9264
9595
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
9265
9596
  }
@@ -9287,7 +9618,7 @@ function devDatabaseUrl(worktree, io = {}) {
9287
9618
  const fromEnv = (io.env ?? process.env).DATABASE_URL;
9288
9619
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
9289
9620
  for (const file of [".env", ".env.example"]) {
9290
- const raw = readText(join17(worktree, file));
9621
+ const raw = readText(join18(worktree, file));
9291
9622
  if (raw === null) continue;
9292
9623
  const m = /^\s*(?:export\s+)?DATABASE_URL\s*=\s*(.*)$/m.exec(raw);
9293
9624
  const value = (m?.[1] ?? "").trim().replace(/^['"]|['"]$/g, "");
@@ -9307,7 +9638,7 @@ function prismaPackages(worktree, io) {
9307
9638
  const readText = io.readText ?? safeReadText;
9308
9639
  const listDirs = io.listDirs ?? safeListDirs;
9309
9640
  const dirs = [worktree, ...workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs })];
9310
- return dirs.filter((dir) => exists(join17(dir, "prisma", "schema.prisma")) && listDirs(join17(dir, "prisma", "migrations")).length > 0);
9641
+ return dirs.filter((dir) => exists(join18(dir, "prisma", "schema.prisma")) && listDirs(join18(dir, "prisma", "migrations")).length > 0);
9311
9642
  }
9312
9643
  function jsExecBin(pm, command) {
9313
9644
  switch (pm) {
@@ -9508,6 +9839,23 @@ ${notice}` : built;
9508
9839
  });
9509
9840
  }
9510
9841
 
9842
+ // src/spawnDefer.ts
9843
+ function noteDefer(episodes, key, reason, heartbeatTicks) {
9844
+ const prev = episodes.get(key);
9845
+ const ticks = (prev?.ticks ?? 0) + 1;
9846
+ const first = prev === void 0;
9847
+ const changed = first || reason !== prev.reason;
9848
+ const beat = !changed && heartbeatTicks > 0 && ticks % heartbeatTicks === 0;
9849
+ episodes.set(key, { ticks, reason });
9850
+ return { log: changed || beat, changed, ticks };
9851
+ }
9852
+ function clearDefer(episodes, key) {
9853
+ return episodes.delete(key);
9854
+ }
9855
+ function pruneDefers(episodes, live) {
9856
+ for (const key of episodes.keys()) if (!live.has(key)) episodes.delete(key);
9857
+ }
9858
+
9511
9859
  // src/overmind.ts
9512
9860
  init_git();
9513
9861
 
@@ -9596,6 +9944,13 @@ function verdictLabel(verdict) {
9596
9944
  return verdict === "approve" ? "\u2705 APPROVE" : "\u274C REQUEST CHANGES";
9597
9945
  }
9598
9946
  async function latestVerdictComment(prUrl, sinceIso, run = realCommentsRunner) {
9947
+ return scanPrComments(prUrl, sinceIso, run, (body) => {
9948
+ const m = body.match(VERDICT_RE);
9949
+ if (!m) return null;
9950
+ return APPROVE_RE.test(m[1]) ? "approve" : "changes";
9951
+ });
9952
+ }
9953
+ async function scanPrComments(prUrl, sinceIso, run, pick3) {
9599
9954
  if (!isGithubPrUrl(prUrl)) return null;
9600
9955
  let out;
9601
9956
  try {
@@ -9615,13 +9970,15 @@ async function latestVerdictComment(prUrl, sinceIso, run = realCommentsRunner) {
9615
9970
  const c = comments[i];
9616
9971
  if (!c?.body || !c.createdAt) continue;
9617
9972
  if (Number.isFinite(sinceMs) && Date.parse(c.createdAt) < sinceMs) continue;
9618
- const m = c.body.match(VERDICT_RE);
9619
- if (!m) continue;
9620
- const verdict = APPROVE_RE.test(m[1]) ? "approve" : "changes";
9973
+ const verdict = pick3(c.body);
9974
+ if (verdict == null) continue;
9621
9975
  return { verdict, createdAt: c.createdAt };
9622
9976
  }
9623
9977
  return null;
9624
9978
  }
9979
+ async function latestVerifyVerdictComment(prUrl, sinceIso, run = realCommentsRunner) {
9980
+ return scanPrComments(prUrl, sinceIso, run, (body) => parseVerifyVerdictComment(body));
9981
+ }
9625
9982
  var realPrCommitsRunner = async (prUrl) => (await $3`gh pr view ${prUrl} --json commits`).stdout;
9626
9983
  async function prHasCommitsSince(prUrl, sinceIso, run = realPrCommitsRunner) {
9627
9984
  if (!isGithubPrUrl(prUrl)) return false;
@@ -9762,6 +10119,7 @@ init_attachments();
9762
10119
  init_shotUpload();
9763
10120
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9764
10121
  import { z } from "zod";
10122
+ init_src();
9765
10123
 
9766
10124
  // src/reviewCycle.ts
9767
10125
  function reviewTransitionPatch(run, prUrl) {
@@ -9835,6 +10193,7 @@ async function buildDroneServer(store, opts = {}) {
9835
10193
  const overmindDead = async () => !await store.alive();
9836
10194
  const slugForInstruction = (fallback) => opts.expectedTaskSlug ?? fallback ?? "<slug>";
9837
10195
  const iAmReviewer = (run) => run?.reviews_run_id != null || run?.kind === "reviewer" || opts.reviewsRunId != null || opts.runKind === "reviewer";
10196
+ const iAmVerifier = (run) => run?.verifies_run_id != null || run?.kind === "verifier" || opts.verifiesRunId != null || opts.runKind === "verifier";
9838
10197
  const freeze = async (withdraw = false) => {
9839
10198
  let slug;
9840
10199
  let branch;
@@ -10123,7 +10482,9 @@ ${notFoundText}` : notFoundText }],
10123
10482
  "\u0420\u0430\u0431\u043E\u0442\u0430 \u0441\u0434\u0430\u043D\u0430. \u0421 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C GitHub-PR (github.com/OWNER/REPO/pull/N) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0443\u0445\u043E\u0434\u0438\u0442 \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u0440\u043E\u0439 \u0434\u043E\u043C\u0435\u0440\u0436\u0438\u0442. \u0411\u0435\u0437 \u0441\u0432\u043E\u0435\u0433\u043E PR (\u0440\u0435\u0432\u044C\u044E/meta-\u0437\u0430\u0434\u0430\u0447\u0430) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u0442\u0441\u044F \u0441\u0440\u0430\u0437\u0443; \u043C\u043E\u0436\u0435\u0448\u044C \u043F\u0435\u0440\u0435\u0434\u0430\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443. summary \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u0438 \u0443 \u0418\u0421\u041F\u041E\u041B\u041D\u0418\u0422\u0415\u041B\u042F, \u0438 \u0443 \u0420\u0415\u0412\u042C\u042E\u0415\u0420\u0410 \u2014 \u043E\u043D \u0443\u0445\u043E\u0434\u0438\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 (TASWM-259). \u0414\u0440\u043E\u043D-\u0420\u0415\u0412\u042C\u042E\u0415\u0420 (\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E, TASWM-148) \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u043E\u0431\u044F\u0437\u0430\u043D \u043F\u0435\u0440\u0435\u0434\u0430\u0442\u044C verdict \u2014 pr_url \u0442\u043E\u0433\u0434\u0430 \u0442\u0430 \u0436\u0435 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 PR.",
10124
10483
  {
10125
10484
  pr_url: z.string().describe("\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0439 GitHub-PR; \u0434\u043B\u044F \u0437\u0430\u0434\u0430\u0447 \u0431\u0435\u0437 PR \u2014 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0440\u043E\u0441\u0442\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044F)"),
10126
- verdict: z.enum(["approve", "changes"]).optional().describe("\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0434\u0440\u043E\u043D\u0430-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u0438\u0442\u043E\u0433 \u043F\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u043E\u043C\u0443 PR \u2014 approve (APPROVE) \u0438\u043B\u0438 changes (REQUEST CHANGES). \u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u0434\u043B\u044F \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430."),
10485
+ verdict: z.enum(["approve", "changes", ...VERIFY_VERDICTS]).optional().describe(
10486
+ "\u0412\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044E\u0449\u0435\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430. \u0420\u0435\u0432\u044C\u044E\u0435\u0440 (\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E): approve | changes. \u0412\u0415\u0420\u0418\u0424\u0418\u041A\u0410\u0422\u041E\u0420 (TASWM-344): confirmed (\u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044E) | not_reproduced (\u043D\u0435 \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442\u0441\u044F) | found_issues (\u043D\u0430\u0448\u0451\u043B \u0432\u043E\u0442 \u044D\u0442\u043E). \u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u0434\u043B\u044F \u043E\u0431\u043E\u0438\u0445."
10487
+ ),
10127
10488
  summary: z.string().optional().describe(
10128
10489
  "\u041E\u0442\u0447\u0451\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443, \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u043F\u0440\u0438 \u0441\u0434\u0430\u0447\u0435 \u0441 PR. \u0418\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C: \u0447\u0442\u043E \u0441\u0434\u0435\u043B\u0430\u043D\u043E, \u043A\u0430\u043A\u0438\u0435 \u0444\u0430\u0439\u043B\u044B \u0437\u0430\u0442\u0440\u043E\u043D\u0443\u0442\u044B, \u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u0448\u0435\u043D\u0438\u044F \u043F\u0440\u0438\u043D\u044F\u043B \u0438 \u0447\u0442\u043E \u043E\u0441\u0442\u0430\u043B\u043E\u0441\u044C \u0437\u0430 \u0441\u043A\u043E\u0431\u043A\u0430\u043C\u0438. \u0420\u0435\u0432\u044C\u044E\u0435\u0440: \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u0447\u0442\u043E \u043D\u0430\u0448\u0451\u043B."
10129
10490
  ),
@@ -10226,6 +10587,66 @@ ${notFoundText}` : notFoundText }],
10226
10587
  \u26A0\uFE0F \u0412\u0435\u0440\u0434\u0438\u043A\u0442-\u043A\u043E\u043C\u043C\u0435\u043D\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 \u041D\u0415 \u0443\u0435\u0445\u0430\u043B (${commentWarning}) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0435\u0441\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u043D\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0435. \u041F\u0440\u043E\u0434\u0443\u0431\u043B\u0438\u0440\u0443\u0439 \u0435\u0433\u043E \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 PR: \`gh pr comment ${trustedPrUrl} --body "${verdictText} \u2026"\`.` : "";
10227
10588
  return { content: [{ type: "text", text: `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0437\u0430\u043F\u0438\u0441\u0430\u043D, \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E${shotsNote(shots)}${commentNote}` }] };
10228
10589
  }
10590
+ if (iAmVerifier(run) && run?.verifies_run_id == null) {
10591
+ return {
10592
+ content: [
10593
+ {
10594
+ type: "text",
10595
+ text: '\u0422\u044B \u0434\u0440\u043E\u043D-\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440, \u043D\u043E \u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u0441\u0432\u044F\u0437\u044C \u0441 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u043C \u2014 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u043A\u0443\u0434\u0430, \u0438 \u0437\u0430\u043A\u0440\u044B\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u043B\u044C\u0437\u044F (TASWM-344). \u041E\u0441\u0442\u0430\u0432\u044C \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 PR (`gh pr comment <pr> --body "**\u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u042E**|**\u041D\u0415 \u0412\u041E\u0421\u041F\u0420\u041E\u0418\u0417\u0412\u041E\u0414\u0418\u0422\u0421\u042F**|**\u041D\u0410\u0428\u0401\u041B** \u2026"`) \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438 done() \u0447\u0435\u0440\u0435\u0437 \u043C\u0438\u043D\u0443\u0442\u0443.'
10596
+ }
10597
+ ],
10598
+ isError: true
10599
+ };
10600
+ }
10601
+ if (run?.verifies_run_id != null) {
10602
+ if (isTerminalStatus(run.status)) {
10603
+ return { content: [{ type: "text", text: "\u043F\u0440\u043E\u0433\u043E\u043D \u0443\u0436\u0435 \u0437\u0430\u043A\u0440\u044B\u0442 (PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D" }] };
10604
+ }
10605
+ if (!isVerifyVerdict(verdict)) {
10606
+ return {
10607
+ content: [
10608
+ {
10609
+ type: "text",
10610
+ text: '\u0422\u044B \u0434\u0440\u043E\u043D-\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 (TASWM-344) \u2014 done() \u0442\u0440\u0435\u0431\u0443\u0435\u0442 verdict: "confirmed" (\u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044E) | "not_reproduced" (\u043D\u0435 \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442\u0441\u044F) | "found_issues" (\u043D\u0430\u0448\u0451\u043B \u0432\u043E\u0442 \u044D\u0442\u043E). \u0427\u0435\u0442\u0432\u0451\u0440\u0442\u043E\u0433\u043E \u0438\u0441\u0445\u043E\u0434\u0430 \u043D\u0435\u0442, \u0438 \u0443\u0441\u043F\u0435\u0445\u043E\u043C \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u0442\u0441\u044F: \u043F\u043E\u043A\u0430 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F \u043F\u0440\u043E\u0448\u0435\u0434\u0448\u0435\u0439 \u043F\u0440\u0438\u0451\u043C\u043A\u0443. \u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u043E\u0432\u0441\u0435 \u2014 \u044D\u0442\u043E "not_reproduced" \u0441 \u0447\u0435\u0441\u0442\u043D\u043E\u0439 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439 \u0432 summary, \u0430 \u043D\u0435 \u043C\u043E\u043B\u0447\u0430\u043D\u0438\u0435.'
10611
+ }
10612
+ ],
10613
+ isError: true
10614
+ };
10615
+ }
10616
+ const verified = await store.getRun(run.verifies_run_id);
10617
+ const verdictText = verifyVerdictLabel(verdict);
10618
+ await store.updateRun(run.verifies_run_id, { verify_verdict: verdict });
10619
+ await store.addEvent(run.verifies_run_id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u043E\u0442 ${run.name}: ${verdictText}${summary ? ` \u2014 ${summary}` : ""}`);
10620
+ const verifiedSlug = reportSlug(verified?.task_slug);
10621
+ let shots = { attachments: [], warning: "" };
10622
+ let commentWarning = "";
10623
+ if (opts.taskless && verifiedSlug) {
10624
+ shots = await uploadShots();
10625
+ try {
10626
+ await opts.taskless.addComment(
10627
+ verifiedSlug,
10628
+ `\u{1F9EA} \u0412\u0415\u0420\u0414\u0418\u041A\u0422 \u0412\u0415\u0420\u0418\u0424\u0418\u041A\u0410\u0422\u041E\u0420\u0410 ${run.name}: **${verdictText}**
10629
+
10630
+ ${(summary ?? "").trim()}${shots.warning}`,
10631
+ void 0,
10632
+ shots.attachments
10633
+ );
10634
+ } catch (e) {
10635
+ commentWarning = e instanceof Error ? e.message : String(e);
10636
+ await store.addEvent(run.verifies_run_id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442-\u043A\u043E\u043C\u043C\u0435\u043D\u0442 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 ${verifiedSlug} \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${commentWarning}`);
10637
+ }
10638
+ } else {
10639
+ commentWarning = opts.taskless ? "\u0437\u0430\u0434\u0430\u0447\u0430 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430" : "\u043A\u043B\u0438\u0435\u043D\u0442 Taskless \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D";
10640
+ await store.addEvent(run.verifies_run_id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442-\u043A\u043E\u043C\u043C\u0435\u043D\u0442 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${commentWarning}`);
10641
+ }
10642
+ const trustedPrUrl = verified?.pr_url ?? pr_url;
10643
+ await store.updateRun(null, { status: "review", pr_url: trustedPrUrl });
10644
+ await store.addEvent(null, "pr_opened", trustedPrUrl);
10645
+ const commentNote = commentWarning ? `
10646
+
10647
+ \u26A0\uFE0F \u0412\u0435\u0440\u0434\u0438\u043A\u0442-\u043A\u043E\u043C\u043C\u0435\u043D\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 \u041D\u0415 \u0443\u0435\u0445\u0430\u043B (${commentWarning}) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0435\u0441\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u043D\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0435. \u041F\u0440\u043E\u0434\u0443\u0431\u043B\u0438\u0440\u0443\u0439 \u0435\u0433\u043E \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 PR: \`gh pr comment ${trustedPrUrl} --body "${verdictText} \u2026"\`.` : "";
10648
+ return { content: [{ type: "text", text: `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u0437\u0430\u043F\u0438\u0441\u0430\u043D (${verdictText})${shotsNote(shots)}${commentNote}` }] };
10649
+ }
10229
10650
  if (isGithubPrUrl(pr_url)) {
10230
10651
  const report = (summary ?? "").trim();
10231
10652
  if (!report) {
@@ -10434,6 +10855,8 @@ function ensureRunHydrated(db, runId, info) {
10434
10855
  kind: info.kind,
10435
10856
  name: info.name,
10436
10857
  reviews_run_id: info.reviews_run_id,
10858
+ verifies_run_id: info.verifies_run_id,
10859
+ // TASWM-344
10437
10860
  hero_run_id: info.hero_run_id
10438
10861
  });
10439
10862
  }
@@ -10447,12 +10870,14 @@ async function runDroneServer(runId) {
10447
10870
  const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
10448
10871
  const expectedKind = toRunKind(process.env.SWARM_RUN_KIND);
10449
10872
  const spawnReviewsRunId = Number(process.env.SWARM_REVIEWS_RUN_ID);
10873
+ const spawnVerifiesRunId = Number(process.env.SWARM_VERIFIES_RUN_ID);
10450
10874
  const common = {
10451
10875
  taskless,
10452
10876
  runKind: expectedKind,
10453
10877
  prHeadBranch,
10454
10878
  ...expectedTaskSlug ? { expectedTaskSlug } : {},
10455
- ...Number.isInteger(spawnReviewsRunId) && spawnReviewsRunId > 0 ? { reviewsRunId: spawnReviewsRunId } : {}
10879
+ ...Number.isInteger(spawnReviewsRunId) && spawnReviewsRunId > 0 ? { reviewsRunId: spawnReviewsRunId } : {},
10880
+ ...Number.isInteger(spawnVerifiesRunId) && spawnVerifiesRunId > 0 ? { verifiesRunId: spawnVerifiesRunId } : {}
10456
10881
  };
10457
10882
  const channelUrl = process.env.SWARM_DRONE_CHANNEL_URL;
10458
10883
  const channelToken = process.env.SWARM_DRONE_CHANNEL_TOKEN;
@@ -10473,6 +10898,7 @@ async function runDroneServer(runId) {
10473
10898
  kind: expectedKind,
10474
10899
  name: process.env.SWARM_RUN_NAME || void 0,
10475
10900
  reviews_run_id: process.env.SWARM_REVIEWS_RUN_ID ? Number(process.env.SWARM_REVIEWS_RUN_ID) : null,
10901
+ verifies_run_id: process.env.SWARM_VERIFIES_RUN_ID ? Number(process.env.SWARM_VERIFIES_RUN_ID) : null,
10476
10902
  hero_run_id: process.env.SWARM_HERO_RUN_ID ? Number(process.env.SWARM_HERO_RUN_ID) : null
10477
10903
  });
10478
10904
  const store = localDroneStore2(db, runId, { heartbeatPath: HEARTBEAT_PATH2, heartbeatStaleMs: HEARTBEAT_STALE_MS2 });
@@ -10668,6 +11094,9 @@ function joinCapped(labels) {
10668
11094
  joined = labels.slice(0, Math.max(cut, 1)).join(", ");
10669
11095
  return `${joined} \u0438 \u0435\u0449\u0451 ${labels.length - Math.max(cut, 1)}`;
10670
11096
  }
11097
+ function autoReviewOnFor(flag, repo) {
11098
+ return typeof flag === "function" ? flag(repo) : flag;
11099
+ }
10671
11100
  function buildAnomalyCandidates(db, now, thresholds, opts) {
10672
11101
  const candidates = [];
10673
11102
  const allRuns = db.listRuns().filter((r) => r.superseded_by == null);
@@ -10802,7 +11231,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
10802
11231
  ...hrefOf(run.task_url)
10803
11232
  })
10804
11233
  });
10805
- } else if (opts.autoReviewEnabled && run.review_verdict == null && db.reviewerCountFor(run.id) === 0) {
11234
+ } else if (autoReviewOnFor(opts.autoReviewEnabled, run.repo) && run.review_verdict == null && db.reviewerCountFor(run.id) === 0) {
10806
11235
  candidates.push({
10807
11236
  key: `pr_no_reviewer:${run.id}`,
10808
11237
  minDurationMs: thresholds.prNoReviewerMs,
@@ -10989,9 +11418,11 @@ init_droneChannel();
10989
11418
  init_procList();
10990
11419
 
10991
11420
  // src/mcpResilient.ts
11421
+ init_linkFailure();
10992
11422
  init_taskless();
10993
11423
  var DEFAULT_BACKOFF_MS = [5e3, 15e3, 3e4, 6e4];
10994
11424
  var DEFAULT_DOWN_AFTER_MS = 5 * 6e4;
11425
+ var DEFAULT_RETRY_DELAYS_MS = [200, 800];
10995
11426
  function errorResult(reason) {
10996
11427
  return { isError: true, content: [{ type: "text", text: `Swarm MCP \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: ${reason}` }] };
10997
11428
  }
@@ -10999,11 +11430,16 @@ function resilientSwarmMcp(opts = {}) {
10999
11430
  const connect = opts.connect ?? (() => connectSwarmMcp());
11000
11431
  const backoff = opts.backoffMs?.length ? opts.backoffMs : DEFAULT_BACKOFF_MS;
11001
11432
  const downAfterMs = opts.downAfterMs ?? DEFAULT_DOWN_AFTER_MS;
11433
+ const retryDelays = opts.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
11434
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
11002
11435
  let caller = null;
11003
11436
  let lastTryAt = 0;
11004
11437
  let failures = 0;
11005
11438
  let downSince = null;
11006
11439
  let downReported = false;
11440
+ let lastConnectReason = null;
11441
+ let lastOkAt = null;
11442
+ let lastCallReason = null;
11007
11443
  function maybeReportDown() {
11008
11444
  if (downReported || downSince === null) return;
11009
11445
  const downMs = Date.now() - downSince;
@@ -11011,45 +11447,65 @@ function resilientSwarmMcp(opts = {}) {
11011
11447
  downReported = true;
11012
11448
  opts.onDown?.(downMs);
11013
11449
  }
11014
- async function ensureCaller() {
11450
+ async function ensureCaller(force = false) {
11015
11451
  if (caller) return caller;
11016
11452
  const now = Date.now();
11017
11453
  const wait = backoff[Math.max(0, Math.min(failures - 1, backoff.length - 1))];
11018
- if (lastTryAt !== 0 && now - lastTryAt < wait) return null;
11019
- lastTryAt = now;
11454
+ if (!force && lastTryAt !== 0 && now - lastTryAt < wait) return null;
11455
+ if (!force) lastTryAt = now;
11020
11456
  let fresh = null;
11021
11457
  try {
11022
11458
  fresh = await connect();
11023
- } catch {
11459
+ if (!fresh) lastConnectReason = lastSwarmConnectFailure() ?? UNKNOWN_LINK_REASON;
11460
+ } catch (e) {
11024
11461
  fresh = null;
11462
+ lastConnectReason = describeError(e);
11025
11463
  }
11026
11464
  if (fresh) {
11027
11465
  caller = fresh;
11028
11466
  failures = 0;
11467
+ lastConnectReason = null;
11029
11468
  if (downReported) opts.onUp?.();
11030
11469
  downSince = null;
11031
11470
  downReported = false;
11032
11471
  return caller;
11033
11472
  }
11034
- failures += 1;
11473
+ if (!force) failures += 1;
11035
11474
  if (downSince === null) downSince = now;
11036
11475
  return null;
11037
11476
  }
11038
- return async (name, args) => {
11039
- const active = await ensureCaller();
11040
- if (!active) {
11041
- maybeReportDown();
11042
- return errorResult("\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E");
11043
- }
11044
- try {
11045
- return await active(name, args);
11046
- } catch (e) {
11047
- caller = null;
11048
- if (downSince === null) downSince = Date.now();
11049
- maybeReportDown();
11050
- return errorResult(e instanceof Error ? e.message : String(e));
11477
+ const call = async (name, args) => {
11478
+ for (let attempt = 0; ; attempt++) {
11479
+ const active = await ensureCaller(attempt > 0);
11480
+ if (!active) {
11481
+ maybeReportDown();
11482
+ return errorResult(lastConnectReason ?? UNKNOWN_LINK_REASON);
11483
+ }
11484
+ try {
11485
+ const res = await active(name, args);
11486
+ lastOkAt = Date.now();
11487
+ lastCallReason = null;
11488
+ return res;
11489
+ } catch (e) {
11490
+ caller = null;
11491
+ if (downSince === null) downSince = Date.now();
11492
+ const { kind, detail: reason } = explainLinkFailure(e);
11493
+ lastCallReason = reason;
11494
+ const delay = retryDelays[attempt];
11495
+ if (delay === void 0 || kind !== "offline") {
11496
+ maybeReportDown();
11497
+ return errorResult(reason);
11498
+ }
11499
+ opts.onRetry?.({ tool: name, attempt: attempt + 1, reason });
11500
+ await sleep2(delay);
11501
+ }
11051
11502
  }
11052
11503
  };
11504
+ return Object.assign(call, {
11505
+ // Причина — ТА ЖЕ, что уехала в лог: сначала свежий отказ вызова, иначе отказ
11506
+ // подключения. Своего разбора здесь нет намеренно (см. lastCallReason выше).
11507
+ link: () => ({ connected: caller !== null, downSince, lastOkAt, lastError: caller !== null ? null : lastCallReason ?? lastConnectReason })
11508
+ });
11053
11509
  }
11054
11510
 
11055
11511
  // src/overmind.ts
@@ -11085,6 +11541,73 @@ function workspaceAccessLines(input) {
11085
11541
  return lines;
11086
11542
  }
11087
11543
 
11544
+ // src/overmind.ts
11545
+ init_authState();
11546
+
11547
+ // src/swarmStatus.ts
11548
+ init_linkFailure();
11549
+
11550
+ // src/release.ts
11551
+ init_src();
11552
+ init_claudeBin();
11553
+ var SWARM_PACKAGE = SWARM_PACKAGE_NAME;
11554
+ var LATEST_TIMEOUT_MS = 4e3;
11555
+ async function fetchLatestVersion(opts = {}) {
11556
+ const fetchFn = opts.fetchFn ?? fetch;
11557
+ const timeoutMs = opts.timeoutMs ?? LATEST_TIMEOUT_MS;
11558
+ const pkg = opts.pkg ?? SWARM_PACKAGE;
11559
+ const ctrl = new AbortController();
11560
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
11561
+ try {
11562
+ const res = await fetchFn(`https://registry.npmjs.org/${pkg}/latest`, {
11563
+ signal: ctrl.signal,
11564
+ headers: { accept: "application/vnd.npm.install-v1+json, application/json" }
11565
+ });
11566
+ if (!res.ok) return null;
11567
+ const body = await res.json();
11568
+ return typeof body.version === "string" && body.version ? body.version : null;
11569
+ } catch {
11570
+ return null;
11571
+ } finally {
11572
+ clearTimeout(timer);
11573
+ }
11574
+ }
11575
+ function isOutdated(current, latest) {
11576
+ if (!latest || !current) return false;
11577
+ return compareVersionDirs(latest, current) > 0;
11578
+ }
11579
+ function outdatedLine(current, latest, updateCommand = "swarm update") {
11580
+ if (!isOutdated(current, latest)) return null;
11581
+ return `\u26A0 \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u043E\u044F \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0430: \u0443 \u0442\u0435\u0431\u044F v${current}, \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest} \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: ${updateCommand}`;
11582
+ }
11583
+
11584
+ // src/swarmStatus.ts
11585
+ init_src();
11586
+ function classifyLinkError(e) {
11587
+ const { kind, detail } = explainLinkFailure(e);
11588
+ const short2 = detail.replace(/\s+/g, " ").trim().slice(0, 200);
11589
+ const map = {
11590
+ unauthorized: "revoked",
11591
+ missing_tool: "server",
11592
+ offline: "network",
11593
+ rejected: "server",
11594
+ unknown: "down"
11595
+ };
11596
+ return { kind: map[kind], detail: short2 };
11597
+ }
11598
+ function composeLink(input) {
11599
+ const base3 = { lastOkAt: input.transport?.lastOkAt != null ? new Date(input.transport.lastOkAt).toISOString() : null, pending: input.pending, api: input.api };
11600
+ if (input.authDenied) return { kind: "revoked", detail: input.authDenied, ...base3 };
11601
+ if (!input.transport) return { kind: "no-prod", detail: "sqlite-\u0440\u0435\u0436\u0438\u043C \u2014 \u043F\u0440\u043E\u0434\u0430 \u0443 \u044D\u0442\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442 \u0432\u043E\u0432\u0441\u0435", ...base3 };
11602
+ if (input.transport.connected) return { kind: "ok", detail: "", ...base3 };
11603
+ if (input.transport.lastOkAt == null && input.transport.downSince == null && !input.transport.lastError) {
11604
+ return { kind: "starting", detail: "\u0434\u0435\u043C\u043E\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F \u2014 \u0432 \u043F\u0440\u043E\u0434 \u0435\u0449\u0451 \u043D\u0435 \u0445\u043E\u0434\u0438\u043B", ...base3 };
11605
+ }
11606
+ const why = input.transport.lastError;
11607
+ if (!why) return { kind: "down", detail: "", ...base3 };
11608
+ return { ...classifyLinkError(new Error(why)), ...base3 };
11609
+ }
11610
+
11088
11611
  // src/overmind.ts
11089
11612
  init_workspace();
11090
11613
  init_attachments();
@@ -11219,10 +11742,23 @@ function makeMergeGateResolver(deps) {
11219
11742
  const run = findRunByPr(deps.listRuns(), ref);
11220
11743
  if (!run) return { ...computeMergeGate({ hasRun: false, autoReview: false, reviewVerdict: null }), run: null };
11221
11744
  const autoReview = deps.autoReview ? await deps.autoReview(run.repo).catch(() => deps.fallbackAutoReview) : deps.fallbackAutoReview;
11222
- const decision = computeMergeGate({ hasRun: true, autoReview, reviewVerdict: run.review_verdict });
11745
+ const decision = computeMergeGate({
11746
+ hasRun: true,
11747
+ autoReview,
11748
+ reviewVerdict: run.review_verdict,
11749
+ autoVerify: deps.autoVerify === true,
11750
+ verifyVerdict: run.verify_verdict ?? null
11751
+ });
11223
11752
  return {
11224
11753
  ...decision,
11225
- run: { id: run.id, task_slug: run.task_slug, name: run.name, pr_url: run.pr_url, review_verdict: run.review_verdict }
11754
+ run: {
11755
+ id: run.id,
11756
+ task_slug: run.task_slug,
11757
+ name: run.name,
11758
+ pr_url: run.pr_url,
11759
+ review_verdict: run.review_verdict,
11760
+ verify_verdict: run.verify_verdict ?? null
11761
+ }
11226
11762
  };
11227
11763
  };
11228
11764
  }
@@ -11665,7 +12201,7 @@ async function changedPaths(worktree, opts = {}) {
11665
12201
  // package.json
11666
12202
  var package_default = {
11667
12203
  name: "@taskless-app/swarm",
11668
- version: "0.6.11",
12204
+ version: "0.6.13",
11669
12205
  type: "module",
11670
12206
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
11671
12207
  keywords: [
@@ -11715,7 +12251,7 @@ var package_default = {
11715
12251
  execa: "^10.0.0",
11716
12252
  zod: "3.25.76"
11717
12253
  },
11718
- gitHead: "f228c3fd9ac879e828e5b813bf8ff077402fbe23"
12254
+ gitHead: "ea3913bf66c52d730c25ad62b7090621a5006ee7"
11719
12255
  };
11720
12256
 
11721
12257
  // src/version.ts
@@ -11727,6 +12263,7 @@ init_storm();
11727
12263
  init_resources();
11728
12264
  init_config();
11729
12265
  init_droneChannel();
12266
+ init_authState();
11730
12267
  init_oauth();
11731
12268
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
11732
12269
  var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
@@ -11735,6 +12272,15 @@ var sqliteIso = (ts) => {
11735
12272
  const ms = sqliteMs(ts);
11736
12273
  return Number.isNaN(ms) ? ts : new Date(ms).toISOString();
11737
12274
  };
12275
+ function stormFacts(db) {
12276
+ if (!db.stormActive()) return null;
12277
+ const since = db.currentStormStartedAt();
12278
+ const recs = since ? db.stormProbeRecordsSince(since) : [];
12279
+ return {
12280
+ reason: lastProbeReason(recs) === "limit" ? "limit" : "provider",
12281
+ hint: recs.length ? recs[recs.length - 1].resetHint ?? null : null
12282
+ };
12283
+ }
11738
12284
  function roySnapshot(db, now = Date.now(), overmindLog = [], paused = false, daemonVersion = null, restart = null, selfHost = null) {
11739
12285
  const all = db.listRuns();
11740
12286
  const c = (s) => all.filter((r) => r.status === s).length;
@@ -11744,11 +12290,11 @@ function roySnapshot(db, now = Date.now(), overmindLog = [], paused = false, dae
11744
12290
  if (r.status === "done" || r.status === "stopped" || r.status === "failed") return sqliteMs(r.updated_at) >= now - DAY_MS;
11745
12291
  return true;
11746
12292
  });
11747
- const stormActive = db.stormActive();
12293
+ const storm = stormFacts(db);
12294
+ const stormActive = storm !== null;
11748
12295
  const stormSince = db.currentStormStartedAt();
11749
- const stormRecs = stormActive && stormSince ? db.stormProbeRecordsSince(stormSince) : [];
11750
- const stormReason = stormActive ? lastProbeReason(stormRecs) === "limit" ? "limit" : "provider" : null;
11751
- const stormResetHint = stormRecs.length ? stormRecs[stormRecs.length - 1].resetHint ?? null : null;
12296
+ const stormReason = storm?.reason ?? null;
12297
+ const stormResetHint = storm?.hint ?? null;
11752
12298
  return {
11753
12299
  metrics: {
11754
12300
  running: c("running"),
@@ -13031,7 +13577,17 @@ function daemonStatus(deps) {
13031
13577
  const counts = {};
13032
13578
  for (const r of deps.db.listRuns()) counts[r.status] = (counts[r.status] ?? 0) + 1;
13033
13579
  const live = deps.liveSessionIds?.() ?? [];
13580
+ const ver = deps.daemonVersion?.() ?? null;
13581
+ const queuedReady = deps.db.listRuns("queued").filter((r) => deps.db.unmetDeps(r.depends_on).length === 0).length;
13034
13582
  return {
13583
+ api: deps.api ?? TASKLESS_API,
13584
+ queuedReady,
13585
+ startedAt: ver?.startedAt ?? null,
13586
+ latestVersion: ver?.latestVersion ?? null,
13587
+ link: deps.link?.() ?? null,
13588
+ questions: deps.db.openQuestions().length,
13589
+ maxDrones: deps.maxDrones?.() ?? null,
13590
+ storm: stormFacts(deps.db),
13035
13591
  hostId: deps.selfHost?.hostId ?? null,
13036
13592
  hostName: deps.selfHost?.hostName ?? null,
13037
13593
  backend: SWARM_BACKEND,
@@ -13041,7 +13597,9 @@ function daemonStatus(deps) {
13041
13597
  counts,
13042
13598
  live,
13043
13599
  version: PACKAGE_VERSION,
13044
- repos: deps.repoMap?.() ?? []
13600
+ repos: deps.repoMap?.() ?? [],
13601
+ // TASWM-324: «нас не пускают» — то же состояние процесса, что кричит рамкой в лог.
13602
+ authDenied: authStateLine()
13045
13603
  };
13046
13604
  }
13047
13605
  function eventsStream(db, narratorLog, control, daemonVersion, restart, liveSessionIds2, selfHost) {
@@ -13141,7 +13699,12 @@ function makeHandler(deps) {
13141
13699
  if (p === "/merge-gate") {
13142
13700
  const ref = url.searchParams.get("pr") ?? "";
13143
13701
  if (!ref.trim()) return Response.json({ error: "\u043D\u0443\u0436\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 pr=<url|\u043D\u043E\u043C\u0435\u0440>" }, { status: 400 });
13144
- const resolve4 = deps.mergeGate ?? makeMergeGateResolver({ listRuns: () => deps.db.listRuns(), fallbackAutoReview: SWARM_AUTO_REVIEW });
13702
+ const resolve4 = deps.mergeGate ?? makeMergeGateResolver({
13703
+ listRuns: () => deps.db.listRuns(),
13704
+ fallbackAutoReview: SWARM_AUTO_REVIEW,
13705
+ autoVerify: SWARM_AUTO_VERIFY
13706
+ // TASWM-344
13707
+ });
13145
13708
  return Response.json(await resolve4(ref));
13146
13709
  }
13147
13710
  if (p === "/resources") return Response.json(hostResourceSnapshot());
@@ -13301,40 +13864,6 @@ function closeWebServer(server) {
13301
13864
  });
13302
13865
  }
13303
13866
 
13304
- // src/release.ts
13305
- init_src();
13306
- init_claudeBin();
13307
- var SWARM_PACKAGE = SWARM_PACKAGE_NAME;
13308
- var LATEST_TIMEOUT_MS = 4e3;
13309
- async function fetchLatestVersion(opts = {}) {
13310
- const fetchFn = opts.fetchFn ?? fetch;
13311
- const timeoutMs = opts.timeoutMs ?? LATEST_TIMEOUT_MS;
13312
- const pkg = opts.pkg ?? SWARM_PACKAGE;
13313
- const ctrl = new AbortController();
13314
- const timer = setTimeout(() => ctrl.abort(), timeoutMs);
13315
- try {
13316
- const res = await fetchFn(`https://registry.npmjs.org/${pkg}/latest`, {
13317
- signal: ctrl.signal,
13318
- headers: { accept: "application/vnd.npm.install-v1+json, application/json" }
13319
- });
13320
- if (!res.ok) return null;
13321
- const body = await res.json();
13322
- return typeof body.version === "string" && body.version ? body.version : null;
13323
- } catch {
13324
- return null;
13325
- } finally {
13326
- clearTimeout(timer);
13327
- }
13328
- }
13329
- function isOutdated(current, latest) {
13330
- if (!latest || !current) return false;
13331
- return compareVersionDirs(latest, current) > 0;
13332
- }
13333
- function outdatedLine(current, latest, updateCommand = "swarm update") {
13334
- if (!isOutdated(current, latest)) return null;
13335
- return `\u26A0 \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u043E\u044F \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0430: \u0443 \u0442\u0435\u0431\u044F v${current}, \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest} \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: ${updateCommand}`;
13336
- }
13337
-
13338
13867
  // src/daemonVersion.ts
13339
13868
  function captureDaemonStart(now = () => /* @__PURE__ */ new Date()) {
13340
13869
  return { startedAt: now().toISOString(), version: PACKAGE_VERSION };
@@ -14459,7 +14988,14 @@ async function prGateForReviewer(deps, run, prUrl, when) {
14459
14988
  }
14460
14989
  }
14461
14990
  function doneMergedPrForSlug(db, slug) {
14462
- const done = db.listRuns("done").find((r) => r.task_slug === slug && r.pr_url && r.kind !== "hero" && r.resolves_run_id == null && r.reviews_run_id == null);
14991
+ const done = db.listRuns("done").find(
14992
+ (r) => r.task_slug === slug && r.pr_url && r.kind !== "hero" && r.resolves_run_id == null && r.reviews_run_id == null && // TASWM-446: и НЕ верификатор. «done = смержено» верно только для прогона, который PR
14993
+ // делал; верификатор же заканчивает в `done`, держа `pr_url` ПРОВЕРЯЕМОГО — открытого —
14994
+ // PR, и его завершение читалось как доказательство мержа. Дальше pre-spawn гейт закрывал
14995
+ // исполнителя как дубль: 17.08 так погибли пять живых прогонов подряд, а задача после
14996
+ // этого не бралась вовсе — следующий дрон умирал на том же месте.
14997
+ r.verifies_run_id == null
14998
+ );
14463
14999
  return done?.pr_url ?? null;
14464
15000
  }
14465
15001
  async function closeDuplicateTaskRun(deps, run, mergedUrl) {
@@ -14478,7 +15014,8 @@ async function closeDuplicateTaskRun(deps, run, mergedUrl) {
14478
15014
  async function closeMergedTaskRuns(deps) {
14479
15015
  const bySlug = /* @__PURE__ */ new Map();
14480
15016
  for (const run of deps.db.listRuns()) {
14481
- if (run.kind === "hero" || run.resolves_run_id != null || run.reviews_run_id != null) continue;
15017
+ if (run.kind === "hero" || run.resolves_run_id != null || run.reviews_run_id != null || run.verifies_run_id != null)
15018
+ continue;
14482
15019
  let group = bySlug.get(run.task_slug);
14483
15020
  if (!group) bySlug.set(run.task_slug, group = []);
14484
15021
  group.push(run);
@@ -14521,20 +15058,57 @@ async function closeMergedTaskRuns(deps) {
14521
15058
  }
14522
15059
  async function closeOrphanedReviewers(deps) {
14523
15060
  for (const rv of deps.db.listRuns()) {
14524
- if (rv.kind !== "reviewer" || rv.reviews_run_id == null) continue;
15061
+ const checkedId = rv.kind === "reviewer" ? rv.reviews_run_id : rv.kind === "verifier" ? rv.verifies_run_id : null;
15062
+ if (checkedId == null) continue;
14525
15063
  if (!REVIEWER_ALIVE_STATUSES.has(rv.status)) continue;
14526
- const parent = deps.db.getRun(rv.reviews_run_id);
15064
+ const parent = deps.db.getRun(checkedId);
14527
15065
  if (parent && !isTerminalStatus(parent.status)) continue;
14528
15066
  try {
14529
15067
  const why = parent ? `\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442 (${STATUS_META[parent.status]?.label ?? parent.status})` : "\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u0438\u0441\u0447\u0435\u0437";
14530
- await closeReviewerRun(deps, rv, `${why} \u2014 PR \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u0440\u0435\u0432\u044C\u044E\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E`);
14531
- if (parent) deps.db.addEvent(parent.id, "step", `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${rv.name} (#${rv.id}) \u0437\u0430\u043A\u0440\u044B\u0442: ${why}`);
15068
+ await closeReviewerRun(deps, rv, `${why} \u2014 PR \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E`);
15069
+ if (parent) deps.db.addEvent(parent.id, "step", `${rv.kind === "verifier" ? "\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440" : "\u0440\u0435\u0432\u044C\u044E\u0435\u0440"} ${rv.name} (#${rv.id}) \u0437\u0430\u043A\u0440\u044B\u0442: ${why}`);
14532
15070
  narrate(deps, `${droneLabel(rv)} \u043E\u0441\u0442\u0430\u043B\u0441\u044F \u043D\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u043E\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u0435 \u2014 \u0433\u0430\u0448\u0443 \u0441\u0430\u043C, \u0441\u043B\u043E\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0451\u043D.`);
14533
15071
  } catch (e) {
14534
15072
  console.error(`[overmind] \u0437\u0430\u043A\u0440\u044B\u0442\u0438\u0435 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 #${rv.id} \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:`, e);
14535
15073
  }
14536
15074
  }
14537
15075
  }
15076
+ var VERIFY_SPAWN_LIMIT = Number(process.env.SWARM_VERIFY_SPAWN_LIMIT) || 2;
15077
+ var VERIFY_ESCALATION_MARK = "\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F: \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u044B";
15078
+ function countActiveVerifiers(db) {
15079
+ return db.listRuns().filter((r) => r.kind === "verifier" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
15080
+ }
15081
+ async function spawnVerifiersForOpenPrs(deps, stormActive) {
15082
+ if (!deps.spawnVerifier) return;
15083
+ if (stormActive) return;
15084
+ for (const run of deps.db.listRuns("review")) {
15085
+ try {
15086
+ if (run.kind !== "drone") continue;
15087
+ if (run.reviews_run_id != null || run.resolves_run_id != null || run.verifies_run_id != null) continue;
15088
+ if (!run.pr_url || !isGithubPrUrl(run.pr_url)) continue;
15089
+ if (run.verify_verdict != null) continue;
15090
+ if (deps.db.verifierCountFor(run.id) > 0) continue;
15091
+ const attempts = deps.db.verifierAttemptCountFor(run.id);
15092
+ if (attempts >= VERIFY_SPAWN_LIMIT) {
15093
+ if (!deps.db.events(run.id).some((e) => (e.text ?? "").includes(VERIFY_ESCALATION_MARK))) {
15094
+ deps.db.addEvent(run.id, "step", `${VERIFY_ESCALATION_MARK}: ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
15095
+ narrate(deps, `\u0412\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F ${droneLabel(run)} \u043D\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0437\u0430 ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`);
15096
+ }
15097
+ continue;
15098
+ }
15099
+ if (deps.resourcesOk && !deps.resourcesOk()) continue;
15100
+ if (deps.reviewCap !== void 0 && countActiveVerifiers(deps.db) >= deps.reviewCap) continue;
15101
+ const st = await deps.prStatus(run.pr_url).catch(() => null);
15102
+ if (prLiveness(st) !== "open") continue;
15103
+ if (st?.isDraft || st?.conflicting) continue;
15104
+ await deps.spawnVerifier(run, st?.files ?? []);
15105
+ narrate(deps, `PR ${droneLabel(run)} \u043E\u0442\u043A\u0440\u044B\u0442 \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440\u0430: \u043F\u0443\u0441\u0442\u044C \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0435\u0442 \u043E\u043F\u0440\u043E\u0432\u0435\u0440\u0433\u043D\u0443\u0442\u044C \u0437\u0430\u044F\u0432\u043B\u0435\u043D\u043D\u043E\u0435.`);
15106
+ } catch (e) {
15107
+ deps.db.addEvent(run.id, "step", `\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0430\u0441\u044C: ${String(e)}`);
15108
+ console.error(`[overmind] \u043F\u043E\u0434\u044A\u0451\u043C \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440\u0430 \u0434\u043B\u044F #${run.id} (${run.task_slug}) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
15109
+ }
15110
+ }
15111
+ }
14538
15112
  var CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C (\u0441\u0435\u0442\u044C/\u043F\u0440\u043E\u0446\u0435\u0441\u0441) \u2014 \u0442\u044B headless-\u0434\u0440\u043E\u043D, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u0423 \u0416\u0415 \u0437\u0430\u0434\u0430\u0447\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 `git status`/`git log` \u0438 \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0441\u0432\u043E\u0438\u0445 \u0448\u0430\u0433\u043E\u0432: \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E \u2014 \u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438 \u0441 \u043C\u0435\u0441\u0442\u0430 \u043E\u0431\u0440\u044B\u0432\u0430. \u0414\u043E\u0432\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u043E \u043A\u043E\u043D\u0446\u0430, \u041A\u041E\u041C\u041C\u0418\u0422\u042C \u0418 \u041F\u0423\u0428\u042C \u043F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0448\u0430\u0433\u0430, \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url). \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 MCP-\u0442\u0443\u043B ask.";
14539
15113
  function buildCiWakeNudge(summary) {
14540
15114
  return [
@@ -14711,6 +15285,16 @@ function ciTrustLine(prUrl, verifyMode) {
14711
15285
  }
14712
15286
  return `\u041D\u0435 \u043F\u0435\u0440\u0435\u0433\u043E\u043D\u044F\u0439 \u0442\u0435\u0441\u0442-\u0441\u044C\u044E\u0442/typecheck \u0437\u0430\u043D\u043E\u0432\u043E \u2014 \u044D\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043B CI \u043D\u0430 \u044D\u0442\u043E\u043C \u0436\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u0435 (\u0434\u0432\u043E\u0439\u043D\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043E\u0442\u043C\u0435\u043D\u0451\u043D, TASWM-249). \u041F\u0440\u043E\u0447\u0438\u0442\u0430\u0439 \u0435\u0433\u043E \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \u0441\u0430\u043C: ${readChecks}. \u0414\u0430\u043B\u044C\u0448\u0435 \u0442\u0440\u0438 \u0440\u0430\u0437\u043D\u044B\u0445 \u0441\u043B\u0443\u0447\u0430\u044F: \u2460 \u0445\u043E\u0442\u044C \u043E\u0434\u0438\u043D \u0447\u0435\u043A \u0435\u0449\u0451 PENDING \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u043E\u043E\u0431\u0449\u0435 \u043D\u0435 \u0432\u044B\u043D\u043E\u0441\u0438: \u043D\u0435 \u043F\u043E\u0441\u0442\u0438 REQUEST CHANGES, \u043D\u0435 \u0437\u043E\u0432\u0438 done(), \u043F\u0440\u043E\u0441\u0442\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438 \u0441\u0435\u0441\u0441\u0438\u044E \u043A\u0430\u043A \u0435\u0441\u0442\u044C \u2014 \u0440\u043E\u0439 \u0441\u0430\u043C \u0437\u0430\u043C\u0435\u0442\u0438\u0442, \u0447\u0442\u043E \u0442\u044B \u0436\u0434\u0451\u0448\u044C CI, \u0438 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442 \u0434\u043E\u0434\u0435\u043B\u0430\u0442\u044C \u0440\u0435\u0432\u044C\u044E, \u043A\u043E\u0433\u0434\u0430 \u0432\u0441\u0435 \u0447\u0435\u043A\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0442\u0441\u044F (\u044D\u0442\u043E \u0448\u0442\u0430\u0442\u043D\u044B\u0439 \u043F\u0443\u0442\u044C, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 \u0437\u0432\u0430\u0442\u044C \u043D\u0435 \u043D\u0443\u0436\u043D\u043E). \u2461 \u0432\u0441\u0435 \u0447\u0435\u043A\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0438\u0441\u044C \u0438 \u0435\u0441\u0442\u044C FAIL \u2014 REQUEST CHANGES, \u043D\u0430\u0437\u043E\u0432\u0438 \u043F\u043E\u0438\u043C\u0451\u043D\u043D\u043E, \u0447\u0442\u043E \u0443\u043F\u0430\u043B\u043E. \u2462 \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043C\u0438\u0442\u0435 \u0447\u0435\u043A\u043E\u0432 \u043D\u0435\u0442 \u0432\u043E\u0432\u0441\u0435 \u2014 \u0442\u043E\u0436\u0435 REQUEST CHANGES: \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u043C (\u0443 \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430 verifyMode=ci, \u0442\u043E \u0435\u0441\u0442\u044C CI \u043E\u0431\u044F\u0437\u0430\u043D \u0431\u044B\u0442\u044C). ${dontTrustAuthor}`;
14713
15287
  }
15288
+ var REVIEW_DEFECT_CLASSES = [
15289
+ "\u041A\u043B\u0430\u0441\u0441\u044B \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u0432 \u2014 \u043F\u0440\u043E\u0439\u0434\u0438 \u043F\u043E \u043A\u0430\u0436\u0434\u043E\u043C\u0443 \u0438 \u0434\u0430\u0439 \u043E\u0442\u0432\u0435\u0442 \u0432 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0435; \xAB\u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\xBB \u2014 \u0437\u0430\u043A\u043E\u043D\u043D\u044B\u0439 \u043E\u0442\u0432\u0435\u0442, \u043D\u043E \u043D\u0430\u0437\u0432\u0430\u043D\u043D\u044B\u0439 \u0438 \u0441 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439 \u0432 \u043F\u043E\u043B\u0441\u0442\u0440\u043E\u043A\u0438 (\xAB\u0440\u0443\u0447\u0435\u043A \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u043E\xBB, \xAB\u043F\u0435\u0440\u0435\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u0439 \u043D\u0435 \u0442\u0440\u043E\u043D\u0443\u0442\u043E\xBB). \u0428\u0435\u0441\u0442\u044C \u0440\u0430\u0437 \xAB\u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\xBB \u0431\u0435\u0437 \u043F\u0440\u0438\u0447\u0438\u043D \u2014 \u043D\u0435 \u0440\u0435\u0432\u044C\u044E, \u0430 \u043E\u0442\u043F\u0438\u0441\u043A\u0430:",
15290
+ "- [ ] \u041F\u0440\u0430\u0432\u0430 \u0430\u043A\u0442\u043E\u0440\u0430 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441: \u043A\u0442\u043E \u0444\u0438\u0437\u0438\u0447\u0435\u0441\u043A\u0438 \u043C\u043E\u0436\u0435\u0442 \u043F\u043E\u0437\u0432\u0430\u0442\u044C \u043D\u043E\u0432\u0443\u044E \u0440\u0443\u0447\u043A\u0443 \u0438\u043B\u0438 \u043D\u0430\u0436\u0430\u0442\u044C \u043A\u043D\u043E\u043F\u043A\u0443 \u2014 \u0447\u0435\u043B\u043E\u0432\u0435\u043A, \u0434\u0440\u043E\u043D \u0440\u043E\u044F, \u0433\u043E\u0441\u0442\u044C, \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A \u0427\u0423\u0416\u041E\u0413\u041E \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430? \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435, \u043A\u043E\u0442\u043E\u0440\u043E\u0435 \u0437\u0430\u0434\u0430\u0447\u0430 \u0437\u0430\u0432\u043E\u0434\u0438\u043B\u0430 \u0440\u0430\u0434\u0438 \u0420\u0415\u0428\u0415\u041D\u0418\u042F \u0427\u0415\u041B\u041E\u0412\u0415\u041A\u0410 (\u043F\u0443\u0431\u043B\u0438\u043A\u0430\u0446\u0438\u044F, \u0430\u043F\u0440\u0443\u0432, \u043E\u043F\u043B\u0430\u0442\u0430, \u0440\u0430\u0441\u0441\u044B\u043B\u043A\u0430), \u043E\u0431\u044F\u0437\u0430\u043D\u043E \u043E\u0442\u0431\u0438\u0432\u0430\u0442\u044C \u0430\u0433\u0435\u043D\u0442\u0430 \u044F\u0432\u043D\u043E (denyAgentActor \u0438 \u0430\u043D\u0430\u043B\u043E\u0433\u0438), \u0430 \u043D\u0435 \u043F\u043E\u043B\u0430\u0433\u0430\u0442\u044C\u0441\u044F \u043D\u0430 \u0442\u043E, \u0447\u0442\u043E \u0443 \u043D\u0435\u0433\u043E \u0440\u043E\u043B\u044C \u043F\u043E\u0436\u0438\u0436\u0435 \u2014 \u0443 \u0434\u0440\u043E\u043D\u0430 \u043E\u043D\u0430 \u043E\u0431\u044B\u0447\u043D\u043E \u043A\u0430\u043A \u0443 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430. \u041B\u044E\u0431\u043E\u0439 id, \u043F\u0440\u0438\u0435\u0445\u0430\u0432\u0448\u0438\u0439 \u0432 \u0442\u0435\u043B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u0430, \u043E\u0431\u044F\u0437\u0430\u043D \u0441\u0432\u0435\u0440\u044F\u0442\u044C\u0441\u044F \u0441 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u043E\u043C \u0437\u0432\u043E\u043D\u044F\u0449\u0435\u0433\u043E: \u0440\u043E\u0443\u0442 \u0431\u0435\u0437 \u0442\u0430\u043A\u043E\u0439 \u0441\u0432\u0435\u0440\u043A\u0438 \u2014 \u043A\u0440\u043E\u0441\u0441-\u0442\u0435\u043D\u0430\u043D\u0442\u043D\u0430\u044F \u0434\u044B\u0440\u0430, \u0438 \u0432 \u044D\u0442\u043E\u043C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u044D\u0442\u043E \u0441\u0430\u043C\u044B\u0439 \u0447\u0430\u0441\u0442\u044B\u0439 \u043A\u043B\u0430\u0441\u0441.",
15291
+ "- [ ] \u041F\u043E\u043B\u043D\u043E\u0442\u0430 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u043E\u0432: PR \u0434\u043E\u0431\u0430\u0432\u0438\u043B \u0432\u0430\u0440\u0438\u0430\u043D\u0442 \u0432 enum/union/\u043D\u0430\u0431\u043E\u0440 \u0441\u0442\u0440\u043E\u043A \u2014 \u043D\u0430\u0439\u0434\u0438 \u0412\u0421\u0415 \u043C\u0435\u0441\u0442\u0430, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u044D\u0442\u0438 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u044B \u043F\u0435\u0440\u0435\u0447\u0438\u0441\u043B\u044F\u044E\u0442 (\u0431\u0435\u043B\u044B\u0435 \u0441\u043F\u0438\u0441\u043A\u0438, switch, \u043C\u0430\u043F\u043F\u0438\u043D\u0433\u0438, \u0443\u0441\u043B\u043E\u0432\u0438\u044F \u0432 UI), \u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u044C, \u0447\u0442\u043E \u043D\u043E\u0432\u044B\u0439 \u0442\u0430\u043C \u0435\u0441\u0442\u044C. typecheck \u044D\u0442\u043E \u043F\u0440\u043E\u043F\u0443\u0441\u043A\u0430\u0435\u0442, \u043A\u043E\u0433\u0434\u0430 \u0441\u043F\u0438\u0441\u043E\u043A \u2014 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435, \u0430 \u043D\u0435 exhaustive-switch. \u042D\u0442\u043E \u041E\u0414\u0418\u041D grep \u043F\u043E \u0438\u043C\u0435\u043D\u0438 \u0442\u0438\u043F\u0430 \u0438\u043B\u0438 \u043F\u043E \u0441\u0430\u043C\u043E\u043C\u0443 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044E, \u0430 \u043D\u0435 \u0447\u0442\u0435\u043D\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \xAB\u0434\u043B\u044F \u043F\u043E\u043D\u0438\u043C\u0430\u043D\u0438\u044F\xBB \u2014 \u043A\u043E\u043D\u0442\u0443\u0440 \u044D\u043A\u043E\u043D\u043E\u043C\u0438\u0438 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435 \u0437\u0430\u043F\u0440\u0435\u0449\u0430\u0435\u0442.",
15292
+ "- [ ] \u0412\u0441\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A\u0430: \u0443 \u0441\u0443\u0449\u043D\u043E\u0441\u0442\u0438 \u043F\u043E\u0447\u0442\u0438 \u0432\u0441\u0435\u0433\u0434\u0430 \u0431\u043E\u043B\u044C\u0448\u0435 \u0434\u0432\u0443\u0445 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0439 (\u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E, \u043F\u0430\u0443\u0437\u0430, drain, \u043C\u043E\u043B\u0447\u0438\u0442, \u0434\u0430\u043D\u043D\u044B\u0445 \u0435\u0449\u0451 \u043D\u0435\u0442, \u0434\u043E\u0441\u0442\u0443\u043F \u0437\u0430\u043A\u0440\u044B\u0442). \u0420\u0430\u0437\u043E\u0431\u0440\u0430\u043D\u044B \u0412\u0421\u0415 \u0438\u043B\u0438 \u0442\u043E\u043B\u044C\u043A\u043E happy path \u0438 \u043E\u0434\u043D\u0430 \u043E\u0448\u0438\u0431\u043A\u0430? \u0427\u0430\u0441\u0442\u043E \u043F\u043E\u043B\u043D\u044B\u0439 \u043D\u0430\u0431\u043E\u0440 \u0443\u0436\u0435 \u043B\u0435\u0436\u0438\u0442 \u0440\u044F\u0434\u043E\u043C \u0432 \u044F\u0434\u0440\u0435 \u2014 \u043F\u043E\u0438\u0449\u0438 \u0435\u0433\u043E, \u043F\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043C \u0441\u0447\u0438\u0442\u0430\u0442\u044C \u043F\u0435\u0440\u0435\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u0435 \u043F\u043E\u043B\u043D\u044B\u043C.",
15293
+ "- [ ] \u041E\u0448\u0438\u0431\u043A\u0430 \u0434\u043E\u0445\u043E\u0434\u0438\u0442 \u0434\u043E \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430: \u043E\u0442\u043A\u0430\u0437 \u0437\u0430\u043F\u0440\u043E\u0441\u0430, \u043F\u0440\u043E\u0432\u0430\u043B \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u0438, \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u2014 \u0432\u0438\u0434\u043D\u044B \u0432 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435, \u0430 \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 console.warn/\u043B\u043E\u0433. \u041A\u043D\u043E\u043F\u043A\u0430, \u043A\u043E\u0442\u043E\u0440\u0430\u044F \u043C\u043E\u043B\u0447\u0430 \u043D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u0441\u0434\u0435\u043B\u0430\u043B\u0430, \u2014 \u0434\u0435\u0444\u0435\u043A\u0442, \u0434\u0430\u0436\u0435 \u0435\u0441\u043B\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E.",
15294
+ "- [ ] \u0414\u0435\u0444\u043E\u043B\u0442 \u043F\u0440\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0438 \u0434\u0430\u043D\u043D\u044B\u0445: \u043F\u043E\u043B\u044F \u043D\u0435\u0442, \u043E\u0442\u0432\u0435\u0442 \u043F\u0443\u0441\u0442\u043E\u0439, \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C \u043C\u043E\u043B\u0447\u0438\u0442 \u2014 \u043A\u0443\u0434\u0430 \u043F\u0430\u0434\u0430\u0435\u0442 \u0440\u0435\u0448\u0435\u043D\u0438\u0435? \u0414\u043B\u044F \u0443\u0434\u0430\u043B\u044F\u044E\u0449\u0438\u0445, \u043F\u0443\u0431\u043B\u0438\u043A\u0443\u044E\u0449\u0438\u0445 \u0438 \u0441\u043F\u0438\u0441\u044B\u0432\u0430\u044E\u0449\u0438\u0445 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0434\u0435\u0444\u043E\u043B\u0442 \u043E\u0431\u044F\u0437\u0430\u043D \u0431\u044B\u0442\u044C \u0432 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u0443\u044E \u0441\u0442\u043E\u0440\u043E\u043D\u0443 (\u043D\u0435 \u0434\u0435\u043B\u0430\u0442\u044C), \u0430 \xAB\u043D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445\xBB \u043D\u0435 \u0441\u043C\u0435\u0435\u0442 \u0447\u0438\u0442\u0430\u0442\u044C\u0441\u044F \u043A\u0430\u043A \xAB\u0447\u0438\u0441\u0442\u043E, \u043C\u043E\u0436\u043D\u043E\xBB.",
15295
+ "- [ ] \u0422\u0435\u0441\u0442\u044B PR \u0434\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u044E\u0442 \u0437\u0430\u044F\u0432\u043B\u0435\u043D\u043D\u043E\u0435. \u0415\u0441\u043B\u0438 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u0423\u0416\u0415 \u043F\u043E\u0434\u043D\u044F\u0442\u043E (\u0432 worktree \u0435\u0441\u0442\u044C node_modules \u0438 \u043F\u0430\u043A\u0435\u0442\u044B \u0441\u043E\u0431\u0440\u0430\u043D\u044B): \u0441\u043D\u0438\u043C\u0438 \u0432 \u041A\u041E\u0414\u0415 \u0442\u0443 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443, \u043A\u043E\u0442\u043E\u0440\u0443\u044E \u043D\u043E\u0432\u044B\u0439 \u0442\u0435\u0441\u0442 \u044F\u043A\u043E\u0431\u044B \u0441\u0442\u043E\u0440\u043E\u0436\u0438\u0442 (\u043E\u0434\u043D\u0443 \u0441\u0442\u0440\u043E\u043A\u0443 \u2014 \u0433\u0435\u0439\u0442, \u0443\u0441\u043B\u043E\u0432\u0438\u0435, \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u044E), \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u041E\u0414\u0418\u041D \u044D\u0442\u043E\u0442 \u0442\u0435\u0441\u0442 (\u043D\u0435 \u0432\u0435\u0441\u044C \u0441\u044C\u044E\u0442), \u0432\u0435\u0440\u043D\u0438 \u0444\u0430\u0439\u043B \u043A\u0430\u043A \u0431\u044B\u043B\u043E. \u0422\u0435\u0441\u0442 \u043E\u0431\u044F\u0437\u0430\u043D \u043F\u043E\u043A\u0440\u0430\u0441\u043D\u0435\u0442\u044C; \u043E\u0441\u0442\u0430\u043B\u0441\u044F \u0437\u0435\u043B\u0451\u043D\u044B\u043C \u2014 \u043D\u0430\u0445\u043E\u0434\u043A\u0430: \u043E\u043D \u043D\u0435 \u0441\u0442\u043E\u0440\u043E\u0436\u0438\u0442 \u043D\u0438\u0447\u0435\u0433\u043E.",
15296
+ " \u041E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u041D\u0415 \u043F\u043E\u0434\u043D\u044F\u0442\u043E \u2014 \u0442\u0430\u043A \u0438 \u0431\u0443\u0434\u0435\u0442: \u0442\u0435\u0431\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0438 \u0447\u0438\u0441\u0442\u044B\u0439 \u0447\u0435\u043A\u0430\u0443\u0442, \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u043D\u0438\u043A\u0442\u043E \u043D\u0435 \u0441\u0442\u0430\u0432\u0438\u043B. \u041F\u043E\u0434\u043D\u0438\u043C\u0430\u0442\u044C \u0438\u0445 \u041D\u0415 \u041D\u0410\u0414\u041E, \u044D\u0442\u043E \u0434\u043E\u0440\u043E\u0436\u0435 \u0432\u0441\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E: \u0441\u043A\u0430\u0436\u0438 \u0432 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0435 \xAB\u043F\u0440\u043E\u0433\u043D\u0430\u0442\u044C \u043D\u0435 \u043C\u043E\u0433, \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F \u043D\u0435\u0442\xBB \u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0442\u0435\u0441\u0442 \u0427\u0422\u0415\u041D\u0418\u0415\u041C. \u0422\u0440\u0438 \u0441\u043F\u043E\u0441\u043E\u0431\u0430 \u0441\u043E\u0432\u0440\u0430\u0442\u044C \u0437\u0435\u043B\u0451\u043D\u044B\u043C, \u0432\u0438\u0434\u043D\u044B\u0435 \u0433\u043B\u0430\u0437\u0430\u043C\u0438: (\u0430) \u0438\u0441\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0433\u043B\u043E\u0447\u0435\u043D\u043E (`try/except`, \u0437\u043E\u043D\u0442 \u0441\u0442\u043E\u0440\u043E\u0436\u0430, `.catch(() => {})`) \u2014 \u0442\u0435\u0441\u0442 \u043D\u0435 \u043F\u0430\u0434\u0430\u0435\u0442 \u0442\u0430\u043C, \u0433\u0434\u0435 \u043E\u0431\u044F\u0437\u0430\u043D; (\u0431) \u0430\u0441\u0441\u0435\u0440\u0442 \u043D\u0430 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435, \u0432\u0435\u0440\u043D\u043E\u0435 \u0432\u0441\u0435\u0433\u0434\u0430, \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u043F\u0440\u0430\u0432\u043A\u0438; (\u0432) \u0442\u0435\u0441\u0442 \u043D\u0435 \u043F\u0440\u043E\u0445\u043E\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 \u0438\u0437\u043C\u0435\u043D\u0451\u043D\u043D\u044B\u0439 \u0433\u0435\u0439\u0442 \u0432\u043E\u0432\u0441\u0435 \u2014 \u0434\u0451\u0440\u0433\u0430\u0435\u0442 \u0441\u043E\u0441\u0435\u0434\u043D\u0438\u0439 \u043F\u0443\u0442\u044C."
15297
+ ].join("\n");
14714
15298
  function buildReviewChecklist(reviewRun, files, priorReview, verifyMode = SWARM_VERIFY) {
14715
15299
  const header = files.length ? `\u0424\u0430\u0439\u043B\u044B \u0434\u0438\u0444\u0444\u0430 (${files.length}):
14716
15300
  ${files.map((f) => `- ${f}`).join("\n")}` : "\u0424\u0430\u0439\u043B\u044B \u0434\u0438\u0444\u0444\u0430: gh \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u0441\u043F\u0438\u0441\u043E\u043A \u2014 \u0441\u043C. PR.";
@@ -14730,7 +15314,11 @@ ${priorReview}` : "",
14730
15314
  "\u041A\u043E\u0440\u043E\u0442\u043A\u0438\u0439 \u0447\u0435\u043A-\u043B\u0438\u0441\u0442 \u0440\u0435-\u0447\u0435\u043A\u0430:",
14731
15315
  "- [ ] \u0417\u0430\u043C\u0435\u0447\u0430\u043D\u0438\u044F \u043F\u0440\u043E\u0448\u043B\u043E\u0433\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u044B \u2014 \u043F\u043E \u043A\u0430\u0436\u0434\u043E\u043C\u0443 \u043F\u0443\u043D\u043A\u0442\u0443.",
14732
15316
  `- [ ] \u0420\u0435\u0433\u0440\u0435\u0441\u0441\u0430 \u043D\u0435\u0442: ${ciTrustLine(prUrl, verifyMode)}`,
14733
- "- [ ] \u041D\u043E\u0432\u044B\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u043D\u0435 \u043F\u0440\u0438\u0432\u043D\u0435\u0441\u043B\u0438 \u043D\u043E\u0432\u044B\u0445 \u043F\u0440\u043E\u0431\u043B\u0435\u043C (\u0433\u043E\u043D\u043A\u0438/\u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438/UI \u043F\u043E \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\u0441\u0442\u0438)."
15317
+ "- [ ] \u041D\u043E\u0432\u044B\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u043D\u0435 \u043F\u0440\u0438\u0432\u043D\u0435\u0441\u043B\u0438 \u043D\u043E\u0432\u044B\u0445 \u043F\u0440\u043E\u0431\u043B\u0435\u043C: \u0433\u043E\u043D\u043A\u0438/\u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438/UI \u043F\u043E \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\u0441\u0442\u0438 \u2014 \u0438 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E \u043F\u0440\u0430\u0432\u0430 \u0430\u043A\u0442\u043E\u0440\u0430, \u0434\u0435\u0444\u043E\u043B\u0442 \u043F\u0440\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0438 \u0434\u0430\u043D\u043D\u044B\u0445, \u043F\u043E\u043B\u043D\u043E\u0442\u0430 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u043E\u0432. \u041F\u0440\u0430\u0432\u043A\u0430 \u043F\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0443 \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u0442\u0435 \u0436\u0435 \u043C\u0435\u0441\u0442\u0430, \u0433\u0434\u0435 \u0434\u0435\u0444\u0435\u043A\u0442 \u0438 \u0431\u044B\u043B, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u0438\u043C\u0435\u043D\u043D\u043E \u043E\u043D\u0430 \u0447\u0430\u0449\u0435 \u0432\u0441\u0435\u0433\u043E \u043F\u0440\u0438\u043D\u043E\u0441\u0438\u0442 \u043D\u043E\u0432\u044B\u0439.",
15318
+ // Единственный класс из REVIEW_DEFECT_CLASSES, который на втором круге обязателен:
15319
+ // правки по вердикту почти всегда приносят НОВЫЕ тесты, и именно тут прошёл #819.
15320
+ // Остальные пять классов заново не гоняем — ре-чек намеренно короткий (TASWM-437).
15321
+ "- [ ] \u041D\u043E\u0432\u044B\u0435 \u0442\u0435\u0441\u0442\u044B \u043F\u0440\u0430\u0432\u043A\u0438: \u0441\u043D\u0438\u043C\u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443, \u043A\u043E\u0442\u043E\u0440\u0443\u044E \u0442\u0435\u0441\u0442 \u0441\u0442\u043E\u0440\u043E\u0436\u0438\u0442, \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u041E\u0414\u0418\u041D \u044D\u0442\u043E\u0442 \u0442\u0435\u0441\u0442, \u0432\u0435\u0440\u043D\u0438 \u043A\u0430\u043A \u0431\u044B\u043B\u043E \u2014 \u043E\u043D \u043E\u0431\u044F\u0437\u0430\u043D \u043F\u043E\u043A\u0440\u0430\u0441\u043D\u0435\u0442\u044C."
14734
15322
  ].filter(Boolean).join("\n");
14735
15323
  }
14736
15324
  return [
@@ -14740,8 +15328,11 @@ ${priorReview}` : "",
14740
15328
  "",
14741
15329
  verifyMode === "local" ? "\u0427\u0435\u043A-\u043B\u0438\u0441\u0442 (\u0433\u0435\u0439\u0442 \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0433\u043E\u043D\u044F\u0435\u0448\u044C \u0442\u044B \u0441\u0430\u043C \u2014 \u0441\u043C. \u043F\u0435\u0440\u0432\u044B\u0439 \u043F\u0443\u043D\u043A\u0442; \u0434\u0430\u043B\u044C\u0448\u0435 \u0440\u0443\u043A\u0430\u043C\u0438 \u0442\u043E, \u0447\u0435\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0435 \u043F\u043E\u043A\u0440\u044B\u0432\u0430\u0435\u0442):" : "\u0427\u0435\u043A-\u043B\u0438\u0441\u0442 (\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0439 \u0440\u0443\u043A\u0430\u043C\u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u0442\u043E, \u0447\u0435\u0433\u043E CI \u0444\u0438\u0437\u0438\u0447\u0435\u0441\u043A\u0438 \u043D\u0435 \u043F\u043E\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u2014 \u043D\u0435 \u0432\u0435\u0440\u044C \u0446\u0438\u0444\u0440\u0430\u043C \u0430\u0432\u0442\u043E\u0440\u0430 PR):",
14742
15330
  `- [ ] ${ciTrustLine(prUrl, verifyMode)}`,
15331
+ "",
15332
+ REVIEW_DEFECT_CLASSES,
15333
+ "",
14743
15334
  "- [ ] \u041C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 (\u0435\u0441\u043B\u0438 \u0435\u0441\u0442\u044C) \u2014 \u0441\u043E\u0432\u043C\u0435\u0441\u0442\u0438\u043C\u043E\u0441\u0442\u044C \u0441\u043E \u0441\u0442\u0430\u0440\u044B\u043C\u0438 \u0434\u0430\u043D\u043D\u044B\u043C\u0438, \u043E\u0442\u043A\u0430\u0442.",
14744
- "- [ ] \u0413\u043E\u043D\u043A\u0438/\u043A\u043E\u043D\u043A\u0443\u0440\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C \u2014 race conditions, \u043F\u043E\u0440\u044F\u0434\u043E\u043A \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0439, \u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C.",
15335
+ "- [ ] \u0413\u043E\u043D\u043A\u0438 \u2014 \u043F\u043E\u0440\u044F\u0434\u043E\u043A \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0439, \u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u043E\u0433\u043E \u0432\u044B\u0437\u043E\u0432\u0430.",
14745
15336
  "- [ ] UI-\u0434\u0438\u0441\u0446\u0438\u043F\u043B\u0438\u043D\u0430 (\u0434\u043B\u044F web-PR) \u2014 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435 \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435 (uikit), \u0441\u043A\u0440\u0438\u043D\u044B mobile+desktop \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u044B \u041A \u0417\u0410\u0414\u0410\u0427\u0415 (done(screenshots), TASWM-294), \u0430 \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u043A PR.",
14746
15337
  "- [ ] \u0415\u0441\u043B\u0438 \u043F\u0440\u0430\u0432\u0438\u0442\u0441\u044F router.tsx \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0430 per-page meta \u0434\u043B\u044F \u0430\u0433\u0435\u043D\u0442\u0441\u043A\u043E\u0433\u043E \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0430 (\u0438\u043D\u0430\u0447\u0435 NL-\u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F \u043C\u043E\u043B\u0447\u0430 \u043B\u043E\u043C\u0430\u0435\u0442\u0441\u044F)."
14747
15338
  ].join("\n");
@@ -14767,9 +15358,16 @@ var REVIEW_SCOPE_BLOCK = [
14767
15358
  "\u2022 \u0417\u0430\u0445\u043E\u0434\u043E\u0432 \u043D\u0430 \u043E\u0434\u0438\u043D \u0432\u043E\u043F\u0440\u043E\u0441 \u2014 \u043E\u0434\u0438\u043D. \u041D\u0435 \u043D\u0430\u0448\u0451\u043B \u043E\u0442\u0432\u0435\u0442\u0430 \u0441 \u043F\u0435\u0440\u0432\u043E\u0433\u043E \u0440\u0430\u0437\u0430 (\u0438\u043B\u0438 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442 \u043C\u043E\u043B\u0447\u0438\u0442) \u2014 \u044D\u0442\u043E \u0443\u0436\u0435 \u043D\u0430\u0445\u043E\u0434\u043A\u0430: \u0441\u043A\u0430\u0436\u0438 \u043E\u0431 \u044D\u0442\u043E\u043C \u0432 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0435, \u0430 \u043D\u0435 \u0443\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0439 \u0442\u0440\u0435\u0442\u0438\u0439 \u043A\u0440\u0443\u0433 \u043F\u043E\u0438\u0441\u043A\u0430.",
14768
15359
  "\u2022 \u0412\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u0443\u0441\u043F\u0435\u043B \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C, \u0412\u0421\u0415\u0413\u0414\u0410 \u043B\u0443\u0447\u0448\u0435 \u0438\u0434\u0435\u0430\u043B\u044C\u043D\u043E\u0433\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u043D\u0435 \u0431\u0443\u0434\u0435\u0442: \u043E\u0434\u043D\u043E\u0433\u043E \u043E\u0431\u043E\u0441\u043D\u043E\u0432\u0430\u043D\u043D\u043E\u0433\u043E \u0437\u0430\u043C\u0435\u0447\u0430\u043D\u0438\u044F \u0445\u0432\u0430\u0442\u0430\u0435\u0442, \u0447\u0442\u043E\u0431\u044B PR \u043F\u043E\u0435\u0445\u0430\u043B \u043D\u0430 \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0443. \u041A\u043E\u043F\u0430\u0435\u0448\u044C \u0434\u043E\u043B\u0433\u043E \u2014 \u0441\u0432\u043E\u0440\u0430\u0447\u0438\u0432\u0430\u0439\u0441\u044F \u0438 \u0432\u044B\u043D\u043E\u0441\u0438 \u0442\u043E, \u0447\u0442\u043E \u0435\u0441\u0442\u044C, \u0447\u0435\u0441\u0442\u043D\u043E \u043D\u0430\u0437\u0432\u0430\u0432, \u0447\u0435\u0433\u043E \u043D\u0435 \u0441\u043C\u043E\u0442\u0440\u0435\u043B."
14769
15360
  ].join("\n");
15361
+ var REVIEW_MISSION_BLOCK = [
15362
+ "\u{1F3AF} \u0422\u0412\u041E\u042F \u0420\u0410\u0411\u041E\u0422\u0410 \u2014 \u041D\u0410\u0419\u0422\u0418 \u0414\u0415\u0424\u0415\u041A\u0422\u042B. \u041F\u0435\u0440\u0435\u0441\u043A\u0430\u0437 \u0442\u043E\u0433\u043E, \u0447\u0442\u043E \u0434\u0435\u043B\u0430\u0435\u0442 PR, \u0440\u0430\u0431\u043E\u0442\u043E\u0439 \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F: \u044D\u0442\u043E \u0430\u0432\u0442\u043E\u0440 \u0443\u0436\u0435 \u043D\u0430\u043F\u0438\u0441\u0430\u043B \u0432 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0438. \u0422\u044B \u0438\u0449\u0435\u0448\u044C, \u0447\u0442\u043E \u0441\u043B\u043E\u043C\u0430\u0435\u0442\u0441\u044F.",
15363
+ "\u2022 \u0417\u0435\u043B\u0451\u043D\u044B\u0439 CI \u043D\u0435 \u0434\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442, \u0447\u0442\u043E \u0434\u0435\u0444\u0435\u043A\u0442\u0430 \u043D\u0435\u0442: \u043E\u043D \u0433\u043E\u043D\u044F\u0435\u0442 \u0442\u0435\u0441\u0442\u044B \u0430\u0432\u0442\u043E\u0440\u0430 \u0438 \u0442\u0438\u043F\u044B. \u0421\u043C\u043E\u0442\u0440\u0438 \u0440\u043E\u0432\u043D\u043E \u0442\u043E, \u0447\u0435\u0433\u043E \u043E\u043D \u043D\u0435 \u0432\u0438\u0434\u0438\u0442 \u2014 \u043F\u0440\u0430\u0432\u0430 \u0430\u043A\u0442\u043E\u0440\u0430, \u043C\u0443\u043B\u044C\u0442\u0438\u0442\u0435\u043D\u0430\u043D\u0442\u043D\u043E\u0441\u0442\u044C, \u0434\u0435\u0444\u043E\u043B\u0442\u044B \u043F\u0440\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0438 \u0434\u0430\u043D\u043D\u044B\u0445, \u043D\u0435\u043E\u0431\u0440\u0430\u0431\u043E\u0442\u0430\u043D\u043D\u044B\u0435 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u044B \u0438 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F, \u0442\u0435\u043A\u0441\u0442 \u043E\u0448\u0438\u0431\u043A\u0438 \u0434\u043B\u044F \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430, \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435 \u043F\u0440\u0438 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0439 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438.",
15364
+ "\u2022 \u041D\u0430\u0445\u043E\u0434\u043A\u0430 \u0434\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u0421\u0426\u0415\u041D\u0410\u0420\u0418\u0415\u041C: \xAB\u0442\u0430\u043A\u043E\u0439 \u0432\u0445\u043E\u0434 \u0438\u043B\u0438 \u0442\u0430\u043A\u043E\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u2192 \u0432\u043E\u0442 \u0442\u0430\u043A\u043E\u0439 \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\xBB. \xAB\u0412\u044B\u0433\u043B\u044F\u0434\u0438\u0442 \u043D\u0435\u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\xBB, \xAB\u0441\u0442\u043E\u0438\u0442 \u043F\u0435\u0440\u0435\u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C\xBB, \xAB\u0436\u0435\u043B\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043F\u043E\u043A\u0440\u044B\u0442\u044C \u0442\u0435\u0441\u0442\u0430\u043C\u0438\xBB \u2014 \u043D\u0435 \u043D\u0430\u0445\u043E\u0434\u043A\u0438; \u0442\u0430\u043A\u043E\u0435 \u043D\u0435 \u043F\u0438\u0448\u0438.",
15365
+ "\u2022 \u041D\u043E\u043B\u044C \u043D\u0430\u0445\u043E\u0434\u043E\u043A \u2014 \u0437\u0430\u043A\u043E\u043D\u043D\u044B\u0439 \u0432\u0435\u0440\u0434\u0438\u043A\u0442, \u043D\u043E \u0442\u043E\u0433\u0434\u0430 \u043D\u0430\u0437\u043E\u0432\u0438, \u0427\u0422\u041E \u0438\u043C\u0435\u043D\u043D\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u043A\u0430\u043A\u0438\u043C \u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0435\u043C. APPROVE \u0431\u0435\u0437 \u044D\u0442\u043E\u0433\u043E \u0440\u043E\u0439 \u0447\u0438\u0442\u0430\u0435\u0442 \u043A\u0430\u043A \u043F\u0435\u0440\u0435\u0441\u043A\u0430\u0437, \u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442 PR \u0437\u0430\u043D\u043E\u0432\u043E \u0432\u0440\u0443\u0447\u043D\u0443\u044E."
15366
+ ].join("\n");
14770
15367
  function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachmentsBlock, verifyMode = SWARM_VERIFY) {
14771
15368
  const name = reviewer.name || `#${reviewer.id}`;
14772
15369
  const prUrl = reviewRun.pr_url ?? "(\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 PR \u2014 \u0441\u043C. \u043B\u043E\u0433 \u043F\u0440\u043E\u0433\u043E\u043D\u0430)";
15370
+ const classesStep = reviewRun.review_cycle > 0 ? "" : "\u043F\u0440\u043E\u0439\u0434\u0438 \u043A\u043B\u0430\u0441\u0441\u044B \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u0432 (\u043F\u043E \u043A\u0430\u0436\u0434\u043E\u043C\u0443 \u2014 \u043E\u0442\u0432\u0435\u0442 \u0441 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439, \xAB\u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\xBB \u0442\u043E\u0436\u0435), ";
14773
15371
  return [
14774
15372
  `\u0422\u044B \u2014 \u0434\u0440\u043E\u043D-\u0420\u0415\u0412\u042C\u042E\u0415\u0420 ${name} \u0440\u043E\u044F, headless-\u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C (\u041D\u0415 \u0438\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0439 \u0447\u0430\u0442). \u0410\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E (TASWM-148): \u0440\u043E\u0439 \u0441\u0430\u043C \u043F\u043E\u0434\u043D\u044F\u043B \u0442\u0435\u0431\u044F \u043D\u0430 PR \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044F \u2014 \u0442\u044B \u041D\u0415 \u0432\u044B\u0431\u0440\u0430\u043D \u0438\u043C \u0438 \u043E\u0442 \u043D\u0435\u0433\u043E \u043D\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u0448\u044C.`,
14775
15373
  "",
@@ -14783,7 +15381,12 @@ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachments
14783
15381
  `\u26A0\uFE0F \u042D\u0442\u043E\u0442 PR \u043E\u0442\u043A\u0440\u044B\u0442 \u0432 \u0432\u0435\u0442\u043A\u0443 \`${reviewRun.base_branch}\` (\u0438\u043D\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u043E\u043D\u043D\u0430\u044F \u043F\u0430\u0447\u043A\u0430), \u0430 \u041D\u0415 \u0432 \u0432\u0435\u0442\u043A\u0443 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E. \u0414\u0438\u0444 \u0441\u043C\u043E\u0442\u0440\u0438 \u0441\u0442\u0440\u043E\u0433\u043E \u043E\u0442 \u043D\u0435\u0451: \`git diff origin/${reviewRun.base_branch}...HEAD\` (\u0441\u043F\u0438\u0441\u043E\u043A \u0444\u0430\u0439\u043B\u043E\u0432 \u043D\u0438\u0436\u0435 \u0443\u0436\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D gh \u043E\u0442\u043D\u043E\u0441\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u044D\u0442\u043E\u0439 \u0431\u0430\u0437\u044B). \u0421\u0440\u0430\u0432\u043D\u0435\u043D\u0438\u0435 \u0441 main \u043F\u043E\u043A\u0430\u0437\u0430\u043B\u043E \u0431\u044B \u0442\u0435\u0431\u0435 \u0447\u0443\u0436\u0438\u0435 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u043F\u0430\u0447\u043A\u0438 \u043A\u0430\u043A \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u044D\u0442\u043E\u0433\u043E \u0434\u0440\u043E\u043D\u0430 \u2014 \u043D\u0435 \u0434\u0435\u043B\u0430\u0439 \u0442\u0430\u043A.`
14784
15382
  ] : [],
14785
15383
  "",
14786
- "\u26A0\uFE0F \u0422\u042B \u041D\u0415 \u041F\u0420\u0410\u0412\u0418\u0428\u042C \u0424\u0410\u0419\u041B\u042B \u0418 \u041D\u0415 \u041F\u0423\u0428\u0418\u0428\u042C \u041A\u041E\u041C\u041C\u0418\u0422\u042B. \u0422\u0432\u043E\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0438 \u0432\u044B\u043D\u0435\u0441\u0442\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442.",
15384
+ // TASWM-437: мутационная проверка теста — это правка файла, и без явного исключения
15385
+ // ревьюер честно откажется её делать. Разрешаем ровно её: локально, с возвратом файла,
15386
+ // без коммита и пуша (worktree ревьюера всё равно удаляется после прогона).
15387
+ "\u26A0\uFE0F \u0422\u042B \u041D\u0415 \u041F\u0420\u0410\u0412\u0418\u0428\u042C \u041A\u041E\u0414 PR \u0418 \u041D\u0415 \u041F\u0423\u0428\u0418\u0428\u042C \u041A\u041E\u041C\u041C\u0418\u0422\u042B. \u0422\u0432\u043E\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0438 \u0432\u044B\u043D\u0435\u0441\u0442\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442. \u0415\u0434\u0438\u043D\u0441\u0442\u0432\u0435\u043D\u043D\u0430\u044F \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D\u043D\u0430\u044F \u043F\u0440\u0430\u0432\u043A\u0430 \u2014 \u0432\u0440\u0435\u043C\u0435\u043D\u043D\u0430\u044F \u043C\u0443\u0442\u0430\u0446\u0438\u044F \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0442\u0435\u0441\u0442\u0430 (\u043F\u0443\u043D\u043A\u0442 \u0447\u0435\u043A-\u043B\u0438\u0441\u0442\u0430 \u043F\u0440\u043E \u0442\u0435\u0441\u0442\u044B): \u0441\u043D\u044F\u043B \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443, \u043F\u0440\u043E\u0433\u043D\u0430\u043B \u043E\u0434\u0438\u043D \u0442\u0435\u0441\u0442, \u0432\u0435\u0440\u043D\u0443\u043B \u0444\u0430\u0439\u043B (`git checkout -- <\u0444\u0430\u0439\u043B>`). \u0412\u043E\u0437\u0432\u0440\u0430\u0442 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u0414\u041E \u043A\u043E\u043D\u0446\u0430 \u0441\u0435\u0441\u0441\u0438\u0438, \u0434\u0430\u0436\u0435 \u0435\u0441\u043B\u0438 \u0441\u0432\u043E\u0440\u0430\u0447\u0438\u0432\u0430\u0435\u0448\u044C\u0441\u044F \u0434\u043E\u0441\u0440\u043E\u0447\u043D\u043E (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 \u0443\u0445\u043E\u0434\u0438\u0448\u044C \u0436\u0434\u0430\u0442\u044C CI): `git status` \u0432 worktree \u043E\u0431\u044F\u0437\u0430\u043D \u0431\u044B\u0442\u044C \u0447\u0438\u0441\u0442. \u041A\u043E\u043C\u043C\u0438\u0442\u0438\u0442\u044C, \u043F\u0443\u0448\u0438\u0442\u044C \u0438 \u043E\u0442\u043A\u0440\u044B\u0432\u0430\u0442\u044C \u0441\u0432\u043E\u0438 PR \u043D\u0435\u043B\u044C\u0437\u044F \u043D\u0438\u0447\u0435\u0433\u043E \u0438 \u043D\u0438\u043A\u043E\u0433\u0434\u0430.",
15388
+ "",
15389
+ REVIEW_MISSION_BLOCK,
14787
15390
  "",
14788
15391
  REVIEW_SCOPE_BLOCK,
14789
15392
  // TASWM-283: ревьюер читает ТУ ЖЕ задачу и так же слеп к её картинкам — без макета
@@ -14795,11 +15398,11 @@ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachments
14795
15398
  "",
14796
15399
  "\u041F\u043E\u0440\u044F\u0434\u043E\u043A:",
14797
15400
  ...verifyMode === "local" ? [
14798
- "1. \u041F\u0440\u043E\u0439\u0434\u0438 \u0447\u0435\u043A-\u043B\u0438\u0441\u0442 \u0432\u044B\u0448\u0435: \u0433\u0435\u0439\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0421\u0410\u041C \u0432 \u0441\u0432\u043E\u0451\u043C worktree (\u044D\u0442\u043E \u0438 \u0435\u0441\u0442\u044C \u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E \u0437\u0435\u043B\u0451\u043D\u043E\u0433\u043E \u2014 \u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 verifyMode=local), \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438/\u0433\u043E\u043D\u043A\u0438/UI/per-page meta \u043F\u043E \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\u0441\u0442\u0438.",
15401
+ `1. \u041F\u0440\u043E\u0439\u0434\u0438 \u0447\u0435\u043A-\u043B\u0438\u0441\u0442 \u0432\u044B\u0448\u0435: \u0433\u0435\u0439\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0421\u0410\u041C \u0432 \u0441\u0432\u043E\u0451\u043C worktree (\u044D\u0442\u043E \u0438 \u0435\u0441\u0442\u044C \u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E \u0437\u0435\u043B\u0451\u043D\u043E\u0433\u043E \u2014 \u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 verifyMode=local), ${classesStep}\u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438/\u0433\u043E\u043D\u043A\u0438/UI/per-page meta \u043F\u043E \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\u0441\u0442\u0438.`,
14799
15402
  " \u0427\u0435\u043A\u043E\u0432 \u043D\u0430 \u043A\u043E\u043C\u043C\u0438\u0442\u0435 \u043D\u0435\u0442 (\u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0435\u0442 CI) \u2014 \u044D\u0442\u043E \u041D\u0415 \u0434\u0435\u0444\u0435\u043A\u0442 PR \u0438 \u041D\u0415 \u043F\u043E\u0432\u043E\u0434 \u0434\u043B\u044F REQUEST CHANGES: \u043D\u0435 \u0436\u0434\u0438 \u0438\u0445, \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0439 \u0437\u0430\u0432\u0435\u0441\u0442\u0438 workflow, \u0441\u0443\u0434\u0438 \u043F\u043E \u0441\u0432\u043E\u0435\u043C\u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0443. \u041A\u0440\u0430\u0441\u043D\u044B\u0439 \u0417\u0410\u0412\u0415\u0420\u0428\u0418\u0412\u0428\u0418\u0419\u0421\u042F \u0447\u0435\u043A, \u0435\u0441\u043B\u0438 CI \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0432\u0441\u0451-\u0442\u0430\u043A\u0438 \u0435\u0441\u0442\u044C, \u2014 \u043F\u043E-\u043F\u0440\u0435\u0436\u043D\u0435\u043C\u0443 \u0431\u043B\u043E\u043A.",
14800
15403
  `2. \u041F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u043E\u043D\u0447\u0435\u043D \u2014 \u043E\u0441\u0442\u0430\u0432\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043D\u0430 \u0441\u0430\u043C PR: \`gh pr comment ${prUrl} --body "..."\` (\u041D\u0415 \`gh pr review\` \u2014 GitHub \u0435\u0433\u043E \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442 \u043D\u0430 \u0441\u0432\u043E\u0451\u043C PR). \u041D\u0430\u0447\u043D\u0438 \u0441 **APPROVE** \u0438\u043B\u0438 **REQUEST CHANGES** \u0437\u0430\u0433\u043B\u0430\u0432\u043D\u044B\u043C\u0438, \u0434\u0430\u043B\u044C\u0448\u0435 \u2014 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u0438\u043A\u0430: \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B (\u043F\u0440\u0438\u0432\u0435\u0434\u0438 \u0432\u044B\u0432\u043E\u0434 \u0441\u0432\u043E\u0435\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430), \u0447\u0442\u043E \u043D\u0430\u0448\u0451\u043B; \u0434\u043B\u044F REQUEST CHANGES \u2014 \u0442\u043E\u0447\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B/\u0441\u0442\u0440\u043E\u043A\u0438 \u0438 \u0447\u0442\u043E \u0447\u0438\u043D\u0438\u0442\u044C. \u041F\u0440\u0435\u0442\u0435\u043D\u0437\u0438\u044F \u043A \u0438\u043D\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (\u043D\u0435\u0442 CI, \u043A\u043E\u043D\u0444\u0438\u0433\u0438, \u0434\u0435\u043F\u043B\u043E\u0439) \u2014 \u043D\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E PR: \u0441\u0447\u0438\u0442\u0430\u0435\u0448\u044C, \u0447\u0442\u043E \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044E \u043D\u0443\u0436\u0435\u043D CI, \u2014 \u0441\u043A\u0430\u0436\u0438 \u044D\u0442\u043E \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439 \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0435, \u043D\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043E\u0442 \u043D\u0435\u0451 \u043D\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u0442.`
14801
15404
  ] : [
14802
- "1. \u041F\u0440\u043E\u0439\u0434\u0438 \u0447\u0435\u043A-\u043B\u0438\u0441\u0442 \u0432\u044B\u0448\u0435: \u0441\u0442\u0430\u0442\u0443\u0441 CI \u043F\u043E \u044D\u0442\u043E\u043C\u0443 \u043A\u043E\u043C\u043C\u0438\u0442\u0443 \u0447\u0438\u0442\u0430\u0439 \u0441\u0430\u043C (\u043D\u0435 \u0432\u0435\u0440\u044C \u043E\u0442\u0447\u0451\u0442\u0443 \u0430\u0432\u0442\u043E\u0440\u0430 PR, \u043D\u043E \u0438 \u043D\u0435 \u043F\u0435\u0440\u0435\u0433\u043E\u043D\u044F\u0439 CI \u0432\u0440\u0443\u0447\u043D\u0443\u044E), \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438/\u0433\u043E\u043D\u043A\u0438/UI/per-page meta \u043F\u043E \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\u0441\u0442\u0438.",
15405
+ `1. \u041F\u0440\u043E\u0439\u0434\u0438 \u0447\u0435\u043A-\u043B\u0438\u0441\u0442 \u0432\u044B\u0448\u0435: \u0441\u0442\u0430\u0442\u0443\u0441 CI \u043F\u043E \u044D\u0442\u043E\u043C\u0443 \u043A\u043E\u043C\u043C\u0438\u0442\u0443 \u0447\u0438\u0442\u0430\u0439 \u0441\u0430\u043C (\u043D\u0435 \u0432\u0435\u0440\u044C \u043E\u0442\u0447\u0451\u0442\u0443 \u0430\u0432\u0442\u043E\u0440\u0430 PR, \u043D\u043E \u0438 \u043D\u0435 \u043F\u0435\u0440\u0435\u0433\u043E\u043D\u044F\u0439 CI \u0432\u0440\u0443\u0447\u043D\u0443\u044E), ${classesStep}\u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438/\u0433\u043E\u043D\u043A\u0438/UI/per-page meta \u043F\u043E \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\u0441\u0442\u0438.`,
14803
15406
  " \u0415\u0441\u043B\u0438 \u0445\u043E\u0442\u044C \u043E\u0434\u0438\u043D \u0447\u0435\u043A \u0435\u0449\u0451 PENDING \u2014 \u043D\u0430 \u044D\u0442\u043E\u043C \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0441\u044C: \u0448\u0430\u0433\u0438 2 \u0438 3 \u043D\u0438\u0436\u0435 \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438 \u0446\u0435\u043B\u0438\u043A\u043E\u043C, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0435 \u0432\u044B\u043D\u043E\u0441\u0438, \u043F\u0440\u043E\u0441\u0442\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438 \u0441\u0435\u0441\u0441\u0438\u044E (\u0441\u043C. \u0447\u0435\u043A-\u043B\u0438\u0441\u0442 \u0432\u044B\u0448\u0435, \u043F\u043E\u0447\u0435\u043C\u0443 \u044D\u0442\u043E \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E \u0438 \u0447\u0442\u043E \u043F\u0440\u043E\u0438\u0437\u043E\u0439\u0434\u0451\u0442 \u0434\u0430\u043B\u044C\u0448\u0435).",
14804
15407
  `2. \u0427\u0435\u043A\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0438\u0441\u044C (\u0437\u0435\u043B\u0451\u043D\u044B\u0435 \u0438\u043B\u0438 \u0441 \u043A\u0440\u0430\u0441\u043D\u044B\u043C) \u0438\u043B\u0438 \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043C\u0438\u0442\u0435 \u0438\u0445 \u0432\u043E\u0432\u0441\u0435 \u043D\u0435\u0442 \u2014 \u043E\u0441\u0442\u0430\u0432\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043D\u0430 \u0441\u0430\u043C PR: \`gh pr comment ${prUrl} --body "..."\` (\u041D\u0415 \`gh pr review\` \u2014 GitHub \u0435\u0433\u043E \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442 \u043D\u0430 \u0441\u0432\u043E\u0451\u043C PR). \u041D\u0430\u0447\u043D\u0438 \u0441 **APPROVE** \u0438\u043B\u0438 **REQUEST CHANGES** \u0437\u0430\u0433\u043B\u0430\u0432\u043D\u044B\u043C\u0438, \u0434\u0430\u043B\u044C\u0448\u0435 \u2014 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u0438\u043A\u0430: \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B, \u0447\u0442\u043E \u043D\u0430\u0448\u0451\u043B; \u0434\u043B\u044F REQUEST CHANGES \u2014 \u0442\u043E\u0447\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B/\u0441\u0442\u0440\u043E\u043A\u0438 \u0438 \u0447\u0442\u043E \u0447\u0438\u043D\u0438\u0442\u044C.`
14805
15408
  ],
@@ -14836,6 +15439,7 @@ async function manageStorm(deps, now, worktreeExists, hasTranscript) {
14836
15439
  const consec = trailingProbeSuccesses(recs);
14837
15440
  if (stormShouldEnd({ consecutiveProbeOk: consec, externalDown: lastExternalDown(recs) === true }, cfg)) {
14838
15441
  db.endStorm(`${STORM_END_TEXT} (${consec} \u0443\u0441\u043F\u0435\u0448\u043D\u044B\u0435 health-probe \u043F\u043E\u0434\u0440\u044F\u0434)`);
15442
+ if (deps.spawnDefer) clearDefer(deps.spawnDefer, "storm");
14839
15443
  narrate(deps, "\u0428\u0442\u043E\u0440\u043C \u043A\u043E\u043D\u0447\u0438\u043B\u0441\u044F \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B.");
14840
15444
  await resurrectStormDead(deps, worktreeExists, hasTranscript);
14841
15445
  return false;
@@ -15117,8 +15721,10 @@ async function wakeReviewerOnCiReady(deps, run, over) {
15117
15721
  if (stats.count >= CI_WAKE_LIMIT || !canContinue) {
15118
15722
  const why = !canContinue ? "worktree/\u0441\u0435\u0441\u0441\u0438\u044F \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B \u2014 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E" : `${CI_WAKE_LIMIT} \u043F\u043E\u0431\u0443\u0434(\u043E\u043A) \u043F\u043E \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 CI \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E`;
15119
15723
  deps.db.updateRun(reviewer.id, { status: "stalled" });
15120
- deps.db.addEvent(reviewer.id, "stalled", `CI \u0433\u043E\u0442\u043E\u0432, \u043D\u043E \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442\u044C \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u0432\u044B\u0448\u043B\u043E: ${why} \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 PR`);
15121
- narrate(deps, `${droneLabel(reviewer)} \u0436\u0434\u0430\u043B CI, \u043D\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${why}) \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430, \u043D\u0435 \u043E\u0441\u0442\u0430\u0432\u043B\u044F\u044E \u0432 stalled.`);
15724
+ const freshWanted = await projectWantsReviewer(deps, run);
15725
+ const freshPlan = freshWanted ? "\u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 PR" : "\u0441\u0432\u0435\u0436\u0435\u0433\u043E \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E: \u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0430\u0432\u0442\u043E\u0440\u0435\u0432\u044C\u044E \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E";
15726
+ deps.db.addEvent(reviewer.id, "stalled", `CI \u0433\u043E\u0442\u043E\u0432, \u043D\u043E \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442\u044C \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u0432\u044B\u0448\u043B\u043E: ${why} \u2014 ${freshPlan}`);
15727
+ narrate(deps, `${droneLabel(reviewer)} \u0436\u0434\u0430\u043B CI, \u043D\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${why}) \u2014 ${freshPlan}.`);
15122
15728
  if (deps.watchdog?.notify) {
15123
15729
  const signal = {
15124
15730
  key: `reviewer_ci_wake_failed:${reviewer.id}`,
@@ -15127,7 +15733,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
15127
15733
  // поднимает свежего ревьюера на том же PR. Решать человеку тут нечего; если и
15128
15734
  // свежий не доедет, конвейер встанет и об этом скажет уже pr_stuck_* пушем.
15129
15735
  level: "feed",
15130
- body: `\u0420\u0435\u0432\u044C\u044E\u0435\u0440 ${droneLabel(reviewer)} \u0434\u043E\u0436\u0434\u0430\u043B\u0441\u044F \u0433\u043E\u0442\u043E\u0432\u043E\u0433\u043E CI, \u043D\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${why}). \u041F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 PR \u2014 ${run.pr_url ?? "?"}.`,
15736
+ body: `\u0420\u0435\u0432\u044C\u044E\u0435\u0440 ${droneLabel(reviewer)} \u0434\u043E\u0436\u0434\u0430\u043B\u0441\u044F \u0433\u043E\u0442\u043E\u0432\u043E\u0433\u043E CI, \u043D\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${why}). ${freshWanted ? "\u041F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 PR" : "\u0421\u0432\u0435\u0436\u0435\u0433\u043E \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E (\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0430\u0432\u0442\u043E\u0440\u0435\u0432\u044C\u044E \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E)"} \u2014 ${run.pr_url ?? "?"}.`,
15131
15737
  runId: reviewer.id,
15132
15738
  ...hrefOf(run.task_url)
15133
15739
  };
@@ -15137,7 +15743,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
15137
15743
  console.error(`[overmind] \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0435 \u043E \u043F\u043E\u0442\u0435\u0440\u0435 \u0441\u0435\u0441\u0441\u0438\u0438 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 #${reviewer.id} \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u043E:`, e);
15138
15744
  }
15139
15745
  }
15140
- await spawnFreshReviewerAfterStall(deps, run, why);
15746
+ if (freshWanted) await spawnFreshReviewerAfterStall(deps, run, why);
15141
15747
  return true;
15142
15748
  }
15143
15749
  try {
@@ -15159,6 +15765,10 @@ function worktreeAndTranscriptAlive(deps, worktree) {
15159
15765
  const transcript = deps.hasTranscript ?? executorHasSession;
15160
15766
  return exists(worktree) && transcript(worktree);
15161
15767
  }
15768
+ async function projectWantsReviewer(deps, run) {
15769
+ if (!deps.autoReviewForRepo) return true;
15770
+ return await deps.autoReviewForRepo(effectiveRepo(deps, run)).catch(() => true);
15771
+ }
15162
15772
  async function spawnFreshReviewerAfterStall(deps, run, why) {
15163
15773
  if (!deps.spawnReviewer) return;
15164
15774
  const live = liveOrWaitingReviewers(deps.db, run.id);
@@ -15251,23 +15861,23 @@ async function freshChangesWakeAllowed(deps, run, prUrl, now) {
15251
15861
  const moved = await deps.commitsSince(prUrl, verdictAt ?? run.created_at).catch(() => true);
15252
15862
  return !moved;
15253
15863
  }
15254
- async function prodStillAllows(deps, run, allowed) {
15255
- if (!deps.db.refreshRun) return true;
15864
+ async function prodOverrideReason(deps, run, allowed) {
15865
+ if (!deps.db.refreshRun) return null;
15256
15866
  let fresh;
15257
15867
  try {
15258
15868
  fresh = await deps.db.refreshRun(run.id);
15259
15869
  } catch {
15260
- return true;
15870
+ return null;
15261
15871
  }
15262
- if (!fresh) return false;
15263
- if (allowed.includes(fresh.status)) return true;
15872
+ if (!fresh) return "\u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0442 \u0432 \u043C\u043E\u0451\u043C \u0437\u0435\u0440\u043A\u0430\u043B\u0435 \u2014 \u043F\u0440\u043E\u0434 \u043E\u0442\u0434\u0430\u043B \u0435\u0433\u043E \u0434\u0440\u0443\u0433\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 (TASWM-300)";
15873
+ if (allowed.includes(fresh.status)) return null;
15264
15874
  deps.db.addEvent(
15265
15875
  run.id,
15266
15876
  "step",
15267
15877
  `\u043F\u0440\u043E\u0434 \u0433\u043E\u0432\u043E\u0440\u0438\u0442 \xAB${fresh.status}\xBB (\u0443 \u043C\u0435\u043D\u044F \u0431\u044B\u043B\u043E \xAB${run.status}\xBB) \u2014 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E, \u0441\u0442\u0430\u0442\u0443\u0441 \u043F\u0440\u043E\u0434\u0430 \u0441\u0438\u043B\u044C\u043D\u0435\u0435 (TASWM-300)`
15268
15878
  );
15269
15879
  narrate(deps, `${droneLabel(run)} \u2014 \u0432 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 \u0443\u0436\u0435 \xAB${fresh.status}\xBB, \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E: \u043F\u0440\u043E\u0434 \u0441\u0438\u043B\u044C\u043D\u0435\u0435 \u043C\u043E\u0435\u0433\u043E \u0437\u0435\u0440\u043A\u0430\u043B\u0430.`);
15270
- return false;
15880
+ return `\u043F\u0440\u043E\u0434 \u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043B \u043F\u0440\u043E\u0433\u043E\u043D, \u043F\u043E\u043A\u0430 \u0442\u043E\u0442 \u0436\u0434\u0430\u043B: \u0442\u0430\u043C \u0443\u0436\u0435 \xAB${fresh.status}\xBB, \u0443 \u043C\u0435\u043D\u044F \u0431\u044B\u043B\u043E \xAB${run.status}\xBB (TASWM-300)`;
15271
15881
  }
15272
15882
  async function envGateAllows(deps, run) {
15273
15883
  if (!deps.envGate) return true;
@@ -15373,13 +15983,18 @@ async function projectQuotaGateAllows(deps, run) {
15373
15983
  return true;
15374
15984
  }
15375
15985
  function pruneGateLogs(deps) {
15376
- if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size && !deps.overlapLogged?.size) return;
15986
+ if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size && !deps.overlapLogged?.size && !deps.spawnDefer?.size) return;
15377
15987
  const open = /* @__PURE__ */ new Set();
15378
15988
  for (const run of deps.db.listRuns()) if (!isTerminalStatus(run.status)) open.add(run.id);
15379
15989
  for (const map of [deps.envGateLogged, deps.exclusiveLogged, deps.overlapLogged]) {
15380
15990
  if (!map) continue;
15381
15991
  for (const id of [...map.keys()]) if (!open.has(id)) map.delete(id);
15382
15992
  }
15993
+ if (deps.spawnDefer) {
15994
+ const live = /* @__PURE__ */ new Set(["storm", "control-pause"]);
15995
+ for (const id of open) live.add(`run:${id}`);
15996
+ pruneDefers(deps.spawnDefer, live);
15997
+ }
15383
15998
  }
15384
15999
  async function projectQuotaBlockedRunIds(deps) {
15385
16000
  const blocked = /* @__PURE__ */ new Set();
@@ -15417,8 +16032,38 @@ function noteExhaustedDeps(deps, run, verdicts) {
15417
16032
  narrate(deps, `${droneLabel(run)} \u2014 ${text}`);
15418
16033
  }
15419
16034
  }
16035
+ function deferSpawn(deps, run, reason, opts = {}) {
16036
+ const eps = deps.spawnDefer ??= /* @__PURE__ */ new Map();
16037
+ const d = noteDefer(eps, `run:${run.id}`, reason, GATE_LOG_HEARTBEAT_TICKS);
16038
+ if (d.changed && opts.event !== false) deps.db.addEvent(run.id, "step", `\u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D: ${reason} \u2014 \u0436\u0434\u0443 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 (TASWM-333)`);
16039
+ const shout = opts.logKey ? noteDefer(eps, opts.logKey, reason, GATE_LOG_HEARTBEAT_TICKS) : d;
16040
+ if (shout.log && !opts.quiet) {
16041
+ const suffix = shout.changed ? "" : ` \u2014 \u0435\u0449\u0451 \u0432\u0438\u0441\u0438\u0442, \u0442\u0438\u043A ${shout.ticks}`;
16042
+ const who = opts.logKey ? "" : `${droneLabel(run)} (${run.task_slug}) \u2014 `;
16043
+ console.error(`[overmind] \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D: ${who}${reason}${suffix}`);
16044
+ }
16045
+ return "skipped";
16046
+ }
16047
+ function noteStormStall(deps) {
16048
+ const waiting = deps.db.listRuns("queued").filter((r) => deps.db.unmetDeps(r.depends_on).length === 0);
16049
+ if (waiting.length === 0) return;
16050
+ const startedAt = deps.db.currentStormStartedAt();
16051
+ const reason = `\u0440\u043E\u0439 \u0432 \u0448\u0442\u043E\u0440\u043C\u043E\u0432\u043E\u043C \u0440\u0435\u0436\u0438\u043C\u0435${startedAt ? ` \u0441 ${startedAt} UTC` : ""} \u2014 \u043D\u043E\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u043C \u0434\u043E \u043E\u0442\u0431\u043E\u044F (TASWM-23)`;
16052
+ const d = noteDefer(deps.spawnDefer ??= /* @__PURE__ */ new Map(), "storm", reason, GATE_LOG_HEARTBEAT_TICKS);
16053
+ if (d.log) {
16054
+ const suffix = d.changed ? "" : ` \u2014 \u0435\u0449\u0451 \u0432\u0438\u0441\u0438\u0442, \u0442\u0438\u043A ${d.ticks}`;
16055
+ console.error(`[overmind] \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D: ${reason}; \u0436\u0434\u0443\u0442 ${waiting.length} \u043F\u0440\u043E\u0433\u043E\u043D(\u043E\u0432)${suffix}`);
16056
+ }
16057
+ for (const run of waiting) deferSpawn(deps, run, reason, { quiet: true });
16058
+ }
15420
16059
  async function trySpawnQueuedRun(deps, run) {
15421
- if (deps.control?.paused) return "skipped";
16060
+ if (deps.control?.paused) {
16061
+ const why = deps.control.state?.reason;
16062
+ return deferSpawn(deps, run, `\u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B \u043F\u0440\u0438\u0451\u043C \u043D\u043E\u0432\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435${why ? ` \u2014 ${why}` : ""} (\u043F\u0430\u0443\u0437\u0430)`, {
16063
+ logKey: "control-pause"
16064
+ // причина одна на машину — в лог уходит одной строкой, а не по строке на прогон
16065
+ });
16066
+ }
15422
16067
  const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
15423
16068
  if (mergedUrl) {
15424
16069
  await closeDuplicateTaskRun(deps, run, mergedUrl);
@@ -15428,15 +16073,20 @@ async function trySpawnQueuedRun(deps, run) {
15428
16073
  return "closed";
15429
16074
  }
15430
16075
  const depVerdicts = deps.db.depVerdicts(run.depends_on);
15431
- if (depVerdicts.some((d) => !d.met)) return "skipped";
16076
+ const unmet = depVerdicts.filter((d) => !d.met);
16077
+ if (unmet.length) return deferSpawn(deps, run, `\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438: ${unmet.map((d) => d.slug).join(", ")}`);
15432
16078
  if (!await envGateAllows(deps, run)) return "skipped";
15433
16079
  if (!await projectQuotaGateAllows(deps, run)) return "skipped";
15434
- if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
15435
- if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
16080
+ const prodOverride = await prodOverrideReason(deps, run, ["queued"]);
16081
+ if (prodOverride) return deferSpawn(deps, run, prodOverride, { event: false });
16082
+ if (deps.db.takeRun && !await deps.db.takeRun(run.id)) {
16083
+ return deferSpawn(deps, run, "\u043D\u0435 \u0432\u0437\u044F\u043B \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0430 \u0441\u0432\u043E\u044E \u043C\u0430\u0448\u0438\u043D\u0443 \u2014 \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u0434\u0430\u043B \u0435\u0433\u043E \u0434\u0440\u0443\u0433\u043E\u043C\u0443 \u0434\u0435\u043C\u043E\u043D\u0443 (TASWM-281)");
16084
+ }
15436
16085
  noteExhaustedDeps(deps, run, depVerdicts);
15437
16086
  await fillSuggestedArea(deps, run);
15438
16087
  try {
15439
16088
  await deps.spawn(run);
16089
+ if (deps.spawnDefer) clearDefer(deps.spawnDefer, `run:${run.id}`);
15440
16090
  narrate(deps, `\u0411\u0435\u0440\u0443 ${run.task_slug} \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0441\u043F\u0430\u0432\u043D\u044E ${droneLabel(run)}.`);
15441
16091
  return "spawned";
15442
16092
  } catch (e) {
@@ -15449,7 +16099,7 @@ async function trySpawnQueuedRun(deps, run) {
15449
16099
  }
15450
16100
  return "skipped";
15451
16101
  }
15452
- if (e instanceof AreaBusyError) return "skipped";
16102
+ if (e instanceof AreaBusyError) return deferSpawn(deps, run, e.message);
15453
16103
  deps.db.updateRun(run.id, { status: "failed" });
15454
16104
  deps.db.addEvent(run.id, "failed", String(e));
15455
16105
  return "failed";
@@ -15820,6 +16470,17 @@ async function salvageVerdictFromPrComment(deps, rv, reviewsRunId, since, ctx) {
15820
16470
  }
15821
16471
  return true;
15822
16472
  }
16473
+ async function salvageVerifyVerdictFromPrComment(deps, vr, verifiesRunId, since, how) {
16474
+ if (!deps.verifyVerdictComment || !vr.pr_url) return false;
16475
+ const found = await deps.verifyVerdictComment(vr.pr_url, since).catch(() => null);
16476
+ if (!found) return false;
16477
+ const verdictText = verifyVerdictLabel(found.verdict);
16478
+ deps.db.updateRun(verifiesRunId, { verify_verdict: found.verdict });
16479
+ deps.db.addEvent(verifiesRunId, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u043F\u043E\u0434\u0445\u0432\u0430\u0447\u0435\u043D \u0438\u0437 PR-\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430 (${how}): ${verdictText}`);
16480
+ await closeReviewerRun(deps, vr, `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u044B\u043D\u0435\u0441\u0435\u043D \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C (${verdictText}), ${how} \u2014 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \u0437\u0430\u043A\u0440\u044B\u0442\u0430 \u0431\u0435\u0437 \u0441\u043F\u0430\u0432\u043D\u0430 \u0432\u0442\u043E\u0440\u043E\u0433\u043E \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440\u0430`);
16481
+ narrate(deps, `${droneLabel(vr)} \u043E\u0441\u0442\u0430\u0432\u0438\u043B \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 (${verdictText}) \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u2014 \u0437\u0430\u0441\u0447\u0438\u0442\u0430\u043B, \u0432\u0442\u043E\u0440\u043E\u0433\u043E \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E.`);
16482
+ return true;
16483
+ }
15823
16484
  async function failReviewerRun(deps, reviewer, reviewsRunId, opts) {
15824
16485
  if (!opts.keepWorktree && reviewer.worktree_path) {
15825
16486
  await deps.removeWorktree(reviewer.repo, reviewer.worktree_path).catch(() => {
@@ -16022,6 +16683,7 @@ async function tickOnce(deps) {
16022
16683
  stormActive = await manageStorm(deps, now, worktreeExists, hasTranscript);
16023
16684
  }
16024
16685
  const droneCap = deps.maxDrones ? await deps.maxDrones() : Infinity;
16686
+ if (stormActive) noteStormStall(deps);
16025
16687
  if (deps.resourcesOk) {
16026
16688
  await manageRelief(deps, now, droneCap, stormActive);
16027
16689
  } else if (!stormActive) {
@@ -16065,6 +16727,26 @@ async function tickOnce(deps) {
16065
16727
  narrate(deps, `${droneLabel(run)} \u0432\u044B\u043D\u0435\u0441 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0440\u0435\u0432\u044C\u044E \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442.`);
16066
16728
  continue;
16067
16729
  }
16730
+ if (run.verifies_run_id != null) {
16731
+ const verified = deps.db.getRun(run.verifies_run_id);
16732
+ const sameCycle = verified != null && (verified.review_cycle ?? 0) === (run.review_cycle ?? 0);
16733
+ if (verified && sameCycle && verified.verify_verdict == null) {
16734
+ if (await salvageVerifyVerdictFromPrComment(deps, run, run.verifies_run_id, run.created_at, "\u043F\u043E\u0437\u0432\u0430\u043B done(), \u043D\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B")) continue;
16735
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
16736
+ });
16737
+ deps.db.updateRun(run.id, { status: "failed" });
16738
+ deps.db.addEvent(run.id, "failed", "\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0437\u0430\u043A\u0440\u044B\u043B\u0441\u044F \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043B\u0430\u0441\u044C");
16739
+ deps.db.addEvent(run.verifies_run_id, "step", `\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 ${run.name} (#${run.id}) \u0437\u0430\u043A\u0440\u044B\u043B\u0441\u044F \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u043D\u043E\u0432\u044B\u0439 \u0437\u0430\u0445\u043E\u0434`);
16740
+ narrate(deps, `${droneLabel(run)} \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B, \u043D\u0435 \u043E\u0441\u0442\u0430\u0432\u0438\u0432 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438, \u2014 \u0437\u0430\u0441\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E.`);
16741
+ continue;
16742
+ }
16743
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
16744
+ });
16745
+ deps.db.updateRun(run.id, { status: "done" });
16746
+ deps.db.addEvent(run.id, "done", "\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0430, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u044B\u043D\u0435\u0441\u0435\u043D");
16747
+ narrate(deps, `${droneLabel(run)} \u0432\u044B\u043D\u0435\u0441 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442.`);
16748
+ continue;
16749
+ }
16068
16750
  if (!run.pr_url) continue;
16069
16751
  const prUrl = run.pr_url;
16070
16752
  const st = await deps.prStatus(prUrl);
@@ -16125,7 +16807,7 @@ async function tickOnce(deps) {
16125
16807
  if (run.review_verdict === "changes" && !stormActive && !st.conflicting && !deps.db.activeResolverFor(run.id) && await wakeAuthorOnChanges(deps, run, now, droneCap)) {
16126
16808
  continue;
16127
16809
  }
16128
- if (!st.conflicting && !st.isDraft && deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt) && deps.db.reviewerCountFor(run.id) === 0 && // ГЕЙТ ПЕРВЫМ: живой/успешный ревьюер уже есть — ни капа, ни спавна не трогаем
16810
+ const reviewerGatesOk = !st.conflicting && !st.isDraft && !!deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt) && deps.db.reviewerCountFor(run.id) === 0 && // ГЕЙТ ПЕРВЫМ: живой/успешный ревьюер уже есть — ни капа, ни спавна не трогаем
16129
16811
  // …и НИ ОДНОГО живого/ждущего ревьюера ЛЮБОГО цикла (TASWM-299). reviewerCountFor
16130
16812
  // выше скоупится по review_cycle, поэтому сама по себе она не инвариант «один
16131
16813
  // ревьюер на PR», а лишь идемпотентность внутри круга: фикс-итерация бампит цикл, и
@@ -16133,7 +16815,8 @@ async function tickOnce(deps) {
16133
16815
  // такого уже закрыла (тот же тик, выше) — этот гейт держит инвариант СТРУКТУРНО, а
16134
16816
  // не порядком веток в 900-строчной функции: увидели живого — значит закрывать его
16135
16817
  // рано, спавнить второго нельзя.
16136
- liveOrWaitingReviewers(deps.db, run.id).length === 0) {
16818
+ liveOrWaitingReviewers(deps.db, run.id).length === 0;
16819
+ if (reviewerGatesOk && deps.spawnReviewer && await projectWantsReviewer(deps, run)) {
16137
16820
  if (run.review_cycle >= REVIEW_CYCLE_LIMIT) {
16138
16821
  if (!deps.db.events(run.id).some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_CYCLE_ESCALATION_MARK))) {
16139
16822
  deps.db.addEvent(run.id, "step", `${REVIEW_CYCLE_ESCALATION_MARK}: ${run.review_cycle} \u0446\u0438\u043A\u043B(\u043E\u0432) \u0440\u0435\u0432\u044C\u044E-\u0444\u0438\u043A\u0441\u043E\u0432 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
@@ -16202,6 +16885,7 @@ async function tickOnce(deps) {
16202
16885
  }
16203
16886
  await closeMergedTaskRuns(deps);
16204
16887
  await closeOrphanedReviewers(deps);
16888
+ await spawnVerifiersForOpenPrs(deps, stormActive);
16205
16889
  refreshReviewPhases(deps);
16206
16890
  let liveChildPpids;
16207
16891
  try {
@@ -16213,7 +16897,7 @@ async function tickOnce(deps) {
16213
16897
  for (const run of [...deps.db.listRuns("running"), ...deps.db.listRuns("blocked")]) {
16214
16898
  const alive = run.pid != null && deps.isAlive(run.pid);
16215
16899
  if (!alive && deps.db.takeRun && !await deps.db.takeRun(run.id)) continue;
16216
- if (!alive && !await prodStillAllows(deps, run, ["running", "blocked"])) continue;
16900
+ if (!alive && await prodOverrideReason(deps, run, ["running", "blocked"])) continue;
16217
16901
  const outcome = alive ? "unknown" : deps.exitOutcome?.(run) ?? "unknown";
16218
16902
  if (!alive && deps.accountCost) {
16219
16903
  try {
@@ -16284,7 +16968,7 @@ async function tickOnce(deps) {
16284
16968
  continue;
16285
16969
  }
16286
16970
  }
16287
- if (!alive && outcome === "graceful" && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && run.kind !== "hero") {
16971
+ if (!alive && outcome === "graceful" && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && run.verifies_run_id == null && run.kind !== "hero") {
16288
16972
  const wt = run.worktree_path;
16289
16973
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
16290
16974
  if (deps.respawn && canContinue && run.respawns < RESPAWN_LIMIT) {
@@ -16309,7 +16993,7 @@ async function tickOnce(deps) {
16309
16993
  narrate(deps, `${droneLabel(run)} \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B \u0441\u0435\u0441\u0441\u0438\u044E \u0441\u0430\u043C, \u043D\u0435 \u043F\u043E\u0437\u0432\u0430\u0432 done() \u2014 \u0441\u0442\u0430\u0442\u0443\u0441 \xAB\u0437\u0430\u043C\u0435\u0440\xBB, \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u0432 \u043B\u043E\u0433\u0435 (TASWM-229).`);
16310
16994
  continue;
16311
16995
  }
16312
- if (!alive && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && deps.respawn && run.respawns < RESPAWN_LIMIT) {
16996
+ if (!alive && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && run.verifies_run_id == null && deps.respawn && run.respawns < RESPAWN_LIMIT) {
16313
16997
  const wt = run.worktree_path;
16314
16998
  const wtAlive = !!wt && worktreeExists(wt);
16315
16999
  const canContinue = wtAlive && hasTranscript(wt);
@@ -16345,6 +17029,10 @@ async function tickOnce(deps) {
16345
17029
  });
16346
17030
  if (salvaged) continue;
16347
17031
  }
17032
+ if (!alive && run.status === "running" && run.verifies_run_id != null && run.pr_url && deps.verifyVerdictComment) {
17033
+ const salvaged = await salvageVerifyVerdictFromPrComment(deps, run, run.verifies_run_id, run.created_at, "\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0432\u044B\u0448\u0435\u043B, \u043D\u0435 \u043F\u043E\u0437\u0432\u0430\u0432 done()");
17034
+ if (salvaged) continue;
17035
+ }
16348
17036
  if (!alive && run.status === "running" && run.reviews_run_id != null && run.pr_url && deps.ci) {
16349
17037
  const parent = deps.db.getRun(run.reviews_run_id);
16350
17038
  const ciWatchOff = parent != null && ciWatchClosed(deps.db, parent);
@@ -16435,9 +17123,17 @@ async function tickOnce(deps) {
16435
17123
  const az = deps.anomalies;
16436
17124
  let ready = [];
16437
17125
  try {
17126
+ const autoReviewSnapshot = /* @__PURE__ */ new Map();
17127
+ if (deps.autoReviewForRepo && az.autoReviewEnabled) {
17128
+ for (const r of deps.db.listRuns("review")) {
17129
+ if (!r.pr_url || autoReviewSnapshot.has(r.repo)) continue;
17130
+ autoReviewSnapshot.set(r.repo, await deps.autoReviewForRepo(effectiveRepo(deps, r)).catch(() => true));
17131
+ }
17132
+ }
16438
17133
  const candidates = [
16439
17134
  ...buildAnomalyCandidates(deps.db, now, az.thresholds, {
16440
- autoReviewEnabled: az.autoReviewEnabled,
17135
+ // Машинный ответ, уточнённый проектом (TASWM-396): нет снимка — как раньше.
17136
+ autoReviewEnabled: (repo) => az.autoReviewEnabled && (autoReviewSnapshot.get(repo) ?? true),
16441
17137
  ...az.outboxPendingCount ? { outboxPendingCount: az.outboxPendingCount() } : {},
16442
17138
  // resource-gate-stall (TASWM-212): та же причина, что видит лог manageRelief — один
16443
17139
  // deps.gateReason() на тик (кэш hostReading() внутри resources.ts, лишнего спавна нет).
@@ -16633,6 +17329,76 @@ async function spawnReviewer(deps, reviewRun, files) {
16633
17329
  throw e;
16634
17330
  }
16635
17331
  }
17332
+ var VERIFY_SCOPE_BLOCK = [
17333
+ "\u{1F4B8} \u041A\u041E\u041D\u0422\u0423\u0420 \u0412\u0415\u0420\u0418\u0424\u0418\u041A\u0410\u0426\u0418\u0418 (\u0436\u0451\u0441\u0442\u043A\u043E). \u0421\u0435\u0441\u0441\u0438\u044F \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0430 \u0431\u044E\u0434\u0436\u0435\u0442\u043E\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0438 \u0441\u0440\u0435\u0436\u0435\u0442 \u0442\u0435\u0431\u044F \u0431\u0435\u0437 \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044F \u2014 \u0443\u043F\u0440\u0451\u0448\u044C\u0441\u044F, \u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u043E\u0432\u0441\u0435. \u041F\u043E\u044D\u0442\u043E\u043C\u0443:",
17334
+ "\u2022 \u0422\u044B \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0448\u044C \u0417\u0410\u042F\u0412\u041B\u0415\u041D\u041D\u041E\u0415, \u0430 \u043D\u0435 \u0432\u0435\u0441\u044C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439. \u0421\u043F\u0438\u0441\u043E\u043A \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u2014 \u0438\u0437 \u043E\u0442\u0447\u0451\u0442\u0430 \u0434\u0440\u043E\u043D\u0430 \u0438 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u044F \u0437\u0430\u0434\u0430\u0447\u0438; \u0432\u0441\u0451 \u043E\u0441\u0442\u0430\u043B\u044C\u043D\u043E\u0435 \u0442\u0440\u043E\u0433\u0430\u0439, \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u043E\u043D\u043E \u043B\u043E\u043C\u0430\u0435\u0442\u0441\u044F \u0442\u0432\u043E\u0435\u0439 \u0436\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u043E\u0439.",
17335
+ "\u2022 \u041F\u043E\u0434\u043D\u044F\u0442\u044C \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u2014 \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 \u0434\u0432\u0443\u0445 \u0437\u0430\u0445\u043E\u0434\u043E\u0432. \u041D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u043E\u0441\u044C \u2014 \u044D\u0442\u043E \u0438 \u0435\u0441\u0442\u044C \u0447\u0430\u0441\u0442\u044C \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 (\xAB\u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C, \u043F\u043E\u0442\u043E\u043C\u0443 \u0447\u0442\u043E \u2026\xBB), \u0430 \u043D\u0435 \u043F\u043E\u0432\u043E\u0434 \u0447\u0438\u043D\u0438\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442.",
17336
+ "\u2022 \u041F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u043E\u0435 \u043D\u0435 \u043F\u0435\u0440\u0435\u0447\u0438\u0442\u044B\u0432\u0430\u0439, \u043E\u0434\u0438\u043D \u0432\u043E\u043F\u0440\u043E\u0441 \u2014 \u043E\u0434\u0438\u043D \u0437\u0430\u0445\u043E\u0434. \u041D\u0435 \u0441\u043E\u0448\u043B\u043E\u0441\u044C \u0441\u043E \u0432\u0442\u043E\u0440\u043E\u0433\u043E \u0440\u0430\u0437\u0430 \u2014 \u0437\u0430\u043F\u0438\u0441\u044B\u0432\u0430\u0439 \u043A\u0430\u043A \u043D\u0430\u0445\u043E\u0434\u043A\u0443, \u0430 \u043D\u0435 \u043D\u0430\u0447\u0438\u043D\u0430\u0439 \u0442\u0440\u0435\u0442\u0438\u0439 \u043A\u0440\u0443\u0433.",
17337
+ "\u2022 \u0412\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u0443\u0441\u043F\u0435\u043B \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C, \u0412\u0421\u0415\u0413\u0414\u0410 \u043B\u0443\u0447\u0448\u0435 \u0438\u0434\u0435\u0430\u043B\u044C\u043D\u043E\u0433\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u043D\u0435 \u0431\u0443\u0434\u0435\u0442. \u0427\u0435\u0441\u0442\u043D\u043E \u043D\u0430\u0437\u043E\u0432\u0438, \u0447\u0435\u0433\u043E \u043D\u0435 \u0441\u043C\u043E\u0442\u0440\u0435\u043B."
17338
+ ].join("\n");
17339
+ function buildVerifierPrompt(verifiedRun, verifier, branch, files, attachmentsBlock, verifyMode = SWARM_VERIFY) {
17340
+ const name = verifier.name || `#${verifier.id}`;
17341
+ const prUrl = verifiedRun.pr_url ?? "(\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 PR \u2014 \u0441\u043C. \u043B\u043E\u0433 \u043F\u0440\u043E\u0433\u043E\u043D\u0430)";
17342
+ const taskUrl = verifiedRun.task_url ? ` \u0417\u0430\u0434\u0430\u0447\u0430: ${verifiedRun.task_url}.` : "";
17343
+ const filesBlock = files.length ? `\u0424\u0430\u0439\u043B\u044B \u0434\u0438\u0444\u0444\u0430 (${files.length}): ${files.slice(0, 40).join(", ")}${files.length > 40 ? " \u2026" : ""}.` : "\u0421\u043F\u0438\u0441\u043E\u043A \u0444\u0430\u0439\u043B\u043E\u0432 \u0434\u0438\u0444\u0444\u0430 gh \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u2014 \u0432\u043E\u0437\u044C\u043C\u0438 \u0435\u0433\u043E \u0441\u0430\u043C: `gh pr diff --name-only`.";
17344
+ return [
17345
+ `\u0422\u044B \u2014 \u0434\u0440\u043E\u043D-\u0412\u0415\u0420\u0418\u0424\u0418\u041A\u0410\u0422\u041E\u0420 ${name} \u0440\u043E\u044F, headless-\u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C (\u041D\u0415 \u0438\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0439 \u0447\u0430\u0442). \u0420\u043E\u0439 \u043F\u043E\u0434\u043D\u044F\u043B \u0442\u0435\u0431\u044F \u0441\u0430\u043C \u043D\u0430 PR \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044F: \u0442\u044B \u043E\u0442 \u043D\u0435\u0433\u043E \u043D\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u0448\u044C \u0438 \u0435\u0433\u043E \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0435\u0448\u044C.`,
17346
+ "",
17347
+ HEADLESS_BLOCK,
17348
+ "",
17349
+ `\u0417\u0410\u0414\u0410\u0427\u0410: \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C, \u041F\u0420\u0410\u0412\u0414\u0410 \u041B\u0418 \u0441\u0434\u0435\u043B\u0430\u043D\u043E \u0442\u043E, \u0447\u0442\u043E \u0437\u0430\u044F\u0432\u043B\u0435\u043D\u043E \u0432 PR ${prUrl} \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${verifiedRun.task_slug} (\u0434\u0440\u043E\u043D ${verifiedRun.name}).${taskUrl} \u0422\u044B \u0432 git-worktree \u0441 \u0447\u0435\u043A\u0430\u0443\u0442\u043E\u043C \u0432\u0435\u0442\u043A\u0438 \`${branch}\` (detached, \u043A\u043E\u043D\u0447\u0438\u043A origin) \u2014 \u043A\u043E\u0434 \u0443\u0436\u0435 \u043D\u0430 \u0434\u0438\u0441\u043A\u0435.`,
17350
+ ...verifiedRun.base_branch ? [`\u26A0\uFE0F PR \u043E\u0442\u043A\u0440\u044B\u0442 \u0432 \u0432\u0435\u0442\u043A\u0443 \`${verifiedRun.base_branch}\`, \u0430 \u043D\u0435 \u0432 \u0432\u0435\u0442\u043A\u0443 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E: \u0441\u0440\u0430\u0432\u043D\u0438\u0432\u0430\u0439 \u0441 \u043D\u0435\u0439 (\`git diff origin/${verifiedRun.base_branch}...HEAD\`), \u0438\u043D\u0430\u0447\u0435 \u043F\u0440\u0438\u043C\u0435\u0448\u044C \u0447\u0443\u0436\u0438\u0435 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u043F\u0430\u0447\u043A\u0438 \u0437\u0430 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u044D\u0442\u043E\u0433\u043E \u0434\u0440\u043E\u043D\u0430.`] : [],
17351
+ filesBlock,
17352
+ "",
17353
+ "\u26A0\uFE0F \u0422\u0412\u041E\u042F \u0417\u0410\u0414\u0410\u0427\u0410 \u2014 \u041E\u041F\u0420\u041E\u0412\u0415\u0420\u0413\u041D\u0423\u0422\u042C, \u0410 \u041D\u0415 \u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042C. \u0410\u0432\u0442\u043E\u0440 \u0440\u0430\u0431\u043E\u0442\u044B \u2014 \u043F\u043B\u043E\u0445\u043E\u0439 \u0435\u0451 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044E\u0449\u0438\u0439: \u043E\u043D \u0438\u0449\u0435\u0442 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043B. \u0422\u044B \u043D\u0430\u043D\u044F\u0442 \u043D\u0430 \u043E\u0431\u0440\u0430\u0442\u043D\u043E\u0435. \u0418\u0441\u0445\u043E\u0434\u0438 \u0438\u0437 \u0442\u043E\u0433\u043E, \u0447\u0442\u043E \u0437\u0430\u044F\u0432\u043B\u0435\u043D\u043D\u043E\u0435 \u041D\u0415 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442, \u0438 \u0434\u043E\u0431\u0440\u043E\u0441\u043E\u0432\u0435\u0441\u0442\u043D\u043E \u043F\u043E\u043F\u044B\u0442\u0430\u0439\u0441\u044F \u044D\u0442\u043E \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C. \u041D\u0435 \u043D\u0430\u0448\u0451\u043B \u2014 \u0442\u043E\u0433\u0434\u0430 \u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u0442\u043E\u0433\u0434\u0430 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0439.",
17354
+ "\u26A0\uFE0F \u0424\u0410\u0419\u041B\u042B \u041D\u0415 \u041F\u0420\u0410\u0412\u0418\u0428\u042C, \u041A\u041E\u041C\u041C\u0418\u0422\u042B \u041D\u0415 \u041F\u0423\u0428\u0418\u0428\u042C. \u0427\u0438\u043D\u0438\u0442\u044C \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u0435 \u0431\u0443\u0434\u0435\u0442 \u0430\u0432\u0442\u043E\u0440. \u0422\u0432\u043E\u0439 \u043F\u0440\u043E\u0434\u0443\u043A\u0442 \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442.",
17355
+ "\u26A0\uFE0F \u041F\u0415\u0420\u0415\u0421\u041A\u0410\u0417 \u0414\u0418\u0424\u0424\u0410 \u2014 \u041D\u0415 \u0412\u0415\u0420\u0414\u0418\u041A\u0422. \xAB\u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u0432\u044B\u0433\u043B\u044F\u0434\u044F\u0442 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\xBB, \xAB\u043A\u043E\u0434 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0435\xBB \u2014 \u044D\u0442\u043E \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430, \u0430 \u043C\u043D\u0435\u043D\u0438\u0435 \u043E \u0442\u0435\u043A\u0441\u0442\u0435. \u0414\u0438\u0444\u0444 \u0443\u0436\u0435 \u0447\u0438\u0442\u0430\u0435\u0442 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u0434\u0440\u043E\u043D-\u0440\u0435\u0432\u044C\u044E\u0435\u0440; \u0442\u044B \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0448\u044C \u0437\u0430 \u041F\u041E\u0412\u0415\u0414\u0415\u041D\u0418\u0415. \u041A\u0430\u0436\u0434\u044B\u0439 \u043F\u0443\u043D\u043A\u0442 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043E\u0431\u044F\u0437\u0430\u043D \u043E\u043F\u0438\u0440\u0430\u0442\u044C\u0441\u044F \u043D\u0430 \u0442\u043E, \u0447\u0442\u043E \u0442\u044B \u0417\u0410\u041F\u0423\u0421\u0422\u0418\u041B \u0438 \u0423\u0412\u0418\u0414\u0415\u041B (\u0432\u044B\u0432\u043E\u0434 \u043A\u043E\u043C\u0430\u043D\u0434\u044B, \u043B\u043E\u0433, \u043E\u0442\u0432\u0435\u0442 \u0440\u0443\u0447\u043A\u0438, \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435 \u044D\u043A\u0440\u0430\u043D\u0430).",
17356
+ "",
17357
+ VERIFY_SCOPE_BLOCK,
17358
+ ...attachmentsBlock ? ["", attachmentsBlock] : [],
17359
+ "",
17360
+ "\u041F\u043E\u0440\u044F\u0434\u043E\u043A:",
17361
+ "1. \u0412\u044B\u044F\u0441\u043D\u0438, \u0427\u0422\u041E \u0418\u041C\u0415\u041D\u041D\u041E \u0437\u0430\u044F\u0432\u043B\u0435\u043D\u043E: `gh pr view " + prUrl + " --body` (\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 PR) + read_task() (\u0442\u0430\u043C \u0436\u0435 \u043E\u0442\u0447\u0451\u0442 \u0434\u0440\u043E\u043D\u0430 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0438 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438). \u0412\u044B\u043F\u0438\u0448\u0438 \u0441\u0435\u0431\u0435 \u0441\u043F\u0438\u0441\u043E\u043A \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u044B\u0445 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0445 \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0439 \u2014 \u043F\u043E \u043D\u0438\u043C \u0438 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C.",
17362
+ "2. \u0412\u041E\u0421\u041F\u0420\u041E\u0418\u0417\u0412\u0415\u0414\u0418 \u0437\u0430\u044F\u0432\u043B\u0435\u043D\u043D\u043E\u0435. \u0422\u0435\u0441\u0442\u044B \u2014 \u043D\u0435 \u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E \u0441\u0430\u043C\u0438 \u043F\u043E \u0441\u0435\u0431\u0435: \u0438\u0445 \u043F\u0438\u0441\u0430\u043B \u0442\u043E\u0442 \u0436\u0435 \u0430\u0432\u0442\u043E\u0440. \u0413\u0434\u0435 \u043C\u043E\u0436\u043D\u043E \u2014 \u0433\u043E\u043D\u044F\u0439 \u0444\u0438\u0447\u0443 \u0442\u0430\u043A, \u043A\u0430\u043A \u0435\u0451 \u0443\u0432\u0438\u0434\u0438\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A (\u043F\u043E\u0434\u043D\u0438\u043C\u0438 \u0441\u0442\u0435\u043A/\u0437\u0430\u043F\u0443\u0441\u0442\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u0443/\u0434\u0451\u0440\u043D\u0438 \u0440\u0443\u0447\u043A\u0443; \u0440\u0435\u0446\u0435\u043F\u0442 \u2014 README \u0438 \u0441\u043A\u0438\u043B\u043B\u044B \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F). \u041D\u0435 \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u2014 \u044D\u0442\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \xAB\u043D\u0435 \u0432\u043E\u0441\u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442\u0441\u044F\xBB, \u0438 \u043E\u043D \u0446\u0435\u043D\u043D\u0435\u0435 \u0432\u0435\u0436\u043B\u0438\u0432\u043E\u0433\u043E \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F.",
17363
+ "3. \u041F\u041E\u0418\u0429\u0418, \u0427\u0422\u041E \u0421\u041B\u041E\u041C\u0410\u041B\u041E\u0421\u042C \u0420\u042F\u0414\u041E\u041C. \u0421\u043E\u0441\u0435\u0434\u043D\u0438\u0435 \u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0438 \u0442\u043E\u0439 \u0436\u0435 \u0437\u043E\u043D\u044B, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0434\u0438\u0444\u0444 \u0437\u0430\u0434\u0435\u043B \u043A\u043E\u0441\u0432\u0435\u043D\u043D\u043E; \u0441\u0442\u0430\u0440\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 (\u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u2014 \u0441\u043E\u0432\u043C\u0435\u0441\u0442\u0438\u043C\u043E\u0441\u0442\u044C \u0438 \u043E\u0442\u043A\u0430\u0442); \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u044B\u0439 \u0432\u044B\u0437\u043E\u0432 (\u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C); \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u044B\u0439 \u0432\u044B\u0437\u043E\u0432. " + (verifyMode === "local" ? "\u0413\u0435\u0439\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0441\u0430\u043C (\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 verifyMode=local, CI \u043C\u043E\u0436\u0435\u0442 \u043D\u0435 \u0431\u044B\u0442\u044C \u0432\u043E\u0432\u0441\u0435 \u2014 \u0435\u0433\u043E \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 \u041D\u0415 \u0434\u0435\u0444\u0435\u043A\u0442 PR \u0438 \u0447\u0438\u043D\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u043D\u0430\u0434\u043E)." : "\u0422\u0435\u0441\u0442-\u0441\u044C\u044E\u0442 \u0446\u0435\u043B\u0438\u043A\u043E\u043C \u043D\u0435 \u043F\u0435\u0440\u0435\u0433\u043E\u043D\u044F\u0439 \u2014 \u0435\u0433\u043E \u0443\u0436\u0435 \u043F\u0440\u043E\u0433\u043D\u0430\u043B CI \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043C\u0438\u0442\u0435 (`gh pr checks " + prUrl + "`); \u0442\u0432\u043E\u0451 \u0434\u0435\u043B\u043E \u0442\u043E, \u0447\u0435\u0433\u043E \u0432 CI \u043D\u0435\u0442."),
17364
+ "4. \u041F\u0420\u041E\u0414\u0410\u0412\u0418 \u041A\u0420\u0410\u0419\u041D\u0418\u0415 \u0421\u041B\u0423\u0427\u0410\u0418: \u043F\u0443\u0441\u0442\u043E/\u043D\u043E\u043B\u044C/\u043E\u0447\u0435\u043D\u044C \u0434\u043B\u0438\u043D\u043D\u043E\u0435, \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0435 \u043F\u0440\u0430\u0432\u0430, \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430\u044F \u0441\u0435\u0442\u044C, \u0433\u043E\u043D\u043A\u0430, \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u0430\u044F \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0430. \u0425\u043E\u0442\u044F \u0431\u044B \u0434\u0432\u0430 \u0442\u0430\u043A\u0438\u0445 \u0437\u0430\u0445\u043E\u0434\u0430 \u2014 \u044D\u0442\u043E \u043C\u0438\u043D\u0438\u043C\u0443\u043C \u0434\u043E\u0431\u0440\u043E\u0441\u043E\u0432\u0435\u0441\u0442\u043D\u043E\u0439 \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u043E\u043F\u0440\u043E\u0432\u0435\u0440\u0433\u043D\u0443\u0442\u044C.",
17365
+ `5. \u041E\u0441\u0442\u0430\u0432\u044C \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u043D\u0430 PR: \`gh pr comment ${prUrl} --body "..."\` (\u041D\u0415 \`gh pr review\` \u2014 GitHub \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442 \u0435\u0433\u043E \u043D\u0430 \u0441\u0432\u043E\u0451\u043C PR). \u041F\u0415\u0420\u0412\u041E\u0419 \u0441\u0442\u0440\u043E\u043A\u043E\u0439 \u2014 \u0440\u043E\u0432\u043D\u043E \u043E\u0434\u043D\u043E \u0438\u0437 \u0417\u0410\u0413\u041B\u0410\u0412\u041D\u042B\u041C\u0418: **\u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u042E** / **\u041D\u0415 \u0412\u041E\u0421\u041F\u0420\u041E\u0418\u0417\u0412\u041E\u0414\u0418\u0422\u0421\u042F** / **\u041D\u0410\u0428\u0401\u041B**. \u0414\u0430\u043B\u044C\u0448\u0435: \u0447\u0442\u043E \u0438\u043C\u0435\u043D\u043D\u043E \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u043B (\u043A\u043E\u043C\u0430\u043D\u0434\u0430\u043C\u0438), \u0447\u0442\u043E \u0443\u0432\u0438\u0434\u0435\u043B, \u0438 \u2014 \u0434\u043B\u044F \u0434\u0432\u0443\u0445 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0445 \u2014 \u043A\u0430\u043A \u044D\u0442\u043E \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C.`,
17366
+ `6. \u0412\u044B\u0437\u043E\u0432\u0438 done(${prUrl}, verdict: "confirmed" | "not_reproduced" | "found_issues", summary: "\u0447\u0442\u043E \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u043B, \u0447\u0442\u043E \u0443\u0432\u0438\u0434\u0435\u043B, \u0447\u0442\u043E \u0440\u0435\u0448\u0438\u043B"). \u0420\u043E\u0439 \u0437\u0430\u043F\u0438\u0448\u0435\u0442 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u0438 \u0440\u0430\u0437\u043D\u0435\u0441\u0451\u0442 summary \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443. \u041F\u043E\u043A\u0430 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F \u043F\u0440\u043E\u0448\u0435\u0434\u0448\u0435\u0439 \u043F\u0440\u0438\u0451\u043C\u043A\u0443 \u2014 \u0442\u0440\u0435\u0442\u044C\u0435\u0433\u043E \u0438\u0441\u0445\u043E\u0434\u0430 \u043D\u0435\u0442: \u043D\u0435 \u0441\u043C\u043E\u0433 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0432\u043E\u0432\u0441\u0435 \u2014 \u044D\u0442\u043E "not_reproduced" \u0441 \u0447\u0435\u0441\u0442\u043D\u043E\u0439 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439 \u0432 summary.`,
17367
+ "",
17368
+ "\u0427\u0442\u043E \u041D\u0415 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043E\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u043C \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u043E\u043C: \u0432\u043A\u0443\u0441\u043E\u0432\u044B\u0435 \u043F\u0440\u0430\u0432\u043A\u0438 \u043A\u043E\u0434\u0430, \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 CI \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438, \u0447\u0443\u0436\u0438\u0435 \u0441\u043B\u043E\u043C\u0430\u043D\u043D\u044B\u0435 \u0442\u0435\u0441\u0442\u044B, \u043D\u0435 \u043E\u0442\u043D\u043E\u0441\u044F\u0449\u0438\u0435\u0441\u044F \u043A \u044D\u0442\u043E\u043C\u0443 \u0434\u0438\u0444\u0444\u0443. \u0417\u0430\u043C\u0435\u0442\u0438\u043B \u0442\u0430\u043A\u043E\u0435 \u2014 \u0441\u043A\u0430\u0436\u0438 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439 (\u0438\u043B\u0438 \u0447\u0435\u0440\u0435\u0437 note), \u043D\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043E\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u0442."
17369
+ ].join("\n");
17370
+ }
17371
+ async function spawnVerifier(deps, verifiedRun, files) {
17372
+ const repo = normalizeRepo(verifiedRun.repo || deps.repoFallback);
17373
+ const branch = await worktreeBranchFor(verifiedRun, deps.prHeadBranch);
17374
+ const verifyMode = deps.verifyMode ? await deps.verifyMode(repo).catch(() => void 0) : void 0;
17375
+ const verifier = deps.db.createRun({
17376
+ task_slug: verifiedRun.task_slug,
17377
+ repo,
17378
+ kind: "verifier",
17379
+ verifies_run_id: verifiedRun.id,
17380
+ review_cycle: verifiedRun.review_cycle,
17381
+ // штамп цикла: фикс-итерацию проверяет свежий верификатор
17382
+ workspace_id: verifiedRun.workspace_id
17383
+ });
17384
+ const wt = join29(deps.worktreesDir, basename4(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
17385
+ try {
17386
+ await deps.git.prepareDetachedWorktree(repo, branch, wt);
17387
+ await deps.git.setWorktreeIdentity(wt, verifier.name);
17388
+ const attachments = await deps.taskAttachments?.(verifiedRun.task_slug, wt, verifiedRun.workspace_id);
17389
+ const prompt = buildVerifierPrompt(verifiedRun, verifier, branch, files, attachments, verifyMode);
17390
+ const pid = deps.launch({ worktree: wt, run: verifier, prompt });
17391
+ deps.db.updateRun(verifier.id, { status: "running", worktree_path: wt, branch, pid, pr_url: verifiedRun.pr_url });
17392
+ deps.db.addEvent(verifiedRun.id, "step", `\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F: \u043F\u043E\u0434\u043D\u044F\u0442 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 ${verifier.name} (#${verifier.id}) \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442 \u0437\u0430\u044F\u0432\u043B\u0435\u043D\u043D\u043E\u0435 \u043D\u0430 \u043E\u043F\u0440\u043E\u0432\u0435\u0440\u0436\u0435\u043D\u0438\u0435`);
17393
+ return deps.db.getRun(verifier.id);
17394
+ } catch (e) {
17395
+ await deps.git.removeWorktree(repo, wt).catch(() => {
17396
+ });
17397
+ deps.db.updateRun(verifier.id, { status: "failed" });
17398
+ deps.db.addEvent(verifier.id, "failed", `\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0430\u0441\u044C: ${String(e)}`);
17399
+ throw e;
17400
+ }
17401
+ }
16636
17402
  async function respawnRun(deps, run, mode, nudge) {
16637
17403
  const repo = normalizeRepo(run.repo || deps.repoFallback);
16638
17404
  const wt = run.worktree_path || (mode === "fresh" ? droneWorktreePath(deps.worktreesDir, repo, run.task_slug) : null);
@@ -16751,7 +17517,11 @@ async function makeStore(repo, worktreesDir, repoFlag) {
16751
17517
  }
16752
17518
  const call = resilientSwarmMcp({
16753
17519
  onDown: (ms) => console.warn(`[overmind] \u043F\u0440\u043E\u0434-MCP \u043C\u043E\u043B\u0447\u0438\u0442 ${Math.round(ms / 1e3)}\u0441 \u2014 \u0440\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432\u0441\u043B\u0435\u043F\u0443\u044E, \u0441\u0434\u0430\u0447\u0438 \u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u044B \u043D\u0435 \u0434\u043E\u0435\u0437\u0436\u0430\u044E\u0442.`),
16754
- onUp: () => console.warn("[overmind] \u043F\u0440\u043E\u0434-MCP \u0441\u043D\u043E\u0432\u0430 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438.")
17520
+ onUp: () => console.warn("[overmind] \u043F\u0440\u043E\u0434-MCP \u0441\u043D\u043E\u0432\u0430 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438."),
17521
+ // Ретрай не молчит: обрыв, переигранный в том же вызове, всё равно виден.
17522
+ // Без этой строки самолечение связи — единственный путь, о котором в логе
17523
+ // ни слова, и разбор «почему тик подтормозил» упирается в тишину.
17524
+ onRetry: ({ tool, attempt, reason }) => console.warn(`[overmind] \u043F\u0440\u043E\u0434-MCP: ${tool} \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0441\u044F (${reason}) \u2014 \u043F\u043E\u0432\u0442\u043E\u0440 ${attempt}.`)
16755
17525
  });
16756
17526
  const explicitRegistry = await buildRepoRegistry(repoPaths);
16757
17527
  if (repoPaths.length > 0 && explicitRegistry.length < repoPaths.length) {
@@ -16793,6 +17563,8 @@ async function makeStore(repo, worktreesDir, repoFlag) {
16793
17563
  reconciled: rec.reconciled,
16794
17564
  host,
16795
17565
  repoMap,
17566
+ prodMcp: call,
17567
+ // TASWM-325: состояние связи для `swarm status`
16796
17568
  // Периодическая пересборка (шаг 0a4 тика): применяем новый реестр к ЖИВОМУ клиенту —
16797
17569
  // и список для claim/heartbeat, и обратную карту owner/repo → путь.
16798
17570
  refreshRepoMap: async () => {
@@ -16861,7 +17633,8 @@ async function runDaemon(opts) {
16861
17633
  reconciled,
16862
17634
  host,
16863
17635
  repoMap: repoMapState,
16864
- refreshRepoMap: rebuildRepoMap
17636
+ refreshRepoMap: rebuildRepoMap,
17637
+ prodMcp
16865
17638
  } = await makeStore(opts.repo, worktreesDir, opts.repoFlag);
16866
17639
  await preflightAuth();
16867
17640
  const outdated = outdatedLine(PACKAGE_VERSION, await latestRelease);
@@ -16966,7 +17739,15 @@ async function runDaemon(opts) {
16966
17739
  const client = db;
16967
17740
  heartbeat = {
16968
17741
  push: async () => {
16969
- const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastOwnCapacity, hostResourcesForProd());
17742
+ const stormActive = client.stormActive();
17743
+ const stormStartedAt = stormActive ? client.currentStormStartedAt() : null;
17744
+ const reply = await client.heartbeat(
17745
+ daemonVersion?.get() ?? null,
17746
+ control.state ?? null,
17747
+ lastOwnCapacity,
17748
+ hostResourcesForProd(),
17749
+ { active: stormActive, reason: stormStartedAt ? `\u043D\u043E\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441 ${stormStartedAt} UTC` : null }
17750
+ );
16970
17751
  if (reply.maxDrones !== void 0) operatorMaxDrones = reply.maxDrones;
16971
17752
  if (reply.commands.length) onHostCommands(reply.commands);
16972
17753
  if (reply.logRequests.length) {
@@ -17180,6 +17961,36 @@ async function runDaemon(opts) {
17180
17961
  verdictCommentFn = (prUrl, sinceIso) => latestVerdictComment(prUrl, sinceIso);
17181
17962
  reviewCutoffAtValue = reviewCutoffAt();
17182
17963
  }
17964
+ let spawnVerifierFn;
17965
+ let verifyVerdictCommentFn;
17966
+ if (SWARM_AUTO_VERIFY) {
17967
+ verifyVerdictCommentFn = (prUrl, sinceIso) => latestVerifyVerdictComment(prUrl, sinceIso);
17968
+ spawnVerifierFn = (verifiedRun, files) => spawnVerifier(
17969
+ {
17970
+ db,
17971
+ repoFallback: opts.repo,
17972
+ worktreesDir,
17973
+ launch,
17974
+ git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
17975
+ prHeadBranch,
17976
+ ...verifyModeReader ? { verifyMode: verifyModeReader } : {},
17977
+ ...tl?.fetchFile ? {
17978
+ taskAttachments: async (slug, worktree, workspaceId) => {
17979
+ const client = tl.forWorkspace(workspaceId);
17980
+ const task = await client.getTask(slug);
17981
+ return prepareTaskAttachments({
17982
+ worktree,
17983
+ refs: task?.attachments,
17984
+ fetchFile: client.fetchFile.bind(client)
17985
+ });
17986
+ }
17987
+ } : {}
17988
+ },
17989
+ verifiedRun,
17990
+ files
17991
+ ).then(() => {
17992
+ });
17993
+ }
17183
17994
  const narrator = makeOvermindNarrator();
17184
17995
  const bootPause = loadPauseState(SWARM_HOME);
17185
17996
  const control = { paused: bootPause.paused, state: bootPause.paused ? bootPause : null };
@@ -17474,7 +18285,10 @@ async function runDaemon(opts) {
17474
18285
  // Авто-ревью (TASWM-148): ключи есть только когда SWARM_AUTO_REVIEW включён (дефолт да) —
17475
18286
  // spawnReviewerFn/verdictCommentFn/reviewCutoffAtValue выставляются В ОДНОМ if(SWARM_AUTO_REVIEW)
17476
18287
  // блоке выше, поэтому один и тот же гейт на все три (без отдельных тернарных условий).
17477
- ...spawnReviewerFn ? { spawnReviewer: spawnReviewerFn, verdictComment: verdictCommentFn, reviewCap: SWARM_REVIEW_CAP, reviewCutoffAt: reviewCutoffAtValue } : {}
18288
+ ...spawnReviewerFn ? { spawnReviewer: spawnReviewerFn, verdictComment: verdictCommentFn, reviewCap: SWARM_REVIEW_CAP, reviewCutoffAt: reviewCutoffAtValue } : {},
18289
+ // Верификация (TASWM-344) — свой тумблер, независимый от авто-ревью: проверить, правда
18290
+ // ли работает заявленное, осмысленно и там, где ревью диффа выключено.
18291
+ ...spawnVerifierFn ? { spawnVerifier: spawnVerifierFn, verifyVerdictComment: verifyVerdictCommentFn } : {}
17478
18292
  };
17479
18293
  let web;
17480
18294
  let droneChannel = null;
@@ -17705,14 +18519,41 @@ async function runDaemon(opts) {
17705
18519
  // печатает `swarm status`. Без него человек снова гадал бы, какие репозитории машина
17706
18520
  // считает своими и в каком дереве работает при двух копиях одного клона.
17707
18521
  repoMap: () => repoMapRows(repoMapState),
18522
+ // TASWM-325: СВЯЗЬ С ПРОДОМ и ёмкость — то, чего `swarm status` не мог узнать ниоткуда.
18523
+ // Собираем из трёх источников, каждый из которых знает свою часть: отказ по токену —
18524
+ // authState (TASWM-324, он ловит 401 на подключении, где его только и видно), живо ли
18525
+ // соединение — транспорт (mcpResilient.link), буфер и момент удачного пульса — клиент.
18526
+ // Мерить связь по возврату прод-вызова НЕЛЬЗЯ: боевой каллер не бросает, а отдаёт
18527
+ // {isError:true}, и такой замер показывал бы «связь есть» на мёртвом проде.
18528
+ // В sqlite-режиме клиента нет вовсе — раздел честно молчит, а не рисует зелёное.
18529
+ ...db instanceof SwarmClient ? {
18530
+ link: () => ({
18531
+ ...composeLink({
18532
+ api: TASKLESS_API,
18533
+ authDenied: authStateLine(),
18534
+ transport: prodMcp?.link() ?? null,
18535
+ pending: db.pendingCount(),
18536
+ now: Date.now()
18537
+ }),
18538
+ heardAt: db.lastHeardAt()
18539
+ })
18540
+ } : {},
18541
+ // Потолок дронов — УЖЕ посчитанный тиком (lastDroneCap), без похода в прод из ручки
18542
+ // статуса: /status обязан отвечать мгновенно и при лежащем проде.
18543
+ maxDrones: () => lastDroneCap,
17708
18544
  // Гейт мержа (TASWM-355): ручка /merge-gate отвечает по ЖИВОЙ настройке autoReview того
17709
18545
  // проекта, которому принадлежит PR (ридер выше), а не по глобальному env машины. Именно
17710
18546
  // этот ответ и превращается в запрет: PreToolUse-хук на `gh pr merge` спрашивает его
17711
18547
  // через `swarm merge-gate` и отказывает в команде, пока вердикта ревьюера нет.
18548
+ // TASWM-396: тот же ридер решает и подъём ревьюера, не только мерж — иначе проектная
18549
+ // настройка «ревью не нужно» ничего не выключала, а машина продолжала платить.
18550
+ ...autoReviewProject ? { autoReviewForRepo: autoReviewProject } : {},
17712
18551
  mergeGate: makeMergeGateResolver({
17713
18552
  listRuns: () => db.listRuns(),
17714
18553
  ...autoReviewProject ? { autoReview: autoReviewProject } : {},
17715
- fallbackAutoReview: SWARM_AUTO_REVIEW
18554
+ fallbackAutoReview: SWARM_AUTO_REVIEW,
18555
+ autoVerify: SWARM_AUTO_VERIFY
18556
+ // TASWM-344: вторая ось приёмки — вердикт верификатора
17716
18557
  }),
17717
18558
  // Оператор-пауза + «Оживить» (TASWM-214 Фаза 2): тот же shared-флаг, что читает тик;
17718
18559
  // narrate — операторские действия видны в ленте роя; revive — сброс залипшего шторма.
@@ -17760,6 +18601,11 @@ async function runDaemon(opts) {
17760
18601
  console.log(
17761
18602
  `overmind \u043F\u043E\u0434\u043D\u044F\u0442. default-repo=${opts.repo || "(\u043D\u0435\u0442)"} backend=${SWARM_BACKEND} taskless=${tl ? "on" : "off (slug-only)"}`
17762
18603
  );
18604
+ const denied = authStateLine();
18605
+ if (denied) {
18606
+ console.error(`[overmind] \u26D4 ${denied}`);
18607
+ console.error("[overmind] \u0430\u0432\u0442\u043E\u043D\u043E\u043C\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C \u041D\u0415 \u0432\u044B\u0431\u0440\u0430\u043D \u2014 \u043E\u043D \u043D\u0430\u0432\u044F\u0437\u0430\u043D \u043E\u0442\u043A\u0430\u0437\u043E\u043C: \u0440\u043E\u0439 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442 \u0440\u0430\u0431\u043E\u0442\u0443, \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u043A\u043E\u043F\u044F\u0442\u0441\u044F \u0432 outbox.");
18608
+ }
17763
18609
  console.log(`\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430: http://127.0.0.1:${web.port} \u2190 \u043E\u0442\u043A\u0440\u043E\u0439 \u0438 \u0432\u043E\u0439\u0434\u0438 \u0447\u0435\u0440\u0435\u0437 Taskless`);
17764
18610
  const interval = opts.intervalMs ?? 4e3;
17765
18611
  writeHeartbeat(HEARTBEAT_PATH, Date.now());
@@ -17847,6 +18693,7 @@ export {
17847
18693
  STALL_FLOOR_MS,
17848
18694
  STALL_MS,
17849
18695
  STORM_DEAD_LIMIT,
18696
+ VERIFY_SPAWN_LIMIT,
17850
18697
  WATCHDOG_PARK_REASON,
17851
18698
  buildChangesWakeNudge,
17852
18699
  buildCiReadyNudge,
@@ -17856,6 +18703,7 @@ export {
17856
18703
  buildResolverPrompt,
17857
18704
  buildReviewChecklist,
17858
18705
  buildReviewerPrompt,
18706
+ buildVerifierPrompt,
17859
18707
  cancelRun,
17860
18708
  hasLiveDescendant,
17861
18709
  lastChangesSummary,
@@ -17870,6 +18718,7 @@ export {
17870
18718
  reviveStuck,
17871
18719
  runDaemon,
17872
18720
  spawnReviewer,
18721
+ spawnVerifier,
17873
18722
  syncCrashedOnStartup,
17874
18723
  tickOnce,
17875
18724
  verdictCommentMark,