@taskless-app/swarm 0.6.4 → 0.6.6

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 +302 -42
  2. package/dist/overmind.js +501 -64
  3. package/package.json +2 -2
package/dist/overmind.js CHANGED
@@ -765,6 +765,32 @@ var init_hosts = __esm({
765
765
  }
766
766
  });
767
767
 
768
+ // ../swarm-core/src/hostResources.ts
769
+ function parseHostResources(raw) {
770
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
771
+ const v = raw;
772
+ const num = (x) => typeof x === "number" && Number.isFinite(x) ? x : null;
773
+ const memFreeGb = num(v.memFreeGb);
774
+ const memTotalGb = num(v.memTotalGb);
775
+ if (memFreeGb == null || memTotalGb == null) return null;
776
+ if (typeof v.measuredAt !== "string" || Number.isNaN(Date.parse(v.measuredAt))) return null;
777
+ const cpu = num(v.cpuBusyPct);
778
+ return {
779
+ cpuBusyPct: cpu == null ? null : Math.min(100, Math.max(0, cpu)),
780
+ memFreeGb,
781
+ memTotalGb,
782
+ diskFreeGb: num(v.diskFreeGb),
783
+ diskTotalGb: num(v.diskTotalGb),
784
+ measuredAt: v.measuredAt
785
+ };
786
+ }
787
+ var init_hostResources = __esm({
788
+ "../swarm-core/src/hostResources.ts"() {
789
+ "use strict";
790
+ init_hosts();
791
+ }
792
+ });
793
+
768
794
  // ../swarm-core/src/hostPool.ts
769
795
  var init_hostPool = __esm({
770
796
  "../swarm-core/src/hostPool.ts"() {
@@ -854,7 +880,12 @@ var init_cost = __esm({
854
880
  });
855
881
 
856
882
  // ../swarm-core/src/runGuards.ts
857
- var DEFAULT_RUN_LIMITS;
883
+ function remainingRunBudgetUsd(maxCostUsd, spentUsd) {
884
+ if (!(maxCostUsd > 0)) return null;
885
+ const spent = Number.isFinite(spentUsd) && spentUsd > 0 ? spentUsd : 0;
886
+ return Math.max(MIN_RUN_BUDGET_USD, Math.round((maxCostUsd - spent) * 100) / 100);
887
+ }
888
+ var DEFAULT_RUN_LIMITS, MIN_RUN_BUDGET_USD;
858
889
  var init_runGuards = __esm({
859
890
  "../swarm-core/src/runGuards.ts"() {
860
891
  "use strict";
@@ -864,6 +895,7 @@ var init_runGuards = __esm({
864
895
  stepTimeoutMs: 90 * 6e4,
865
896
  lostMs: 30 * 6e4
866
897
  };
898
+ MIN_RUN_BUDGET_USD = 0.05;
867
899
  }
868
900
  });
869
901
 
@@ -1267,6 +1299,20 @@ var init_codeZones = __esm({
1267
1299
  }
1268
1300
  });
1269
1301
 
1302
+ // ../swarm-core/src/effort.ts
1303
+ function toEffortLevel(raw) {
1304
+ const s = typeof raw === "string" ? raw.trim().toLowerCase() : "";
1305
+ return EFFORT_SET.has(s) ? s : null;
1306
+ }
1307
+ var EFFORT_LEVELS, EFFORT_SET;
1308
+ var init_effort = __esm({
1309
+ "../swarm-core/src/effort.ts"() {
1310
+ "use strict";
1311
+ EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
1312
+ EFFORT_SET = new Set(EFFORT_LEVELS);
1313
+ }
1314
+ });
1315
+
1270
1316
  // ../swarm-core/src/index.ts
1271
1317
  var init_src = __esm({
1272
1318
  "../swarm-core/src/index.ts"() {
@@ -1284,12 +1330,14 @@ var init_src = __esm({
1284
1330
  init_names();
1285
1331
  init_baseBranch();
1286
1332
  init_hosts();
1333
+ init_hostResources();
1287
1334
  init_hostPool();
1288
1335
  init_hostCommands();
1289
1336
  init_relief();
1290
1337
  init_cost();
1291
1338
  init_runGuards();
1292
1339
  init_codeZones();
1340
+ init_effort();
1293
1341
  }
1294
1342
  });
1295
1343
 
@@ -1470,14 +1518,32 @@ function launchCostUsd(result, model) {
1470
1518
  return costFromUsage(result.usage, model);
1471
1519
  }
1472
1520
  function launchSpend(result) {
1473
- const u = result.usage ?? {};
1474
1521
  const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1475
- return {
1522
+ const sum = (u) => ({
1476
1523
  tokensIn: num(u.input_tokens) + num(u.cache_creation_input_tokens) + num(u.cache_read_input_tokens),
1477
- tokensOut: num(u.output_tokens),
1478
- durationMs: num(result.duration_ms)
1524
+ tokensOut: num(u.output_tokens)
1525
+ });
1526
+ const spend = sum(result.usage ?? {});
1527
+ if (spend.tokensIn === 0 && spend.tokensOut === 0 && result.modelUsage) {
1528
+ for (const m of Object.values(result.modelUsage)) {
1529
+ const perModel = sum(asTokenUsage(m));
1530
+ spend.tokensIn += perModel.tokensIn;
1531
+ spend.tokensOut += perModel.tokensOut;
1532
+ }
1533
+ }
1534
+ return { ...spend, durationMs: num(result.duration_ms) };
1535
+ }
1536
+ function asTokenUsage(m) {
1537
+ return {
1538
+ ...m?.inputTokens !== void 0 ? { input_tokens: m.inputTokens } : {},
1539
+ ...m?.outputTokens !== void 0 ? { output_tokens: m.outputTokens } : {},
1540
+ ...m?.cacheCreationInputTokens !== void 0 ? { cache_creation_input_tokens: m.cacheCreationInputTokens } : {},
1541
+ ...m?.cacheReadInputTokens !== void 0 ? { cache_read_input_tokens: m.cacheReadInputTokens } : {}
1479
1542
  };
1480
1543
  }
1544
+ function budgetExhausted(result) {
1545
+ return result.subtype === "error_max_budget_usd" || result.terminal_reason === "budget_exhausted";
1546
+ }
1481
1547
  var init_claudeResult = __esm({
1482
1548
  "src/claudeResult.ts"() {
1483
1549
  "use strict";
@@ -1489,8 +1555,10 @@ var init_claudeResult = __esm({
1489
1555
  import { readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
1490
1556
  import { homedir } from "os";
1491
1557
  import { join as join2 } from "path";
1492
- function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
1558
+ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings, maxBudgetUsd, effort) {
1493
1559
  const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
1560
+ const budgetFlags = typeof maxBudgetUsd === "number" && maxBudgetUsd > 0 ? ["--max-budget-usd", String(maxBudgetUsd)] : [];
1561
+ const effortFlags = effort ? ["--effort", effort] : [];
1494
1562
  const base3 = [
1495
1563
  "--dangerously-skip-permissions",
1496
1564
  ...ISOLATION_FLAGS,
@@ -1498,6 +1566,8 @@ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs
1498
1566
  cfgPath,
1499
1567
  ...settings ? ["--settings", settings] : [],
1500
1568
  ...pluginFlags,
1569
+ ...budgetFlags,
1570
+ ...effortFlags,
1501
1571
  "--output-format",
1502
1572
  "json",
1503
1573
  "-p",
@@ -1574,7 +1644,7 @@ var init_claudeCode = __esm({
1574
1644
  const settings = spec.denyPaths.length > 0 ? droneSettingsJson(spec.denyPaths) : void 0;
1575
1645
  return {
1576
1646
  bin: resolveClaudeBin(),
1577
- args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings)
1647
+ args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings, spec.maxBudgetUsd, spec.effort)
1578
1648
  };
1579
1649
  },
1580
1650
  parseResult(raw, model) {
@@ -1587,7 +1657,10 @@ var init_claudeCode = __esm({
1587
1657
  ...est.warning ? { warning: est.warning } : {},
1588
1658
  tokensIn: spend.tokensIn,
1589
1659
  tokensOut: spend.tokensOut,
1590
- durationMs: spend.durationMs
1660
+ durationMs: spend.durationMs,
1661
+ // TASWM-377: «упёрся в потолок» — отдельный факт об этом же итоге, а не отдельный
1662
+ // разбор файла: у claude он лежит в тех же полях, что деньги и токены.
1663
+ ...budgetExhausted(result) ? { stoppedByBudget: true } : {}
1591
1664
  };
1592
1665
  },
1593
1666
  session: {
@@ -1747,6 +1820,13 @@ ${notice}` : spec.prompt;
1747
1820
  SKIP_GIT_CHECK_FLAG,
1748
1821
  BYPASS_FLAG,
1749
1822
  ...spec.model ? ["-m", spec.model] : [],
1823
+ // TASWM-395: УСИЛИЕ РАССУЖДЕНИЯ (spec.effort) codex пока НЕ получает — и это сказано
1824
+ // вслух, как требует контракт адаптера (см. ExecutorLaunchSpec.effort), а не забыто.
1825
+ // Кандидат есть — `-c model_reasoning_effort=<low|medium|high>` — но словари не совпадают
1826
+ // (у нас пять уровней, xhigh/max пришлось бы схлопывать в high), а главное: непонятый
1827
+ // ключ в `-c` роняет НЕ один флаг, а весь спавн. Проверять это надо на живом codex, и
1828
+ // до такой проверки правильный размен — работающий codex на дефолтном усилии, а не
1829
+ // сломанный на настроенном. Провода до сюда доведены: останется дописать одну строку.
1750
1830
  ...mcpConfigArgs(spec),
1751
1831
  prompt
1752
1832
  ];
@@ -1849,6 +1929,10 @@ __export(config_exports, {
1849
1929
  SWARM_AUTO_REVIEW: () => SWARM_AUTO_REVIEW,
1850
1930
  SWARM_BACKEND: () => SWARM_BACKEND,
1851
1931
  SWARM_CLAIM_INTERVAL_MS: () => SWARM_CLAIM_INTERVAL_MS,
1932
+ SWARM_CPU_SANITY_BUSY: () => SWARM_CPU_SANITY_BUSY,
1933
+ SWARM_CPU_SANITY_LAG_MS: () => SWARM_CPU_SANITY_LAG_MS,
1934
+ SWARM_CPU_SANITY_LOAD_FACTOR: () => SWARM_CPU_SANITY_LOAD_FACTOR,
1935
+ SWARM_CPU_WINDOW_MS: () => SWARM_CPU_WINDOW_MS,
1852
1936
  SWARM_DRONE_BRIDGE_MS: () => SWARM_DRONE_BRIDGE_MS,
1853
1937
  SWARM_HOME: () => SWARM_HOME,
1854
1938
  SWARM_LOW_DISK_GB: () => SWARM_LOW_DISK_GB,
@@ -1973,7 +2057,7 @@ function reviewCutoffAt() {
1973
2057
  }
1974
2058
  return at;
1975
2059
  }
1976
- 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_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;
2060
+ 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;
1977
2061
  var init_config = __esm({
1978
2062
  "src/config.ts"() {
1979
2063
  "use strict";
@@ -2003,6 +2087,10 @@ var init_config = __esm({
2003
2087
  SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
2004
2088
  SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
2005
2089
  SWARM_MAX_CPU_BUSY = envNum("SWARM_MAX_CPU_BUSY", 0.85);
2090
+ SWARM_CPU_WINDOW_MS = Math.max(1e3, envNum("SWARM_CPU_WINDOW_MS", 3e3));
2091
+ SWARM_CPU_SANITY_BUSY = envNum("SWARM_CPU_SANITY_BUSY", 0.9);
2092
+ SWARM_CPU_SANITY_LOAD_FACTOR = envNum("SWARM_CPU_SANITY_LOAD_FACTOR", 0.5);
2093
+ SWARM_CPU_SANITY_LAG_MS = envNum("SWARM_CPU_SANITY_LAG_MS", 6e4);
2006
2094
  SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
2007
2095
  SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
2008
2096
  SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
@@ -2173,22 +2261,59 @@ function cpuBusyFromTicks(prev, cur) {
2173
2261
  if (dTotal <= 0) return null;
2174
2262
  return Math.min(1, Math.max(0, 1 - dIdle / dTotal));
2175
2263
  }
2176
- function cpuBusyRatio() {
2177
- const cur = readCpuTicks(os.cpus());
2178
- const busy = cpuBusyFromTicks(prevCpuTicks, cur);
2179
- prevCpuTicks = cur;
2180
- return busy;
2264
+ function cpuBusyFromSamples(prev, cur, minWindowMs = SWARM_CPU_WINDOW_MS) {
2265
+ if (!prev) return null;
2266
+ if (cur.at - prev.at < minWindowMs) return null;
2267
+ return cpuBusyFromTicks(prev, cur);
2268
+ }
2269
+ function cpuBusySane(busy, load1, cpus, highForMs, sanityBusy = SWARM_CPU_SANITY_BUSY, sanityLoadFactor = SWARM_CPU_SANITY_LOAD_FACTOR, lagMs = SWARM_CPU_SANITY_LAG_MS) {
2270
+ if (busy < sanityBusy) return true;
2271
+ if (highForMs < lagMs) return true;
2272
+ const cores = cpus > 0 ? cpus : 1;
2273
+ if (!Number.isFinite(load1) || load1 < 0) return true;
2274
+ return load1 >= cores * sanityLoadFactor;
2275
+ }
2276
+ function sampleCpuBusy(now = Date.now(), ticks = readCpuTicks(os.cpus())) {
2277
+ const cur = { at: now, ...ticks };
2278
+ if (!cpuWindowBase) {
2279
+ cpuWindowBase = cur;
2280
+ return cpuBusyValue;
2281
+ }
2282
+ if (cur.at - cpuWindowBase.at < SWARM_CPU_WINDOW_MS) return cpuBusyValue;
2283
+ cpuBusyValue = cpuBusyFromSamples(cpuWindowBase, cur);
2284
+ cpuWindowBase = cur;
2285
+ return cpuBusyValue;
2286
+ }
2287
+ function cpuBusyReading() {
2288
+ return cpuBusyValue;
2289
+ }
2290
+ function trustedCpuBusy(busy, load1, cpus, now) {
2291
+ if (busy == null || busy < SWARM_CPU_SANITY_BUSY) {
2292
+ cpuHighSince = null;
2293
+ return busy;
2294
+ }
2295
+ cpuHighSince ??= now;
2296
+ if (cpuBusySane(busy, load1, cpus, now - cpuHighSince)) return busy;
2297
+ if (now - cpuSanityWarnAt >= CPU_SANITY_WARN_EVERY_MS) {
2298
+ cpuSanityWarnAt = now;
2299
+ console.warn(
2300
+ `[overmind] \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u0438\u044E \xABCPU \u0437\u0430\u043D\u044F\u0442 ${Math.round(busy * 100)}%\xBB \u043D\u0435 \u0432\u0435\u0440\u044E: \u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044F ${Math.round((now - cpuHighSince) / 1e3)}\u0441, \u0430 load1 \u0432\u0441\u0451 \u0435\u0449\u0451 ${load1.toFixed(2)} \u043F\u0440\u0438 ${cpus} \u044F\u0434\u0440\u0430\u0445 \u2014 \u0442\u0430\u043A \u043D\u0435 \u0431\u044B\u0432\u0430\u0435\u0442. \u0413\u0435\u0439\u0442 \u0441\u043F\u0430\u0432\u043D\u0430 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u043E load average, \u043F\u043E\u043A\u0430 \u0437\u0430\u043C\u0435\u0440 \u043D\u0435 \u0441\u043E\u0439\u0434\u0451\u0442\u0441\u044F.`
2301
+ );
2302
+ }
2303
+ return null;
2181
2304
  }
2182
2305
  function hostReading() {
2183
2306
  const now = Date.now();
2184
2307
  if (readingCache && now - readingCache.at < RESOURCES_CACHE_MS) return readingCache.reading;
2308
+ const load1 = os.loadavg()[0] ?? 0;
2309
+ const cpus = os.cpus().length;
2310
+ sampleCpuBusy(now);
2185
2311
  const reading = {
2186
- load1: os.loadavg()[0] ?? 0,
2187
- cpus: os.cpus().length,
2312
+ load1,
2313
+ cpus,
2188
2314
  freeGb: availableMemBytes() / 1e9,
2189
2315
  fdLoadRatio: fileDescriptorLoadRatio(),
2190
- cpuBusy: cpuBusyRatio(),
2191
- // TASWM-234: раз в тик (кэш выше) — иначе окно дельты схлопнется
2316
+ cpuBusy: trustedCpuBusy(cpuBusyReading(), load1, cpus, now),
2192
2317
  totalGb: os.totalmem() / 1e9,
2193
2318
  disk: diskGb()
2194
2319
  };
@@ -2247,6 +2372,22 @@ function pressureReport(r, minFreeGb = SWARM_RELIEF_MIN_FREE_GB, maxCpuBusy = SW
2247
2372
  function hostPressure() {
2248
2373
  return pressureReport(hostReading());
2249
2374
  }
2375
+ function resourcesForProd(r, measuredAt) {
2376
+ const gb = (v) => Math.round(v * 10) / 10;
2377
+ return {
2378
+ // null — busy% ещё не измерена (первый тик демона: она дельта тиков CPU). Ноль вместо
2379
+ // неё сказал бы «процессор простаивает» ровно тогда, когда мы про него не знаем.
2380
+ cpuBusyPct: r.cpuBusy == null ? null : Math.round(r.cpuBusy * 100),
2381
+ memFreeGb: gb(r.freeGb),
2382
+ memTotalGb: gb(r.totalGb),
2383
+ diskFreeGb: r.disk ? gb(r.disk.freeGb) : null,
2384
+ diskTotalGb: r.disk ? gb(r.disk.totalGb) : null,
2385
+ measuredAt
2386
+ };
2387
+ }
2388
+ function hostResourcesForProd() {
2389
+ return resourcesForProd(hostReading(), (/* @__PURE__ */ new Date()).toISOString());
2390
+ }
2250
2391
  function hostResourceSnapshot() {
2251
2392
  const r = hostReading();
2252
2393
  const reason = gateReason(r.load1, r.cpus, r.freeGb, r.fdLoadRatio, void 0, void 0, void 0, r.cpuBusy);
@@ -2261,13 +2402,17 @@ function hostResourceSnapshot() {
2261
2402
  thresholds: { maxLoadFactor: SWARM_MAX_LOAD_FACTOR, minFreeGb: SWARM_MIN_FREE_GB, maxFdLoadFactor: SWARM_MAX_FD_LOAD_FACTOR, maxCpuBusy: SWARM_MAX_CPU_BUSY }
2262
2403
  };
2263
2404
  }
2264
- var prevCpuTicks, RESOURCES_CACHE_MS, readingCache;
2405
+ var cpuWindowBase, cpuBusyValue, cpuHighSince, cpuSanityWarnAt, CPU_SANITY_WARN_EVERY_MS, RESOURCES_CACHE_MS, readingCache;
2265
2406
  var init_resources = __esm({
2266
2407
  "src/resources.ts"() {
2267
2408
  "use strict";
2268
2409
  init_quietExec();
2269
2410
  init_config();
2270
- prevCpuTicks = null;
2411
+ cpuWindowBase = null;
2412
+ cpuBusyValue = null;
2413
+ cpuHighSince = null;
2414
+ cpuSanityWarnAt = 0;
2415
+ CPU_SANITY_WARN_EVERY_MS = 5 * 6e4;
2271
2416
  RESOURCES_CACHE_MS = 250;
2272
2417
  readingCache = null;
2273
2418
  }
@@ -4105,6 +4250,16 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
4105
4250
  const text = res?.content?.find((c) => c.type === "text")?.text;
4106
4251
  return typeof text === "string" && text.trim() ? text : null;
4107
4252
  }
4253
+ async function reportAcceptanceCheck(input) {
4254
+ const res = await safeCall("report_acceptance_check", {
4255
+ id: input.id,
4256
+ state: input.state,
4257
+ ...input.evidence ? { evidence: input.evidence } : {}
4258
+ });
4259
+ if (!res) return null;
4260
+ const text = res.content?.find((c) => c.type === "text")?.text;
4261
+ return typeof text === "string" && text.trim() ? text : null;
4262
+ }
4108
4263
  async function resolveId(idOrSlug) {
4109
4264
  const cached = idCache.get(idOrSlug);
4110
4265
  if (cached) return cached;
@@ -4173,6 +4328,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
4173
4328
  backlogStatusId,
4174
4329
  getDocument,
4175
4330
  getTaskContext,
4331
+ reportAcceptanceCheck,
4176
4332
  createTask: async (input) => {
4177
4333
  if (!input.projectId) {
4178
4334
  throw new Error("createTask: projectId \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u2014 \u0430\u0432\u0442\u043E-\u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0440\u043E\u0435\u043C \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u043E (TASWM-204)");
@@ -4421,7 +4577,7 @@ var init_taskless = __esm({
4421
4577
  init_agentToken();
4422
4578
  init_workspace();
4423
4579
  SLUG_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
4424
- WS_FREE_TOOLS = /* @__PURE__ */ new Set(["list_workspaces", "who_am_i", "finalize_upload"]);
4580
+ WS_FREE_TOOLS = /* @__PURE__ */ new Set(["list_workspaces", "who_am_i", "finalize_upload", "report_acceptance_check"]);
4425
4581
  TaskBodyUnavailableError = class extends Error {
4426
4582
  constructor(slug, workspaceId) {
4427
4583
  super(
@@ -4492,6 +4648,10 @@ function rowToRun(row) {
4492
4648
  area: row.area ?? null,
4493
4649
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
4494
4650
  model: row.model ?? null,
4651
+ // TASWM-395: уровень усилия — через toEffortLevel, а не сырой строкой: в зеркале может
4652
+ // лежать значение, записанное демоном другой версии (словарь уровней задаёт CLI), и
4653
+ // непонятное здесь означает «уровень не задан», а не сломанный тип.
4654
+ effort: toEffortLevel(row.effort),
4495
4655
  cost_usd: row.cost_usd ?? 0,
4496
4656
  // Токены/время работы (TASWM-353) — старые прогоны без колонок → 0.
4497
4657
  tokens_in: row.tokens_in ?? 0,
@@ -4561,6 +4721,7 @@ CREATE TABLE IF NOT EXISTS runs (
4561
4721
  area TEXT,
4562
4722
  depends_on TEXT,
4563
4723
  model TEXT,
4724
+ effort TEXT,
4564
4725
  cost_usd REAL NOT NULL DEFAULT 0,
4565
4726
  tokens_in INTEGER NOT NULL DEFAULT 0,
4566
4727
  tokens_out INTEGER NOT NULL DEFAULT 0,
@@ -4662,7 +4823,10 @@ CREATE TABLE IF NOT EXISTS imports (
4662
4823
  // Чем обошёлся прогон, кроме денег (TASWM-353): токены вход/выход и время работы.
4663
4824
  "ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
4664
4825
  "ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
4665
- "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0"
4826
+ "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0",
4827
+ // TASWM-395: усилие рассуждения прогона (`claude --effort`). Nullable без DEFAULT —
4828
+ // NULL это и все прогоны до фичи, и прогоны на дефолте харнесса.
4829
+ "ALTER TABLE runs ADD COLUMN effort TEXT"
4666
4830
  ]) {
4667
4831
  try {
4668
4832
  this.db.exec(alter);
@@ -4682,7 +4846,7 @@ CREATE TABLE IF NOT EXISTS imports (
4682
4846
  createRun(input) {
4683
4847
  const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
4684
4848
  const res = this.db.prepare(
4685
- "INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, decision, resolves_run_id, reviews_run_id, review_cycle, priority, base_branch, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
4849
+ "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
4686
4850
  ).run(
4687
4851
  input.task_slug,
4688
4852
  name,
@@ -4695,6 +4859,8 @@ CREATE TABLE IF NOT EXISTS imports (
4695
4859
  input.area ?? null,
4696
4860
  input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
4697
4861
  input.model ?? null,
4862
+ input.effort ?? null,
4863
+ // усилие рассуждения (TASWM-395); null → разрешит спавн
4698
4864
  input.decision ? JSON.stringify(input.decision) : null,
4699
4865
  input.resolves_run_id ?? null,
4700
4866
  input.reviews_run_id ?? null,
@@ -5619,6 +5785,8 @@ var SwarmClient = class _SwarmClient {
5619
5785
  // предикат «репо есть локально» для решения, брать ли свободный прогон.
5620
5786
  host;
5621
5787
  repoExists;
5788
+ // TASWM-398: ЧЕМ эта машина исполняет прогоны — id адаптера (claude-code|codex).
5789
+ executorId;
5622
5790
  // Зеркало.
5623
5791
  runs = /* @__PURE__ */ new Map();
5624
5792
  runtime = /* @__PURE__ */ new Map();
@@ -5696,6 +5864,7 @@ var SwarmClient = class _SwarmClient {
5696
5864
  this.host = opts.host ?? null;
5697
5865
  this.repoExists = opts.repoExists ?? existsSync5;
5698
5866
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
5867
+ this.executorId = opts.executorId ?? EXECUTOR.id;
5699
5868
  this.outboxPath = opts.outboxPath;
5700
5869
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
5701
5870
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
@@ -5809,8 +5978,10 @@ var SwarmClient = class _SwarmClient {
5809
5978
  runFields(patch) {
5810
5979
  const out = {};
5811
5980
  if ("status" in patch && patch.status !== void 0) out.status = patch.status;
5981
+ if (patch.status === "running") out.executor = this.executorId;
5812
5982
  if ("area" in patch) out.area = patch.area ?? null;
5813
5983
  if ("model" in patch) out.model = patch.model ?? null;
5984
+ if ("effort" in patch) out.effort = patch.effort ?? null;
5814
5985
  if ("name" in patch) out.droneName = patch.name ?? null;
5815
5986
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
5816
5987
  if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
@@ -5881,6 +6052,9 @@ var SwarmClient = class _SwarmClient {
5881
6052
  area: input.area ?? null,
5882
6053
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
5883
6054
  model: input.model ?? null,
6055
+ // Усилие рассуждения (TASWM-395): пин, если его поставили на спавне; иначе null —
6056
+ // разрешённый уровень впечатает launcher, когда действительно поднимет дрона.
6057
+ effort: input.effort ?? null,
5884
6058
  cost_usd: 0,
5885
6059
  tokens_in: 0,
5886
6060
  // копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
@@ -5933,6 +6107,8 @@ var SwarmClient = class _SwarmClient {
5933
6107
  status: "queued",
5934
6108
  area: d.area,
5935
6109
  model: d.model,
6110
+ effort: d.effort,
6111
+ // усилие рассуждения (TASWM-395), если пинили на спавне
5936
6112
  name: d.name,
5937
6113
  repo: d.repo,
5938
6114
  depends_on: d.depends_on,
@@ -6686,6 +6862,19 @@ var SwarmClient = class _SwarmClient {
6686
6862
  * → null: ридер (makeMaxDronesReader) берёт env-фолбэк.
6687
6863
  */
6688
6864
  async ownerMaxDrones() {
6865
+ return (await this.ownerSwarmSettings())?.maxDrones ?? null;
6866
+ }
6867
+ /**
6868
+ * TASWM-364: настройки роя ЦЕЛИКОМ из того же who_am_i — живой кап дронов И модели, на
6869
+ * которых спавнятся дроны и ревьюеры (раздел «Рой» в настройках). Одним вызовом, а не
6870
+ * двумя: читатель у них один (ридер maxDrones раз в TTL), и второй сетевой поход за
6871
+ * соседним полем того же ответа был бы платой ни за что.
6872
+ *
6873
+ * Прод недоступен / ответ без валидного капа → null: вызывающий берёт env-фолбэки
6874
+ * (SWARM_MAX_IN_FLIGHT для капа, SWARM_DRONE_MODEL для модели). Модель без капа отдельно
6875
+ * не отдаём СОЗНАТЕЛЬНО — «ответ прода не разобрался» это одно событие, а не два.
6876
+ */
6877
+ async ownerSwarmSettings() {
6689
6878
  let res;
6690
6879
  try {
6691
6880
  res = await this.call("who_am_i", {});
@@ -6695,7 +6884,27 @@ var SwarmClient = class _SwarmClient {
6695
6884
  if (res?.isError) return null;
6696
6885
  const obj = jsonFromTextContent(res);
6697
6886
  const n = obj?.maxDrones;
6698
- return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
6887
+ if (!(typeof n === "number" && Number.isFinite(n) && n > 0)) return null;
6888
+ const str2 = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
6889
+ const cost = obj?.swarmSettings?.maxCostUsd;
6890
+ return {
6891
+ maxDrones: Math.trunc(n),
6892
+ // Старый прод (до TASWM-364) поля swarmSettings не отдаёт вовсе — обе модели null,
6893
+ // то есть прежнее поведение: env машины и дефолт харнесса.
6894
+ models: { drone: str2(obj?.swarmSettings?.droneModel), reviewer: str2(obj?.swarmSettings?.reviewerModel) },
6895
+ // TASWM-395: уровни усилия — из того же ответа и с той же семантикой, что модели:
6896
+ // старый прод поля не отдаёт → оба null, то есть env машины и дефолт харнесса.
6897
+ // toEffortLevel заодно отсекает мусор, записанный в uiPrefs мимо схемы.
6898
+ efforts: {
6899
+ drone: toEffortLevel(obj?.swarmSettings?.droneEffort),
6900
+ reviewer: toEffortLevel(obj?.swarmSettings?.reviewerEffort)
6901
+ },
6902
+ // TASWM-377: потолок трат — из того же ответа, тем же одним чтением. НОЛЬ здесь
6903
+ // значим («гарантия выключена осознанно»), поэтому пропускаем и его, а не только
6904
+ // положительные: null остаётся ровно за «ручка не задана / старый прод», и только
6905
+ // он уводит спавн на env-фолбэк и дефолт ядра.
6906
+ maxCostUsd: typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : null
6907
+ };
6699
6908
  }
6700
6909
  /**
6701
6910
  * Live verifyMode проекта из Taskless (TASWM-171): читает swarm_repo_config по
@@ -6833,19 +7042,25 @@ var SwarmClient = class _SwarmClient {
6833
7042
  if (res?.isError) return [];
6834
7043
  const raw = (res.structuredContent ?? {}).hosts;
6835
7044
  if (!Array.isArray(raw)) return [];
6836
- return raw.filter((h) => typeof h === "object" && h !== null).filter((h) => typeof h.hostId === "string" && h.hostId).map((h) => ({
6837
- hostId: h.hostId,
6838
- hostName: typeof h.hostName === "string" && h.hostName ? h.hostName : h.hostId,
6839
- lastSeenAt: typeof h.lastSeenAt === "string" ? h.lastSeenAt : null,
6840
- // TASWM-298: репозитории машины — «на связи» ещё не значит «возьмёт эту задачу».
6841
- // Прод старой версии поля не пришлёт → пустой список, строка просто без хвоста.
6842
- reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : [],
6843
- // TASWM-326: машина на паузе — «на связи, но работу не берёт». Прод старой
6844
- // версии поля не пришлёт → undefined, строка выглядит как раньше.
6845
- ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
6846
- ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
6847
- ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {}
6848
- }));
7045
+ return raw.filter((h) => typeof h === "object" && h !== null).filter((h) => typeof h.hostId === "string" && h.hostId).map((h) => {
7046
+ const resources = parseHostResources(h.resources);
7047
+ return {
7048
+ hostId: h.hostId,
7049
+ hostName: typeof h.hostName === "string" && h.hostName ? h.hostName : h.hostId,
7050
+ lastSeenAt: typeof h.lastSeenAt === "string" ? h.lastSeenAt : null,
7051
+ // TASWM-298: репозитории машины — «на связи» ещё не значит «возьмёт эту задачу».
7052
+ // Прод старой версии поля не пришлёт → пустой список, строка просто без хвоста.
7053
+ reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : [],
7054
+ // TASWM-326: машина на паузе — «на связи, но работу не берёт». Прод старой
7055
+ // версии поля не пришлёт → undefined, строка выглядит как раньше.
7056
+ ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
7057
+ ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
7058
+ ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {},
7059
+ // Показаний нет (старый прод, машина со старым демоном) — ключа не заводим вовсе,
7060
+ // ровно как с паузой выше: «не знаем» и так значит «не знаем».
7061
+ ...resources ? { resources } : {}
7062
+ };
7063
+ });
6849
7064
  }
6850
7065
  /**
6851
7066
  * TASWM-326: убрать ЭТУ машину из парка (`swarm remove`). Прод удаляет строку
@@ -6911,13 +7126,22 @@ var SwarmClient = class _SwarmClient {
6911
7126
  * разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
6912
7127
  * трогает прошлое значение — та же логика, что у repoFullNames.
6913
7128
  *
7129
+ * TASWM-394: тем же пульсом уезжают РЕСУРСЫ машины (`resources` — процессор, память,
7130
+ * диск и время замера). Это не новое измерение, а ровно то, что демон УЖЕ посчитал для
7131
+ * своего гейта спавна: числа в интерфейсе обязаны совпадать с теми, по которым он
7132
+ * отказывается поднимать дрона. Без них «машина на связи, свободна» и «машина физически
7133
+ * не может поднять дрона» выглядят в проде одинаково — 12.08 это стоило полудня
7134
+ * простоя, видимого только в логе на самой машине. Не передали (нечем измерить) → ключ
7135
+ * не шлём, прод прошлое не трогает — та же логика, что у capacity/repoFullNames.
7136
+ *
6914
7137
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
6915
7138
  */
6916
- async heartbeat(daemonVersion, pause, capacity) {
7139
+ async heartbeat(daemonVersion, pause, capacity, resources) {
6917
7140
  try {
6918
7141
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
6919
7142
  if (daemonVersion) args.daemonVersion = daemonVersion;
6920
7143
  if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
7144
+ if (resources) args.resources = resources;
6921
7145
  if (pause !== void 0) {
6922
7146
  args.paused = !!pause?.paused;
6923
7147
  if (pause?.reason) args.pauseReason = pause.reason;
@@ -7124,6 +7348,8 @@ var SwarmClient = class _SwarmClient {
7124
7348
  area: null,
7125
7349
  depends_on: [],
7126
7350
  model: null,
7351
+ effort: null,
7352
+ // усилие рассуждения (TASWM-395) — прод-поле, заполняется ниже из ответа
7127
7353
  cost_usd: 0,
7128
7354
  tokens_in: 0,
7129
7355
  // токены и время работы (TASWM-353) — прод-поля, заполняются ниже
@@ -7192,6 +7418,7 @@ var SwarmClient = class _SwarmClient {
7192
7418
  local.area = pr.area ?? null;
7193
7419
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
7194
7420
  local.model = pr.model ?? null;
7421
+ local.effort = toEffortLevel(pr.effort);
7195
7422
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
7196
7423
  local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
7197
7424
  local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
@@ -8186,8 +8413,26 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
8186
8413
  };
8187
8414
  }
8188
8415
 
8416
+ // src/swarmModels.ts
8417
+ init_src();
8418
+ function pick(v) {
8419
+ const s = typeof v === "string" ? v.trim() : "";
8420
+ return s.length > 0 ? s : null;
8421
+ }
8422
+ function forKind(kind, swarm, norm) {
8423
+ if (!swarm) return null;
8424
+ return kind === "reviewer" ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
8425
+ }
8426
+ function resolveRunModel(run, swarm, env = process.env) {
8427
+ return pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL) ?? null;
8428
+ }
8429
+ function resolveRunEffort(run, swarm, env = process.env) {
8430
+ return toEffortLevel(run.effort) ?? forKind(run.kind, swarm, toEffortLevel) ?? toEffortLevel(env.SWARM_DRONE_EFFORT) ?? null;
8431
+ }
8432
+
8189
8433
  // src/launcher.ts
8190
8434
  init_executors();
8435
+ init_src();
8191
8436
  var requireFromHere = createRequire(import.meta.url);
8192
8437
  function resolveTsxCli() {
8193
8438
  try {
@@ -8199,6 +8444,15 @@ function resolveTsxCli() {
8199
8444
  }
8200
8445
  }
8201
8446
  var MCP_SERVER_NAME = "overmind";
8447
+ function launchBudgetUsd(limit, spentUsd, env = process.env) {
8448
+ return remainingRunBudgetUsd(limit ?? envCapUsd(env), spentUsd);
8449
+ }
8450
+ function envCapUsd(env) {
8451
+ const raw = env.SWARM_MAX_COST_USD;
8452
+ if (raw === void 0 || raw === "") return DEFAULT_RUN_LIMITS.maxCostUsd;
8453
+ const n = Number(raw);
8454
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_RUN_LIMITS.maxCostUsd;
8455
+ }
8202
8456
  function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
8203
8457
  const env = { ...base3 };
8204
8458
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
@@ -8323,7 +8577,13 @@ function makeLauncher(cfg) {
8323
8577
  ...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
8324
8578
  }
8325
8579
  };
8326
- const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8580
+ const model = resolveRunModel(run, cfg.swarmModels?.() ?? null);
8581
+ const effort = resolveRunEffort(run, cfg.swarmEfforts?.() ?? null);
8582
+ try {
8583
+ cfg.recordEffort?.(run.id, effort);
8584
+ } catch (e) {
8585
+ console.error(`[launcher] \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u0443\u0441\u0438\u043B\u0438\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${String(e)}`);
8586
+ }
8327
8587
  const realHome = cfg.droneIsolation?.realHome ?? homedir4();
8328
8588
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
8329
8589
  if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join12(cfg.droneIsolation.swarmHome, "token"))) {
@@ -8342,7 +8602,14 @@ function makeLauncher(cfg) {
8342
8602
  // Закрытые дрону пути имеют смысл только при теневом HOME: без него дрон и так
8343
8603
  // сидит в HOME демона, и запирать половину дверей — самообман.
8344
8604
  denyPaths: cfg.droneIsolation ? dronePrivatePaths(realHome, cfg.droneIsolation.swarmHome) : [],
8345
- realHome
8605
+ realHome,
8606
+ // TASWM-377: остаток бюджета ЭТОГО прогона — харнессу, чтобы он резал трату у себя, в
8607
+ // момент её совершения. Прод-сторож остаётся страховкой, но он по устройству видит
8608
+ // стоимость только по записанному итогу: живой прогон стоит для него ноль, сколько бы
8609
+ // ни жёг, — отсюда и $74 при лимите $20.
8610
+ maxBudgetUsd: launchBudgetUsd(cfg.maxCostUsd?.() ?? null, run.cost_usd),
8611
+ // TASWM-395: усилие рассуждения — харнессу флагом (claude `--effort`). null = флага нет.
8612
+ effort
8346
8613
  });
8347
8614
  let stdio = "ignore";
8348
8615
  let outFd;
@@ -8391,7 +8658,7 @@ function accountRunCost(deps, run) {
8391
8658
  const dir = join13(deps.runLogsDir, String(run.id));
8392
8659
  const files = unprocessedLogFiles(dir);
8393
8660
  if (files.length === 0) return;
8394
- const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8661
+ const model = resolveRunModel(run, deps.swarmModels?.() ?? null);
8395
8662
  const executor = deps.executor ?? resolveExecutor();
8396
8663
  let added = 0;
8397
8664
  let tokensIn = 0;
@@ -8441,6 +8708,9 @@ function makeCostAccountant(deps) {
8441
8708
  init_executors();
8442
8709
  import { readdirSync as readdirSync9, readFileSync as readFileSync11 } from "fs";
8443
8710
  import { join as join14 } from "path";
8711
+ function endedItself(outcome) {
8712
+ return outcome === "graceful" || outcome === "budget";
8713
+ }
8444
8714
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
8445
8715
  function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
8446
8716
  const dir = join14(runLogsDir, String(runId));
@@ -8464,7 +8734,9 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
8464
8734
  } catch {
8465
8735
  return "unknown";
8466
8736
  }
8467
- return executor.parseResult(raw, null) ? "graceful" : "crash";
8737
+ const result = executor.parseResult(raw, null);
8738
+ if (!result) return "crash";
8739
+ return result.stoppedByBudget ? "budget" : "graceful";
8468
8740
  }
8469
8741
  function makeExitClassifier(runLogsDir, executor) {
8470
8742
  const exec = executor ?? resolveExecutor();
@@ -9166,6 +9438,9 @@ async function mergedPrForBranch(branch, repoDir, run = realRepoRunner) {
9166
9438
  async function closePR(prUrl, run = realRunner) {
9167
9439
  await run(["pr", "close", prUrl, "--comment", "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 (swarm)"]);
9168
9440
  }
9441
+ async function postPrComment(prUrl, body, run = realRunner) {
9442
+ await run(["pr", "comment", prUrl, "--body", body]);
9443
+ }
9169
9444
  var realCommentsRunner = async (prUrl) => (await $3`gh pr view ${prUrl} --json comments`).stdout;
9170
9445
  var VERDICT_RE = /^[^A-Za-z]*(APPROVED?|LGTM|(?:REQUEST(?:ED)?\s+)?CHANGES(?:\s+REQUESTED)?)\b/i;
9171
9446
  var APPROVE_RE = /^(APPROVE|LGTM)/i;
@@ -9495,6 +9770,41 @@ ${notFoundText}` : notFoundText }],
9495
9770
  return { content: [{ type: "text", text: lines.join("\n") }] };
9496
9771
  }
9497
9772
  );
9773
+ server.tool(
9774
+ "report_acceptance_check",
9775
+ "\u0417\u0430\u043A\u0440\u044B\u0442\u044C \u043E\u0434\u0438\u043D \u043F\u0443\u043D\u043A\u0442 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0435\u0432 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 \u0442\u0432\u043E\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438: passed \u2014 \u043F\u0440\u043E\u0448\u043B\u0430, failed \u2014 \u0443\u043F\u0430\u043B\u0430, pending \u2014 \u0441\u043D\u043E\u0432\u0430 \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u043B\u0430\u0441\u044C. id \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0431\u0435\u0440\u0438 \u0438\u0437 \u0431\u043B\u043E\u043A\u0430 \xAB\u041A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438\xBB (\u0441\u0442\u0440\u043E\u043A\u0430 \xAB(id: \u2026)\xBB) \u0432 \u043F\u0440\u043E\u043C\u043F\u0442\u0435 \u0438\u043B\u0438 \u0432 read_task. evidence \u2014 \u0414\u041E\u041A\u0410\u0417\u0410\u0422\u0415\u041B\u042C\u0421\u0422\u0412\u041E: \u0441\u0442\u0440\u043E\u043A\u0430 \u0432\u044B\u0432\u043E\u0434\u0430 \u043A\u043E\u043C\u0430\u043D\u0434\u044B, \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 PR/\u0441\u043A\u0440\u0438\u043D, \u0447\u0442\u043E \u0438\u043C\u0435\u043D\u043D\u043E \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u043B. \u0421\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u0441\u0442\u0430\u0432\u044C \u043F\u043E \u0424\u0410\u041A\u0422\u0423 \u043F\u0440\u043E\u0433\u043E\u043D\u0430, \u0430 \u043D\u0435 \u043F\u043E \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044E: \xAB\u0443\u043F\u0430\u043B\u0430\xBB \u0438 \xAB\u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u043B\u0430\u0441\u044C\xBB \u2014 \u0440\u0430\u0437\u043D\u043E\u0435. \u041F\u0440\u043E\u0439\u0434\u0438\u0441\u044C \u043F\u043E \u0432\u0441\u0435\u043C \u043F\u0443\u043D\u043A\u0442\u0430\u043C \u041F\u0415\u0420\u0415\u0414 done(): \u043D\u0435\u043F\u0440\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043D\u044B\u0435 \u0433\u0430\u043B\u043E\u0447\u043A\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442 \u0440\u0443\u043A\u0430\u043C\u0438 \u043F\u043E \u0442\u0432\u043E\u0435\u043C\u0443 \u043E\u0442\u0447\u0451\u0442\u0443 \u2014 \u0440\u043E\u0432\u043D\u043E \u0442\u0430 \u0440\u0430\u0431\u043E\u0442\u0430, \u043A\u043E\u0442\u043E\u0440\u0443\u044E \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0441\u043D\u0438\u043C\u0430\u044E\u0442. \u0421\u0442\u0430\u0432\u0438\u0442\u044C \u0441\u0430\u043C\u0438 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u043D\u0435\u0447\u0435\u043C: \u0438\u0445 \u0437\u0430\u0434\u0430\u0451\u0442 \u043F\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0449\u0438\u043A.",
9776
+ {
9777
+ id: z.string().describe("id \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0438\u0437 \u0431\u043B\u043E\u043A\u0430 \xAB\u041A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438\xBB"),
9778
+ state: z.enum(["passed", "failed", "pending"]).describe("passed | failed | pending \u2014 \u043F\u043E \u0444\u0430\u043A\u0442\u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430"),
9779
+ evidence: z.string().max(4e3).optional().describe("\u0447\u0435\u043C \u0434\u043E\u043A\u0430\u0437\u0430\u043D\u043E: \u0432\u044B\u0432\u043E\u0434 \u043A\u043E\u043C\u0430\u043D\u0434\u044B, \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 PR/\u0441\u043A\u0440\u0438\u043D, \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0435\u043C\u043E\u0435 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435")
9780
+ },
9781
+ async ({ id, state, evidence }) => {
9782
+ if (!opts.taskless) {
9783
+ return {
9784
+ content: [
9785
+ {
9786
+ type: "text",
9787
+ text: "Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u0433\u0430\u043B\u043E\u0447\u043A\u0443 \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u043C. \u041E\u0442\u0447\u0438\u0442\u0430\u0439\u0441\u044F \u043F\u043E \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u044E \u0442\u0435\u043A\u0441\u0442\u043E\u043C \u0432 summary \u043F\u0440\u0438 done()."
9788
+ }
9789
+ ],
9790
+ isError: true
9791
+ };
9792
+ }
9793
+ const text = await opts.taskless.reportAcceptanceCheck({ id, state, evidence });
9794
+ if (!text) {
9795
+ return {
9796
+ content: [
9797
+ {
9798
+ type: "text",
9799
+ text: `\u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 ${id} \u043D\u0435 \u043E\u0442\u043C\u0435\u0447\u0435\u043D\u0430 (\u0441\u043C. \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430): id \u043C\u043E\u0433 \u0431\u044B\u0442\u044C \u0447\u0443\u0436\u0438\u043C \u0438\u043B\u0438 \u0443\u0441\u0442\u0430\u0440\u0435\u0442\u044C \u2014 \u043F\u0435\u0440\u0435\u0447\u0438\u0442\u0430\u0439 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 read_task. \u041D\u0435 \u043C\u043E\u043B\u0447\u0438 \u043E\u0431 \u044D\u0442\u043E\u043C \u0432 summary \u043F\u0440\u0438 done().`
9800
+ }
9801
+ ],
9802
+ isError: true
9803
+ };
9804
+ }
9805
+ return { content: [{ type: "text", text }] };
9806
+ }
9807
+ );
9498
9808
  server.tool(
9499
9809
  "ask",
9500
9810
  "\u0417\u0430\u0434\u0430\u0442\u044C \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u0438 \u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F \u043E\u0442\u0432\u0435\u0442\u0430. \u0415\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F \u2014 \u0434\u0430\u0439 options (2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430, \u0441\u0442\u0430\u043D\u0443\u0442 \u043A\u043D\u043E\u043F\u043A\u0430\u043C\u0438); \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0432\u044B\u0431\u0435\u0440\u0435\u0442 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 \u0438\u043B\u0438 \u043D\u0430\u043F\u0438\u0448\u0435\u0442 \u0441\u0432\u043E\u0451. \u041E\u043F\u0443\u0441\u0442\u0438 options \u0434\u043B\u044F \u043E\u0442\u043A\u0440\u044B\u0442\u043E\u0433\u043E \u0432\u043E\u043F\u0440\u043E\u0441\u0430. \u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043E\u0442\u0432\u0435\u0442 \u0441\u0442\u0440\u043E\u043A\u043E\u0439. \u0417\u043E\u0432\u0438, \u043A\u043E\u0433\u0434\u0430 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u2014 \u043D\u0435 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439.",
@@ -11037,7 +11347,7 @@ async function changedPaths(worktree, opts = {}) {
11037
11347
  // package.json
11038
11348
  var package_default = {
11039
11349
  name: "@taskless-app/swarm",
11040
- version: "0.6.4",
11350
+ version: "0.6.6",
11041
11351
  type: "module",
11042
11352
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
11043
11353
  keywords: [
@@ -11087,7 +11397,7 @@ var package_default = {
11087
11397
  execa: "^10.0.0",
11088
11398
  zod: "3.25.76"
11089
11399
  },
11090
- gitHead: "12ecebcf1e60baab07e68cae3e78a29d78cb7fde"
11400
+ gitHead: "89e3fe0078755d130512c75ac85f195e6b950392"
11091
11401
  };
11092
11402
 
11093
11403
  // src/version.ts
@@ -11310,7 +11620,8 @@ function timelineState(db, now = Date.now(), overmindLog = []) {
11310
11620
  heroRunId: r.hero_run_id,
11311
11621
  reviewVerdict: r.review_verdict,
11312
11622
  reviewPhase: r.review_phase,
11313
- model: r.model
11623
+ model: r.model,
11624
+ effort: r.effort
11314
11625
  });
11315
11626
  }
11316
11627
  const liveLanes = lanes.filter((l) => l.live);
@@ -13084,9 +13395,9 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
13084
13395
  )
13085
13396
  );
13086
13397
  return {
13087
- since: pick(since, fresh),
13088
- lastFiredAt: pick(lastFiredAt, fresh),
13089
- fires: pick(fires, fresh),
13398
+ since: pick2(since, fresh),
13399
+ lastFiredAt: pick2(lastFiredAt, fresh),
13400
+ fires: pick2(fires, fresh),
13090
13401
  // TAS-485: очередь гашения НЕ фильтруем тем же TTL, что ключи. Она про
13091
13402
  // уведомление, которое УЖЕ висит у человека в инбоксе: пролежав неделю
13092
13403
  // недоставленным, гашение не становится лишним — оно становится нужнее.
@@ -13170,7 +13481,7 @@ function strings(raw) {
13170
13481
  if (!Array.isArray(raw)) return [];
13171
13482
  return raw.filter((v) => typeof v === "string" && v.length > 0);
13172
13483
  }
13173
- function pick(src, keys) {
13484
+ function pick2(src, keys) {
13174
13485
  const out = {};
13175
13486
  for (const k of keys) if (src[k] != null) out[k] = src[k];
13176
13487
  return out;
@@ -13568,13 +13879,20 @@ var CHANGES_WAKE_COOLDOWN_MS = Number(process.env.SWARM_CHANGES_WAKE_COOLDOWN_MS
13568
13879
  var CHANGES_WAKE_LIMIT = Number(process.env.SWARM_CHANGES_WAKE_LIMIT) || 3;
13569
13880
  var CHANGES_WAKE_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u044F \u043F\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0443 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB";
13570
13881
  var CHANGES_WAKE_ESCALATION_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u044F \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0430\u0441\u044C";
13882
+ var BUDGET_STOP_REASON = "\u043D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442: \u0441\u0435\u0441\u0441\u0438\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B \u0431\u044E\u0434\u0436\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430";
13883
+ var NO_AUTO_WAKE_REASONS = [EXITED_NO_DONE_REASON, BUDGET_STOP_REASON];
13884
+ var REVIEW_BUDGET_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u043E\u0441\u044C \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442";
13885
+ function reviewBudgetMark(cycle) {
13886
+ return `${REVIEW_BUDGET_MARK} (\u043A\u0440\u0443\u0433 ${cycle})`;
13887
+ }
13571
13888
  var NEEDS_HUMAN_MARKS = [
13572
13889
  RESOLVE_ESCALATION_MARK,
13573
13890
  REVIEW_ESCALATION_MARK,
13574
13891
  REVIEW_UNRECOVERABLE_MARK,
13575
13892
  REVIEW_CYCLE_ESCALATION_MARK,
13576
13893
  CI_WAKE_ESCALATION_MARK,
13577
- CHANGES_WAKE_ESCALATION_MARK
13894
+ CHANGES_WAKE_ESCALATION_MARK,
13895
+ REVIEW_BUDGET_MARK
13578
13896
  ];
13579
13897
  var SELF_REFERENCE_MARK = "PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430";
13580
13898
  var REVIEW_NO_VERDICT_REASON = "\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u043E\u0441\u044C \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430: \u043D\u0438 approve, \u043D\u0438 changes \u2014 \u0437\u0430\u0441\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E, \u043D\u0443\u0436\u0435\u043D \u043D\u043E\u0432\u044B\u0439 \u0437\u0430\u0445\u043E\u0434 (TASWM-375)";
@@ -13954,8 +14272,14 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
13954
14272
  // вторую ветку мимо PR. Пишем ту, что реально записана на прогоне; пусто (первый спавн —
13955
14273
  // ветку заводит spawnDrone из слага) — прежняя формулировка.
13956
14274
  `\u0422\u044B \u2014 \u0434\u0440\u043E\u043D \u0440\u043E\u044F. \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C \u0432 \u044D\u0442\u043E\u043C git-worktree, \u0432\u0435\u0442\u043A\u0430 \`${run.branch || run.task_slug}\`${run.branch && run.branch !== run.task_slug ? " (\u043E\u043D\u0430 \u041D\u0415 \u0440\u0430\u0432\u043D\u0430 \u0441\u043B\u0430\u0433\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u044D\u0442\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u0435 \u0443\u0436\u0435 \u043E\u0442\u043A\u0440\u044B\u0442\u043E\u0433\u043E PR, \u043F\u0443\u0448 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u043D\u0435\u0451)" : " \u0441\u043E\u0437\u0434\u0430\u043D\u0430 \u043F\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438"}.`,
13957
- "\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; read_task() \u2014 \u0441\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 (\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435+\u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438+\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u044B+\u0434\u043E\u043A\u0438+\u0438\u0441\u0445\u043E\u0434\u044B \u0441\u043E\u0441\u0435\u0434\u043D\u0438\u0445 \u0437\u0430\u0434\u0430\u0447 \u043F\u043E \u0437\u043E\u043D\u0435 \u043A\u043E\u0434\u0430) \u0431\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432, \u0435\u0441\u043B\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043C\u043E\u0433\u043B\u0438 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435 \u0441\u0442\u0430\u0440\u0442\u0430; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR; note(text) \u2014 \u0437\u0430\u043C\u0435\u0442\u043A\u0430 \xAB\u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438\xBB \u0411\u0415\u0417 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0438: \u043D\u0430\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u043D\u0430 \u043F\u0440\u043E\u0431\u0435\u043B \u0432 \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435, \u043F\u0440\u043E\u0442\u0443\u0445\u0448\u0443\u044E \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u044E, \u0431\u0430\u0433 \u0440\u044F\u0434\u043E\u043C \u0441\u043E \u0441\u0432\u043E\u0438\u043C \u0434\u0438\u0444\u0444\u043E\u043C \u2014 \u0441\u043A\u0430\u0436\u0438 \u0438 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u0434\u0430\u043B\u044C\u0448\u0435, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0443\u0432\u0438\u0434\u0438\u0442 \u0438 \u0441\u0430\u043C \u0440\u0435\u0448\u0438\u0442, \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u044C \u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043D\u0435 \u043F\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0439 \u0442\u0430\u043A\u043E\u0435 \u0432 \u0434\u043E\u0440\u043E\u0433\u043E\u0439 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 ask \u0438 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u0438 \u0432 \u043E\u0442\u0447\u0451\u0442\u0435 \u043E \u0441\u0434\u0430\u0447\u0435).",
14275
+ "\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; read_task() \u2014 \u0441\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 (\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435+\u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438+\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u044B+\u0434\u043E\u043A\u0438+\u0438\u0441\u0445\u043E\u0434\u044B \u0441\u043E\u0441\u0435\u0434\u043D\u0438\u0445 \u0437\u0430\u0434\u0430\u0447 \u043F\u043E \u0437\u043E\u043D\u0435 \u043A\u043E\u0434\u0430) \u0431\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432, \u0435\u0441\u043B\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043C\u043E\u0433\u043B\u0438 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435 \u0441\u0442\u0430\u0440\u0442\u0430; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); report_acceptance_check(id, state, evidence) \u2014 \u0437\u0430\u043A\u0440\u043E\u0439 \u041A\u0410\u0416\u0414\u042B\u0419 \u043F\u0443\u043D\u043A\u0442 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0435\u0432 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 \u0437\u0430\u0434\u0430\u0447\u0438 (id \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0438\u0437 \u0431\u043B\u043E\u043A\u0430 \xAB\u041A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438\xBB, state = passed|failed|pending \u043F\u043E \u0444\u0430\u043A\u0442\u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430, evidence \u2014 \u0432\u044B\u0432\u043E\u0434 \u043A\u043E\u043C\u0430\u043D\u0434\u044B/\u0441\u0441\u044B\u043B\u043A\u0430/\u0447\u0442\u043E \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u043B); \u043F\u0440\u043E\u0439\u0434\u0438\u0441\u044C \u043F\u043E \u043D\u0438\u043C \u041F\u0415\u0420\u0415\u0414 done(), \u0438\u043D\u0430\u0447\u0435 \u0433\u0430\u043B\u043E\u0447\u043A\u0438 \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442 \u0440\u0443\u043A\u0430\u043C\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440; done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR; note(text) \u2014 \u0437\u0430\u043C\u0435\u0442\u043A\u0430 \xAB\u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438\xBB \u0411\u0415\u0417 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0438: \u043D\u0430\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u043D\u0430 \u043F\u0440\u043E\u0431\u0435\u043B \u0432 \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435, \u043F\u0440\u043E\u0442\u0443\u0445\u0448\u0443\u044E \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u044E, \u0431\u0430\u0433 \u0440\u044F\u0434\u043E\u043C \u0441\u043E \u0441\u0432\u043E\u0438\u043C \u0434\u0438\u0444\u0444\u043E\u043C \u2014 \u0441\u043A\u0430\u0436\u0438 \u0438 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u0434\u0430\u043B\u044C\u0448\u0435, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0443\u0432\u0438\u0434\u0438\u0442 \u0438 \u0441\u0430\u043C \u0440\u0435\u0448\u0438\u0442, \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u044C \u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043D\u0435 \u043F\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0439 \u0442\u0430\u043A\u043E\u0435 \u0432 \u0434\u043E\u0440\u043E\u0433\u043E\u0439 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 ask \u0438 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u0438 \u0432 \u043E\u0442\u0447\u0451\u0442\u0435 \u043E \u0441\u0434\u0430\u0447\u0435).",
13958
14276
  "\u041F\u0440\u0430\u0432\u0438\u043B\u0430: \u0442\u0440\u043E\u0433\u0430\u0435\u0448\u044C UI \u2014 \u0421\u041D\u0410\u0427\u0410\u041B\u0410 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438, \u0435\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0441\u043A\u0438\u043B\u043B \u0441 UI-\u043F\u0440\u0430\u0432\u0438\u043B\u0430\u043C\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430, \u0438 \u0441\u043B\u0435\u0434\u0443\u0439 \u0435\u0433\u043E \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435 (\u043D\u0435\u0442 \u043D\u0443\u0436\u043D\u043E\u0433\u043E \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0430 \u2014 \u0437\u0430\u0432\u0435\u0434\u0438 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443, \u043D\u0435 \u0445\u0430\u0440\u0434\u043A\u043E\u0434\u044C \u043E\u0434\u043D\u043E\u0440\u0430\u0437\u043E\u0432\u043E\u0435); \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0441\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0438; \u043F\u043E\u0441\u043B\u0435 UI-\u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u0441\u043A\u0440\u0438\u043D\u044B mobile+desktop \u041A \u0417\u0410\u0414\u0410\u0427\u0415 (\u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 screenshots \u0443 done); \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0447\u0435\u0440\u0435\u0437 gh \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url).",
14277
+ // TASWM-377: у сессии есть ПОТОЛОК ТРАТ, и он режет её без предупреждения, прямо посреди
14278
+ // работы. Дрон об этом знать обязан: цена сессии растёт от ЧИСЛА ХОДОВ (каждый ход заново
14279
+ // оплачивает весь накопленный контекст), поэтому «просканирую репозиторий, чтобы вникнуть»
14280
+ // — это не тщательность, а способ не доехать до PR. Замер по прогонам роя: дорогие сессии
14281
+ // отличаются от дешёвых числом заходов, а не размером задачи.
14282
+ "\u{1F4B8} \u0421\u0435\u0441\u0441\u0438\u044F \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0430 \u041F\u041E\u0422\u041E\u041B\u041A\u041E\u041C \u0422\u0420\u0410\u0422: \u0443\u043F\u0440\u0451\u0448\u044C\u0441\u044F \u2014 \u0442\u0435\u0431\u044F \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u044F\u0442 \u043F\u043E\u0441\u0440\u0435\u0434\u0438 \u0440\u0430\u0431\u043E\u0442\u044B, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u043F\u0440\u043E\u043F\u0430\u0434\u0451\u0442, \u0430 \u0437\u0430\u0434\u0430\u0447\u0443 \u0431\u0443\u0434\u0435\u0442 \u0434\u043E\u0434\u0435\u043B\u044B\u0432\u0430\u0442\u044C \u0447\u0435\u043B\u043E\u0432\u0435\u043A. \u0427\u0438\u0442\u0430\u0439 \u0442\u043E, \u0447\u0442\u043E \u043D\u0443\u0436\u043D\u043E \u043F\u0440\u0430\u0432\u043A\u0435 (\u043A\u0430\u0440\u0442\u0430 \u043A\u043E\u0434\u0430\u0431\u0430\u0437\u044B + \u0437\u0430\u0442\u0440\u043E\u043D\u0443\u0442\u044B\u0435 \u0444\u0430\u0439\u043B\u044B), \u0430 \u043D\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u0446\u0435\u043B\u0438\u043A\u043E\u043C; \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u043E\u0435 \u043D\u0435 \u043F\u0435\u0440\u0435\u0447\u0438\u0442\u044B\u0432\u0430\u0439; \u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0438 \u043F\u0443\u0448\u044C \u043F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0437\u043D\u0430\u0447\u0438\u043C\u043E\u0433\u043E \u0448\u0430\u0433\u0430 \u2014 \u0442\u043E\u0433\u0434\u0430 \u0434\u0430\u0436\u0435 \u043E\u0431\u0440\u044B\u0432 \u043F\u043E \u0431\u044E\u0434\u0436\u0435\u0442\u0443 \u043E\u0441\u0442\u0430\u0432\u0438\u0442 \u0440\u0430\u0431\u043E\u0442\u0443 \u0432 origin.",
13959
14283
  // TASWM-293: живой случай SCE-5 — ревьюер заблокировал PR за отсутствие CI (у проекта
13960
14284
  // его нет и не предполагается), автора разбудили «закрывать замечания», замечаний по коду
13961
14285
  // не было, и дрон завёл в ЧУЖОМ репозитории .github/workflows/check.yml, чтобы пройти
@@ -14099,6 +14423,13 @@ function lastChangesSummary(db, reviewRunId) {
14099
14423
  }
14100
14424
  return null;
14101
14425
  }
14426
+ var REVIEW_SCOPE_BLOCK = [
14427
+ "\u{1F4B8} \u041A\u041E\u041D\u0422\u0423\u0420 \u0420\u0415\u0412\u042C\u042E (\u0436\u0451\u0441\u0442\u043A\u043E). \u0422\u0432\u043E\u044F \u0441\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 \u043E\u043D \u0441\u0440\u0435\u0436\u0435\u0442 \u0435\u0451 \u0411\u0415\u0417 \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044F: \u0443\u043F\u0440\u0451\u0448\u044C\u0441\u044F \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u043E\u0432\u0441\u0435, \u0430 PR \u0443\u0439\u0434\u0451\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443. \u041F\u043E\u044D\u0442\u043E\u043C\u0443:",
14428
+ "\u2022 \u0427\u0438\u0442\u0430\u0439 \u0414\u0418\u0424\u0424 \u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u0442\u043E, \u0447\u0435\u0433\u043E \u043E\u043D \u043A\u0430\u0441\u0430\u0435\u0442\u0441\u044F: \u0441\u0430\u043C\u0438 \u0438\u0437\u043C\u0435\u043D\u0451\u043D\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B + \u043C\u0435\u0441\u0442\u0430, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0438\u043C\u0438 \u043B\u043E\u043C\u0430\u044E\u0442\u0441\u044F. \u0420\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \xAB\u0434\u043B\u044F \u043E\u0431\u0449\u0435\u0433\u043E \u043F\u043E\u043D\u0438\u043C\u0430\u043D\u0438\u044F\xBB \u043D\u0435 \u0441\u043A\u0430\u043D\u0438\u0440\u0443\u0439 \u2014 \u043D\u0430 \u044D\u0442\u043E \u0443\u0445\u043E\u0434\u0438\u0442 \u0432\u0435\u0441\u044C \u0431\u044E\u0434\u0436\u0435\u0442 \u0438 \u043D\u0435 \u043D\u0430\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u043D\u0438\u0447\u0435\u0433\u043E.",
14429
+ "\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. \u041F\u043E\u043D\u0430\u0434\u043E\u0431\u0438\u043B\u043E\u0441\u044C \u0432\u0435\u0440\u043D\u0443\u0442\u044C\u0441\u044F \u043A \u0444\u0430\u0439\u043B\u0443 \u2014 \u0432\u0435\u0440\u043D\u0438\u0441\u044C \u043A \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u0443\u0436\u0435 \u0432\u0438\u0434\u0435\u043B \u0432 \u044D\u0442\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438, \u0430 \u043D\u0435 \u0447\u0438\u0442\u0430\u0439 \u0435\u0433\u043E \u0437\u0430\u043D\u043E\u0432\u043E; `git diff`/`gh pr diff` \u0442\u043E\u0436\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442 \u043E\u0434\u043D\u043E\u0433\u043E \u0440\u0430\u0437\u0430.",
14430
+ "\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.",
14431
+ "\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."
14432
+ ].join("\n");
14102
14433
  function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachmentsBlock, verifyMode = SWARM_VERIFY) {
14103
14434
  const name = reviewer.name || `#${reviewer.id}`;
14104
14435
  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)";
@@ -14116,6 +14447,8 @@ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachments
14116
14447
  ] : [],
14117
14448
  "",
14118
14449
  "\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.",
14450
+ "",
14451
+ REVIEW_SCOPE_BLOCK,
14119
14452
  // TASWM-283: ревьюер читает ТУ ЖЕ задачу и так же слеп к её картинкам — без макета
14120
14453
  // «сделано ли как просили» он проверить не может, а UI-пункт чек-листа держится
14121
14454
  // ровно на этом. Кладём ему тот же каталог вложений, что и исполнителю.
@@ -15109,6 +15442,67 @@ async function salvageVerdictFromPrComment(deps, rv, reviewsRunId, since, ctx) {
15109
15442
  }
15110
15443
  return true;
15111
15444
  }
15445
+ async function failReviewerRun(deps, reviewer, reviewsRunId, opts) {
15446
+ if (!opts.keepWorktree && reviewer.worktree_path) {
15447
+ await deps.removeWorktree(reviewer.repo, reviewer.worktree_path).catch(() => {
15448
+ });
15449
+ }
15450
+ deps.db.updateRun(reviewer.id, { status: "failed", stop_reason: opts.reason });
15451
+ deps.db.addEvent(reviewer.id, "failed", opts.reason);
15452
+ deps.db.addEvent(reviewsRunId, "step", opts.noteForReviewed);
15453
+ narrate(deps, opts.narration);
15454
+ }
15455
+ async function handleBudgetExhausted(deps, run, ctx) {
15456
+ const spentUsd = deps.db.getRun(run.id)?.cost_usd ?? run.cost_usd;
15457
+ const spent = `$${spentUsd.toFixed(2)}`;
15458
+ const reason = `${BUDGET_STOP_REASON} (\u043F\u043E\u0442\u0440\u0430\u0447\u0435\u043D\u043E ${spent})`;
15459
+ if (run.reviews_run_id != null) {
15460
+ const salvaged = await salvageVerdictFromPrComment(deps, run, run.reviews_run_id, run.created_at, {
15461
+ how: `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442 (${spent})`,
15462
+ now: ctx.now,
15463
+ droneCap: ctx.droneCap,
15464
+ stormActive: ctx.stormActive
15465
+ });
15466
+ if (salvaged) return;
15467
+ const reviewed = deps.db.getRun(run.reviews_run_id);
15468
+ const cycle = reviewed?.review_cycle ?? 0;
15469
+ const refusal = [
15470
+ `\u26D4 \u0410\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043B\u043E\u0441\u044C: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${run.name} \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 (\u043F\u043E\u0442\u0440\u0430\u0447\u0435\u043D\u043E ${spent}).`,
15471
+ "",
15472
+ "\u0412\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043F\u043E \u043A\u043E\u0434\u0443 \u043D\u0435\u0442 \u2014 \u043D\u0438 \u043E\u0434\u043E\u0431\u0440\u0435\u043D\u0438\u044F, \u043D\u0438 \u0437\u0430\u043C\u0435\u0447\u0430\u043D\u0438\u0439: \u0441\u0435\u0441\u0441\u0438\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B \u0431\u044E\u0434\u0436\u0435\u0442 \u0440\u0430\u043D\u044C\u0448\u0435, \u0447\u0435\u043C \u0440\u0435\u0432\u044C\u044E \u0434\u043E\u0448\u043B\u043E \u0434\u043E \u043A\u043E\u043D\u0446\u0430.",
15473
+ "\u042D\u0442\u043E\u0442 PR \u043D\u0443\u0436\u043D\u043E \u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u0440\u0443\u043A\u0430\u043C\u0438. \u041D\u043E\u0432\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u0440\u043E\u0439 \u043D\u0430 \u044D\u0442\u043E\u0442 \u043A\u0440\u0443\u0433 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442 \u2014 \u043E\u043D \u0443\u043F\u0451\u0440\u0441\u044F \u0431\u044B \u0432 \u0442\u043E\u0442 \u0436\u0435 \u043F\u043E\u0442\u043E\u043B\u043E\u043A.",
15474
+ "",
15475
+ "\u041F\u043E\u0442\u043E\u043B\u043E\u043A \u0437\u0430\u0434\u0430\u0451\u0442\u0441\u044F \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 \u0440\u043E\u044F (\xAB\u0420\u043E\u0439\xBB \u2192 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442 \u043D\u0430 \u043F\u0440\u043E\u0433\u043E\u043D) \u2014 \u0435\u0441\u043B\u0438 \u0440\u0435\u0432\u044C\u044E \u0442\u0430\u043A\u043E\u0433\u043E PR \u0447\u0435\u0441\u0442\u043D\u043E \u0441\u0442\u043E\u0438\u0442 \u0434\u043E\u0440\u043E\u0436\u0435, \u043F\u043E\u0434\u043D\u0438\u043C\u0438\u0442\u0435 \u0447\u0438\u0441\u043B\u043E \u0438 \u043F\u043E\u0437\u043E\u0432\u0438\u0442\u0435 \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043D\u043E\u0432\u043E."
15476
+ ].join("\n");
15477
+ await Promise.all([
15478
+ run.pr_url && deps.prComment ? deps.prComment(run.pr_url, refusal).catch((e) => {
15479
+ deps.db.addEvent(run.id, "step", `\u043E\u0442\u043A\u0430\u0437 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0437\u0430\u043F\u043E\u0441\u0442\u0438\u043B\u0441\u044F \u0432 PR: ${e instanceof Error ? e.message : String(e)}`);
15480
+ }) : Promise.resolve(),
15481
+ run.worktree_path ? deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
15482
+ }) : Promise.resolve()
15483
+ ]);
15484
+ await failReviewerRun(deps, run, run.reviews_run_id, {
15485
+ reason,
15486
+ noteForReviewed: `${reviewBudgetMark(cycle)}: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${run.name} (#${run.id}) \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0431\u044E\u0434\u0436\u0435\u0442\u043E\u043C, \u043F\u043E\u0442\u0440\u0430\u0447\u0435\u043D\u043E ${spent} \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`,
15487
+ narration: `${droneLabel(run)} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043F\u043E\u0442\u043E\u043B\u043A\u043E\u043C \u0442\u0440\u0430\u0442 (${spent}) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u043E\u0441\u0442\u0430\u0432\u0438\u043B \u0432 PR \u043E\u0442\u043A\u0430\u0437 \u0438 \u0437\u043E\u0432\u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430.`,
15488
+ // worktree уже снесён выше — параллельно с отказом в PR.
15489
+ keepWorktree: true
15490
+ });
15491
+ return;
15492
+ }
15493
+ if (run.resolves_run_id != null) {
15494
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
15495
+ });
15496
+ deps.db.updateRun(run.id, { status: "failed", stop_reason: reason });
15497
+ deps.db.addEvent(run.id, "failed", reason);
15498
+ deps.db.addEvent(run.resolves_run_id, "conflict", `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 ${run.name} (#${run.id}) \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043F\u043E\u0442\u043E\u043B\u043A\u043E\u043C \u0442\u0440\u0430\u0442 (${spent}) \u2014 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D`);
15499
+ narrate(deps, `${droneLabel(run)} (\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440) \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442 (${spent}) \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u043F\u043E\u043F\u044B\u0442\u043A\u0443, \u0434\u0430\u043B\u044C\u0448\u0435 \u0440\u0435\u0448\u0430\u0435\u0442 \u0440\u043E\u0434\u0438\u0442\u0435\u043B\u044C\u0441\u043A\u0438\u0439 PR.`);
15500
+ return;
15501
+ }
15502
+ deps.db.updateRun(run.id, { status: "paused", stop_reason: reason });
15503
+ deps.db.addEvent(run.id, "paused", `${reason} \u2014 worktree \u0446\u0435\u043B, \u0430\u0432\u0442\u043E-\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u044F \u043D\u0435\u0442: \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0431\u044E\u0434\u0436\u0435\u0442\u0430 \u0443\u0436\u0435 \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u0442`);
15504
+ narrate(deps, `${droneLabel(run)} \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442 (${spent}) \u2014 \u0437\u0430\u043C\u0435\u0440, \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 worktree \u0446\u0435\u043B\u0430, \u0434\u0430\u043B\u044C\u0448\u0435 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0437\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C.`);
15505
+ }
15112
15506
  function buildEscalationCandidates(db) {
15113
15507
  const candidates = [];
15114
15508
  for (const run of db.listRuns("review")) {
@@ -15132,6 +15526,7 @@ async function tickOnce(deps) {
15132
15526
  const now = deps.now ? deps.now() : Date.now();
15133
15527
  const worktreeExists = deps.worktreeExists ?? existsSync16;
15134
15528
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
15529
+ deps.sampleHost?.();
15135
15530
  if (deps.rehydrate) {
15136
15531
  const rh = deps.rehydrate;
15137
15532
  const sinceLast = rh.lastAt == null ? Infinity : now - rh.lastAt;
@@ -15243,7 +15638,7 @@ async function tickOnce(deps) {
15243
15638
  continue;
15244
15639
  }
15245
15640
  }
15246
- if (deps.exitOutcome && deps.exitOutcome(run) === "graceful") continue;
15641
+ if (deps.exitOutcome && endedItself(deps.exitOutcome(run))) continue;
15247
15642
  deps.db.recordDeath(run.id, `\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440 (pid ${run.pid} \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442)`);
15248
15643
  }
15249
15644
  stormActive = await manageStorm(deps, now, worktreeExists, hasTranscript);
@@ -15278,12 +15673,11 @@ async function tickOnce(deps) {
15278
15673
  stormActive
15279
15674
  });
15280
15675
  if (salvaged) continue;
15281
- if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
15676
+ await failReviewerRun(deps, run, run.reviews_run_id, {
15677
+ reason: REVIEW_NO_VERDICT_REASON,
15678
+ noteForReviewed: `\u0440\u0435\u0432\u044C\u044E\u0435\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 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043B\u043E\u0441\u044C, \u043D\u0443\u0436\u0435\u043D \u043D\u043E\u0432\u044B\u0439 \u0437\u0430\u0445\u043E\u0434`,
15679
+ narration: `${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, \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D \u043A\u0430\u043A \u0443\u043F\u0430\u0432\u0448\u0438\u0439: \u0437\u0430\u0441\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E.`
15282
15680
  });
15283
- deps.db.updateRun(run.id, { status: "failed", stop_reason: REVIEW_NO_VERDICT_REASON });
15284
- deps.db.addEvent(run.id, "failed", REVIEW_NO_VERDICT_REASON);
15285
- deps.db.addEvent(run.reviews_run_id, "step", `\u0440\u0435\u0432\u044C\u044E\u0435\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 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043B\u043E\u0441\u044C, \u043D\u0443\u0436\u0435\u043D \u043D\u043E\u0432\u044B\u0439 \u0437\u0430\u0445\u043E\u0434`);
15286
- 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, \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D \u043A\u0430\u043A \u0443\u043F\u0430\u0432\u0448\u0438\u0439: \u0437\u0430\u0441\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E.`);
15287
15681
  continue;
15288
15682
  }
15289
15683
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
@@ -15372,6 +15766,8 @@ async function tickOnce(deps) {
15372
15766
  const attempts = deps.db.reviewerAttemptCountFor(run.id);
15373
15767
  const runEvents = deps.db.events(run.id);
15374
15768
  const unrecoverableMark = reviewUnrecoverableMark(run.branch || run.task_slug);
15769
+ const budgetMark = reviewBudgetMark(run.review_cycle ?? 0);
15770
+ const LATCHES = [unrecoverableMark, budgetMark];
15375
15771
  let verdictFromComment = null;
15376
15772
  let lastAttempt = null;
15377
15773
  if (attempts > 0 && deps.verdictComment) {
@@ -15391,7 +15787,7 @@ async function tickOnce(deps) {
15391
15787
  await wakeAuthorOnChanges(deps, run, now, droneCap);
15392
15788
  }
15393
15789
  }
15394
- } else if (runEvents.some((e) => (e.text ?? "").includes(unrecoverableMark))) {
15790
+ } else if (runEvents.some((e) => LATCHES.some((mark) => (e.text ?? "").includes(mark)))) {
15395
15791
  } else if (attempts >= REVIEW_SPAWN_LIMIT) {
15396
15792
  if (!runEvents.some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
15397
15793
  deps.db.addEvent(run.id, "step", `${REVIEW_ESCALATION_MARK}: ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u0440\u0435\u0432\u044C\u044E \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
@@ -15474,6 +15870,10 @@ async function tickOnce(deps) {
15474
15870
  }
15475
15871
  continue;
15476
15872
  }
15873
+ if (!alive && outcome === "budget") {
15874
+ await handleBudgetExhausted(deps, run, { now, droneCap, stormActive });
15875
+ continue;
15876
+ }
15477
15877
  if (!alive && run.kind === "hero") {
15478
15878
  const young = now - lastActivityMs(deps.db, run) < RESPAWN_YOUNG_MS;
15479
15879
  if (young && deps.respawn && run.respawns < RESPAWN_LIMIT) {
@@ -15631,7 +16031,7 @@ async function tickOnce(deps) {
15631
16031
  if (run.kind === "hero") continue;
15632
16032
  if (run.reviews_run_id != null) continue;
15633
16033
  if (run.stop_reason === RELIEF_PAUSE_REASON) continue;
15634
- if (run.stop_reason != null && run.stop_reason.startsWith(EXITED_NO_DONE_REASON)) continue;
16034
+ if (NO_AUTO_WAKE_REASONS.some((r) => run.stop_reason?.startsWith(r))) continue;
15635
16035
  if (run.pid != null && deps.isAlive(run.pid)) continue;
15636
16036
  if (!await projectQuotaGateAllows(deps, run)) continue;
15637
16037
  const wt = run.worktree_path;
@@ -16117,6 +16517,9 @@ async function runDaemon(opts) {
16117
16517
  }
16118
16518
  } : void 0;
16119
16519
  let lastDroneCap = null;
16520
+ let lastSwarmModels = null;
16521
+ let lastSwarmEfforts = null;
16522
+ let lastMaxCostUsd = null;
16120
16523
  let lastOwnCapacity = null;
16121
16524
  const hostCommandState = hostCommandStatePath(SWARM_HOME);
16122
16525
  let hostMaxDrones = loadMaxDronesOverride(hostCommandState);
@@ -16129,7 +16532,16 @@ async function runDaemon(opts) {
16129
16532
  executor: EXECUTOR,
16130
16533
  droneIsolation: DRONE_ISOLATION,
16131
16534
  channel: droneChannelLaunch,
16132
- droneCap: () => lastDroneCap
16535
+ droneCap: () => lastDroneCap,
16536
+ swarmModels: () => lastSwarmModels,
16537
+ swarmEfforts: () => lastSwarmEfforts,
16538
+ // TASWM-395: разрешённый уровень усилия впечатывается в прогон на спавне — иначе в учёте
16539
+ // виден только пин (то есть null у всех, кто шёл на настройке роя). Оттуда же его берут
16540
+ // респавн и `--continue`: прогон, начатый трудным, не должен подешеветь на середине.
16541
+ recordEffort: (runId, effort) => {
16542
+ db.updateRun(runId, { effort });
16543
+ },
16544
+ maxCostUsd: () => lastMaxCostUsd
16133
16545
  });
16134
16546
  let prepareReader;
16135
16547
  const zoneModels = new ZoneModels({ manualAreas: () => new Map(db.listRuns().map((r) => [r.task_slug, r.area])) });
@@ -16175,7 +16587,7 @@ async function runDaemon(opts) {
16175
16587
  const client = db;
16176
16588
  heartbeat = {
16177
16589
  push: async () => {
16178
- const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastOwnCapacity);
16590
+ const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastOwnCapacity, hostResourcesForProd());
16179
16591
  if (reply.maxDrones !== void 0) operatorMaxDrones = reply.maxDrones;
16180
16592
  if (reply.commands.length) onHostCommands(reply.commands);
16181
16593
  if (reply.logRequests.length) {
@@ -16265,12 +16677,24 @@ async function runDaemon(opts) {
16265
16677
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
16266
16678
  const client = db;
16267
16679
  const readMaxDrones = makeMaxDronesReader({
16268
- fetch: () => client.ownerMaxDrones(),
16680
+ // TASWM-364: одним чтением берём и кап, и модели роя — это соседние поля ОДНОГО
16681
+ // ответа who_am_i. Модели складываем в снимок для спавна тем же побочным ходом,
16682
+ // что и кап ниже: второго сетевого канала за ними заводить незачем.
16683
+ fetch: async () => {
16684
+ const settings = await client.ownerSwarmSettings();
16685
+ if (settings) {
16686
+ lastSwarmModels = settings.models;
16687
+ lastSwarmEfforts = settings.efforts;
16688
+ lastMaxCostUsd = settings.maxCostUsd;
16689
+ }
16690
+ return settings?.maxDrones ?? null;
16691
+ },
16269
16692
  fallback: SWARM_MAX_IN_FLIGHT > 0 ? SWARM_MAX_IN_FLIGHT : 3,
16270
16693
  ttlMs: 1e4
16271
16694
  });
16272
16695
  maxDrones = async () => {
16273
- const own = lastOwnCapacity = hostMaxDrones ?? await readMaxDrones();
16696
+ const live = await readMaxDrones();
16697
+ const own = lastOwnCapacity = hostMaxDrones ?? live;
16274
16698
  return lastDroneCap = operatorMaxDrones != null ? Math.min(own, operatorMaxDrones) : own;
16275
16699
  };
16276
16700
  }
@@ -16467,7 +16891,9 @@ async function runDaemon(opts) {
16467
16891
  repoHasCi: (run) => repoHasCiWorkflows(run.worktree_path, run.repo || opts.repo),
16468
16892
  isAlive: pidAlive,
16469
16893
  // Учёт cost/usage (TASWM-188): реальный аккаунтант поверх RUN_LOGS_DIR + db.
16470
- accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR }),
16894
+ // TASWM-364: swarmModels — ТОТ ЖЕ снимок, что уехал в launcher: цена токенов считается
16895
+ // по модели, на которой прогон реально работал.
16896
+ accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR, swarmModels: () => lastSwarmModels }),
16471
16897
  // Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
16472
16898
  // (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
16473
16899
  exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
@@ -16519,6 +16945,9 @@ async function runDaemon(opts) {
16519
16945
  transcriptMtimeMs: (p) => EXECUTOR.session.mtimeMs(p),
16520
16946
  // Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
16521
16947
  resourcesOk: hostResourcesOk,
16948
+ // Замер занятости CPU раз в тик (TASWM-397): окно фиксированной длины набирает пульс
16949
+ // тика, гейт ниже только читает готовое показание.
16950
+ sampleHost: () => void sampleCpuBusy(),
16522
16951
  // Ресурс-гейт v2 (TASWM-151): жёсткий порог активной разгрузки — те же сырые показания
16523
16952
  // хоста (общий кэш в resources.ts), но заведомо строже настроенные пороги.
16524
16953
  reliefNeeded: hostReliefNeeded,
@@ -16649,6 +17078,11 @@ async function runDaemon(opts) {
16649
17078
  // Сторож прогресса роя (TASWM-270): в отличие от anomalies выше — заведён БЕЗУСЛОВНО
16650
17079
  // (см. докблок конструирования watchdog выше — рунги а/б/в работают и без client-режима).
16651
17080
  watchdog,
17081
+ // TASWM-377: отказ в PR за мёртвого (ревьюер, срезанный потолком трат). Заведён
17082
+ // БЕЗУСЛОВНО, в отличие от ключей авто-ревью ниже: разбирать уже поднятого ревьюера
17083
+ // приходится и после того, как авто-ревью выключили, — а молчать за него тем более
17084
+ // некому.
17085
+ prComment: postPrComment,
16652
17086
  // Авто-ревью (TASWM-148): ключи есть только когда SWARM_AUTO_REVIEW включён (дефолт да) —
16653
17087
  // spawnReviewerFn/verdictCommentFn/reviewCutoffAtValue выставляются В ОДНОМ if(SWARM_AUTO_REVIEW)
16654
17088
  // блоке выше, поэтому один и тот же гейт на все три (без отдельных тернарных условий).
@@ -16974,6 +17408,7 @@ if (isEntrypoint) {
16974
17408
  }
16975
17409
  export {
16976
17410
  AWAITING_CI_REASON,
17411
+ BUDGET_STOP_REASON,
16977
17412
  CHANGES_WAKE_COOLDOWN_MS,
16978
17413
  CHANGES_WAKE_ESCALATION_MARK,
16979
17414
  CHANGES_WAKE_LIMIT,
@@ -17001,6 +17436,7 @@ export {
17001
17436
  RESOLVE_LIMIT,
17002
17437
  RESPAWN_LIMIT,
17003
17438
  RESPAWN_YOUNG_MS,
17439
+ REVIEW_BUDGET_MARK,
17004
17440
  REVIEW_CYCLE_ESCALATION_MARK,
17005
17441
  REVIEW_CYCLE_LIMIT,
17006
17442
  REVIEW_ESCALATION_MARK,
@@ -17030,6 +17466,7 @@ export {
17030
17466
  repoHasCiWorkflows,
17031
17467
  resolveConflict,
17032
17468
  respawnRun,
17469
+ reviewBudgetMark,
17033
17470
  reviewUnrecoverableMark,
17034
17471
  reviveStuck,
17035
17472
  runDaemon,