@taskless-app/swarm 0.6.5 → 0.6.7

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 +294 -47
  2. package/dist/overmind.js +430 -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,36 @@ 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
+
1316
+ // ../swarm-core/src/queuePlan.ts
1317
+ var MIN_MS, QUEUE_NOMINAL_RUN_MS, QUEUE_MIN_TAIL_MS, QUEUE_REVIEW_TAIL_MS;
1318
+ var init_queuePlan = __esm({
1319
+ "../swarm-core/src/queuePlan.ts"() {
1320
+ "use strict";
1321
+ init_status();
1322
+ init_hostPool();
1323
+ init_hosts();
1324
+ init_time();
1325
+ MIN_MS = 6e4;
1326
+ QUEUE_NOMINAL_RUN_MS = 25 * MIN_MS;
1327
+ QUEUE_MIN_TAIL_MS = 3 * MIN_MS;
1328
+ QUEUE_REVIEW_TAIL_MS = 10 * MIN_MS;
1329
+ }
1330
+ });
1331
+
1270
1332
  // ../swarm-core/src/index.ts
1271
1333
  var init_src = __esm({
1272
1334
  "../swarm-core/src/index.ts"() {
@@ -1284,12 +1346,15 @@ var init_src = __esm({
1284
1346
  init_names();
1285
1347
  init_baseBranch();
1286
1348
  init_hosts();
1349
+ init_hostResources();
1287
1350
  init_hostPool();
1288
1351
  init_hostCommands();
1289
1352
  init_relief();
1290
1353
  init_cost();
1291
1354
  init_runGuards();
1292
1355
  init_codeZones();
1356
+ init_effort();
1357
+ init_queuePlan();
1293
1358
  }
1294
1359
  });
1295
1360
 
@@ -1470,14 +1535,32 @@ function launchCostUsd(result, model) {
1470
1535
  return costFromUsage(result.usage, model);
1471
1536
  }
1472
1537
  function launchSpend(result) {
1473
- const u = result.usage ?? {};
1474
1538
  const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1475
- return {
1539
+ const sum = (u) => ({
1476
1540
  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)
1541
+ tokensOut: num(u.output_tokens)
1542
+ });
1543
+ const spend = sum(result.usage ?? {});
1544
+ if (spend.tokensIn === 0 && spend.tokensOut === 0 && result.modelUsage) {
1545
+ for (const m of Object.values(result.modelUsage)) {
1546
+ const perModel = sum(asTokenUsage(m));
1547
+ spend.tokensIn += perModel.tokensIn;
1548
+ spend.tokensOut += perModel.tokensOut;
1549
+ }
1550
+ }
1551
+ return { ...spend, durationMs: num(result.duration_ms) };
1552
+ }
1553
+ function asTokenUsage(m) {
1554
+ return {
1555
+ ...m?.inputTokens !== void 0 ? { input_tokens: m.inputTokens } : {},
1556
+ ...m?.outputTokens !== void 0 ? { output_tokens: m.outputTokens } : {},
1557
+ ...m?.cacheCreationInputTokens !== void 0 ? { cache_creation_input_tokens: m.cacheCreationInputTokens } : {},
1558
+ ...m?.cacheReadInputTokens !== void 0 ? { cache_read_input_tokens: m.cacheReadInputTokens } : {}
1479
1559
  };
1480
1560
  }
1561
+ function budgetExhausted(result) {
1562
+ return result.subtype === "error_max_budget_usd" || result.terminal_reason === "budget_exhausted";
1563
+ }
1481
1564
  var init_claudeResult = __esm({
1482
1565
  "src/claudeResult.ts"() {
1483
1566
  "use strict";
@@ -1489,8 +1572,10 @@ var init_claudeResult = __esm({
1489
1572
  import { readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
1490
1573
  import { homedir } from "os";
1491
1574
  import { join as join2 } from "path";
1492
- function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
1575
+ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings, maxBudgetUsd, effort) {
1493
1576
  const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
1577
+ const budgetFlags = typeof maxBudgetUsd === "number" && maxBudgetUsd > 0 ? ["--max-budget-usd", String(maxBudgetUsd)] : [];
1578
+ const effortFlags = effort ? ["--effort", effort] : [];
1494
1579
  const base3 = [
1495
1580
  "--dangerously-skip-permissions",
1496
1581
  ...ISOLATION_FLAGS,
@@ -1498,6 +1583,8 @@ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs
1498
1583
  cfgPath,
1499
1584
  ...settings ? ["--settings", settings] : [],
1500
1585
  ...pluginFlags,
1586
+ ...budgetFlags,
1587
+ ...effortFlags,
1501
1588
  "--output-format",
1502
1589
  "json",
1503
1590
  "-p",
@@ -1574,7 +1661,7 @@ var init_claudeCode = __esm({
1574
1661
  const settings = spec.denyPaths.length > 0 ? droneSettingsJson(spec.denyPaths) : void 0;
1575
1662
  return {
1576
1663
  bin: resolveClaudeBin(),
1577
- args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings)
1664
+ args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings, spec.maxBudgetUsd, spec.effort)
1578
1665
  };
1579
1666
  },
1580
1667
  parseResult(raw, model) {
@@ -1587,7 +1674,10 @@ var init_claudeCode = __esm({
1587
1674
  ...est.warning ? { warning: est.warning } : {},
1588
1675
  tokensIn: spend.tokensIn,
1589
1676
  tokensOut: spend.tokensOut,
1590
- durationMs: spend.durationMs
1677
+ durationMs: spend.durationMs,
1678
+ // TASWM-377: «упёрся в потолок» — отдельный факт об этом же итоге, а не отдельный
1679
+ // разбор файла: у claude он лежит в тех же полях, что деньги и токены.
1680
+ ...budgetExhausted(result) ? { stoppedByBudget: true } : {}
1591
1681
  };
1592
1682
  },
1593
1683
  session: {
@@ -1747,6 +1837,13 @@ ${notice}` : spec.prompt;
1747
1837
  SKIP_GIT_CHECK_FLAG,
1748
1838
  BYPASS_FLAG,
1749
1839
  ...spec.model ? ["-m", spec.model] : [],
1840
+ // TASWM-395: УСИЛИЕ РАССУЖДЕНИЯ (spec.effort) codex пока НЕ получает — и это сказано
1841
+ // вслух, как требует контракт адаптера (см. ExecutorLaunchSpec.effort), а не забыто.
1842
+ // Кандидат есть — `-c model_reasoning_effort=<low|medium|high>` — но словари не совпадают
1843
+ // (у нас пять уровней, xhigh/max пришлось бы схлопывать в high), а главное: непонятый
1844
+ // ключ в `-c` роняет НЕ один флаг, а весь спавн. Проверять это надо на живом codex, и
1845
+ // до такой проверки правильный размен — работающий codex на дефолтном усилии, а не
1846
+ // сломанный на настроенном. Провода до сюда доведены: останется дописать одну строку.
1750
1847
  ...mcpConfigArgs(spec),
1751
1848
  prompt
1752
1849
  ];
@@ -1849,6 +1946,10 @@ __export(config_exports, {
1849
1946
  SWARM_AUTO_REVIEW: () => SWARM_AUTO_REVIEW,
1850
1947
  SWARM_BACKEND: () => SWARM_BACKEND,
1851
1948
  SWARM_CLAIM_INTERVAL_MS: () => SWARM_CLAIM_INTERVAL_MS,
1949
+ SWARM_CPU_SANITY_BUSY: () => SWARM_CPU_SANITY_BUSY,
1950
+ SWARM_CPU_SANITY_LAG_MS: () => SWARM_CPU_SANITY_LAG_MS,
1951
+ SWARM_CPU_SANITY_LOAD_FACTOR: () => SWARM_CPU_SANITY_LOAD_FACTOR,
1952
+ SWARM_CPU_WINDOW_MS: () => SWARM_CPU_WINDOW_MS,
1852
1953
  SWARM_DRONE_BRIDGE_MS: () => SWARM_DRONE_BRIDGE_MS,
1853
1954
  SWARM_HOME: () => SWARM_HOME,
1854
1955
  SWARM_LOW_DISK_GB: () => SWARM_LOW_DISK_GB,
@@ -1973,7 +2074,7 @@ function reviewCutoffAt() {
1973
2074
  }
1974
2075
  return at;
1975
2076
  }
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;
2077
+ 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
2078
  var init_config = __esm({
1978
2079
  "src/config.ts"() {
1979
2080
  "use strict";
@@ -2003,6 +2104,10 @@ var init_config = __esm({
2003
2104
  SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
2004
2105
  SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
2005
2106
  SWARM_MAX_CPU_BUSY = envNum("SWARM_MAX_CPU_BUSY", 0.85);
2107
+ SWARM_CPU_WINDOW_MS = Math.max(1e3, envNum("SWARM_CPU_WINDOW_MS", 3e3));
2108
+ SWARM_CPU_SANITY_BUSY = envNum("SWARM_CPU_SANITY_BUSY", 0.9);
2109
+ SWARM_CPU_SANITY_LOAD_FACTOR = envNum("SWARM_CPU_SANITY_LOAD_FACTOR", 0.5);
2110
+ SWARM_CPU_SANITY_LAG_MS = envNum("SWARM_CPU_SANITY_LAG_MS", 6e4);
2006
2111
  SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
2007
2112
  SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
2008
2113
  SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
@@ -2173,22 +2278,59 @@ function cpuBusyFromTicks(prev, cur) {
2173
2278
  if (dTotal <= 0) return null;
2174
2279
  return Math.min(1, Math.max(0, 1 - dIdle / dTotal));
2175
2280
  }
2176
- function cpuBusyRatio() {
2177
- const cur = readCpuTicks(os.cpus());
2178
- const busy = cpuBusyFromTicks(prevCpuTicks, cur);
2179
- prevCpuTicks = cur;
2180
- return busy;
2281
+ function cpuBusyFromSamples(prev, cur, minWindowMs = SWARM_CPU_WINDOW_MS) {
2282
+ if (!prev) return null;
2283
+ if (cur.at - prev.at < minWindowMs) return null;
2284
+ return cpuBusyFromTicks(prev, cur);
2285
+ }
2286
+ function cpuBusySane(busy, load1, cpus, highForMs, sanityBusy = SWARM_CPU_SANITY_BUSY, sanityLoadFactor = SWARM_CPU_SANITY_LOAD_FACTOR, lagMs = SWARM_CPU_SANITY_LAG_MS) {
2287
+ if (busy < sanityBusy) return true;
2288
+ if (highForMs < lagMs) return true;
2289
+ const cores = cpus > 0 ? cpus : 1;
2290
+ if (!Number.isFinite(load1) || load1 < 0) return true;
2291
+ return load1 >= cores * sanityLoadFactor;
2292
+ }
2293
+ function sampleCpuBusy(now = Date.now(), ticks = readCpuTicks(os.cpus())) {
2294
+ const cur = { at: now, ...ticks };
2295
+ if (!cpuWindowBase) {
2296
+ cpuWindowBase = cur;
2297
+ return cpuBusyValue;
2298
+ }
2299
+ if (cur.at - cpuWindowBase.at < SWARM_CPU_WINDOW_MS) return cpuBusyValue;
2300
+ cpuBusyValue = cpuBusyFromSamples(cpuWindowBase, cur);
2301
+ cpuWindowBase = cur;
2302
+ return cpuBusyValue;
2303
+ }
2304
+ function cpuBusyReading() {
2305
+ return cpuBusyValue;
2306
+ }
2307
+ function trustedCpuBusy(busy, load1, cpus, now) {
2308
+ if (busy == null || busy < SWARM_CPU_SANITY_BUSY) {
2309
+ cpuHighSince = null;
2310
+ return busy;
2311
+ }
2312
+ cpuHighSince ??= now;
2313
+ if (cpuBusySane(busy, load1, cpus, now - cpuHighSince)) return busy;
2314
+ if (now - cpuSanityWarnAt >= CPU_SANITY_WARN_EVERY_MS) {
2315
+ cpuSanityWarnAt = now;
2316
+ console.warn(
2317
+ `[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.`
2318
+ );
2319
+ }
2320
+ return null;
2181
2321
  }
2182
2322
  function hostReading() {
2183
2323
  const now = Date.now();
2184
2324
  if (readingCache && now - readingCache.at < RESOURCES_CACHE_MS) return readingCache.reading;
2325
+ const load1 = os.loadavg()[0] ?? 0;
2326
+ const cpus = os.cpus().length;
2327
+ sampleCpuBusy(now);
2185
2328
  const reading = {
2186
- load1: os.loadavg()[0] ?? 0,
2187
- cpus: os.cpus().length,
2329
+ load1,
2330
+ cpus,
2188
2331
  freeGb: availableMemBytes() / 1e9,
2189
2332
  fdLoadRatio: fileDescriptorLoadRatio(),
2190
- cpuBusy: cpuBusyRatio(),
2191
- // TASWM-234: раз в тик (кэш выше) — иначе окно дельты схлопнется
2333
+ cpuBusy: trustedCpuBusy(cpuBusyReading(), load1, cpus, now),
2192
2334
  totalGb: os.totalmem() / 1e9,
2193
2335
  disk: diskGb()
2194
2336
  };
@@ -2247,6 +2389,22 @@ function pressureReport(r, minFreeGb = SWARM_RELIEF_MIN_FREE_GB, maxCpuBusy = SW
2247
2389
  function hostPressure() {
2248
2390
  return pressureReport(hostReading());
2249
2391
  }
2392
+ function resourcesForProd(r, measuredAt) {
2393
+ const gb = (v) => Math.round(v * 10) / 10;
2394
+ return {
2395
+ // null — busy% ещё не измерена (первый тик демона: она дельта тиков CPU). Ноль вместо
2396
+ // неё сказал бы «процессор простаивает» ровно тогда, когда мы про него не знаем.
2397
+ cpuBusyPct: r.cpuBusy == null ? null : Math.round(r.cpuBusy * 100),
2398
+ memFreeGb: gb(r.freeGb),
2399
+ memTotalGb: gb(r.totalGb),
2400
+ diskFreeGb: r.disk ? gb(r.disk.freeGb) : null,
2401
+ diskTotalGb: r.disk ? gb(r.disk.totalGb) : null,
2402
+ measuredAt
2403
+ };
2404
+ }
2405
+ function hostResourcesForProd() {
2406
+ return resourcesForProd(hostReading(), (/* @__PURE__ */ new Date()).toISOString());
2407
+ }
2250
2408
  function hostResourceSnapshot() {
2251
2409
  const r = hostReading();
2252
2410
  const reason = gateReason(r.load1, r.cpus, r.freeGb, r.fdLoadRatio, void 0, void 0, void 0, r.cpuBusy);
@@ -2261,13 +2419,17 @@ function hostResourceSnapshot() {
2261
2419
  thresholds: { maxLoadFactor: SWARM_MAX_LOAD_FACTOR, minFreeGb: SWARM_MIN_FREE_GB, maxFdLoadFactor: SWARM_MAX_FD_LOAD_FACTOR, maxCpuBusy: SWARM_MAX_CPU_BUSY }
2262
2420
  };
2263
2421
  }
2264
- var prevCpuTicks, RESOURCES_CACHE_MS, readingCache;
2422
+ var cpuWindowBase, cpuBusyValue, cpuHighSince, cpuSanityWarnAt, CPU_SANITY_WARN_EVERY_MS, RESOURCES_CACHE_MS, readingCache;
2265
2423
  var init_resources = __esm({
2266
2424
  "src/resources.ts"() {
2267
2425
  "use strict";
2268
2426
  init_quietExec();
2269
2427
  init_config();
2270
- prevCpuTicks = null;
2428
+ cpuWindowBase = null;
2429
+ cpuBusyValue = null;
2430
+ cpuHighSince = null;
2431
+ cpuSanityWarnAt = 0;
2432
+ CPU_SANITY_WARN_EVERY_MS = 5 * 6e4;
2271
2433
  RESOURCES_CACHE_MS = 250;
2272
2434
  readingCache = null;
2273
2435
  }
@@ -4503,6 +4665,10 @@ function rowToRun(row) {
4503
4665
  area: row.area ?? null,
4504
4666
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
4505
4667
  model: row.model ?? null,
4668
+ // TASWM-395: уровень усилия — через toEffortLevel, а не сырой строкой: в зеркале может
4669
+ // лежать значение, записанное демоном другой версии (словарь уровней задаёт CLI), и
4670
+ // непонятное здесь означает «уровень не задан», а не сломанный тип.
4671
+ effort: toEffortLevel(row.effort),
4506
4672
  cost_usd: row.cost_usd ?? 0,
4507
4673
  // Токены/время работы (TASWM-353) — старые прогоны без колонок → 0.
4508
4674
  tokens_in: row.tokens_in ?? 0,
@@ -4572,6 +4738,7 @@ CREATE TABLE IF NOT EXISTS runs (
4572
4738
  area TEXT,
4573
4739
  depends_on TEXT,
4574
4740
  model TEXT,
4741
+ effort TEXT,
4575
4742
  cost_usd REAL NOT NULL DEFAULT 0,
4576
4743
  tokens_in INTEGER NOT NULL DEFAULT 0,
4577
4744
  tokens_out INTEGER NOT NULL DEFAULT 0,
@@ -4673,7 +4840,10 @@ CREATE TABLE IF NOT EXISTS imports (
4673
4840
  // Чем обошёлся прогон, кроме денег (TASWM-353): токены вход/выход и время работы.
4674
4841
  "ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
4675
4842
  "ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
4676
- "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0"
4843
+ "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0",
4844
+ // TASWM-395: усилие рассуждения прогона (`claude --effort`). Nullable без DEFAULT —
4845
+ // NULL это и все прогоны до фичи, и прогоны на дефолте харнесса.
4846
+ "ALTER TABLE runs ADD COLUMN effort TEXT"
4677
4847
  ]) {
4678
4848
  try {
4679
4849
  this.db.exec(alter);
@@ -4693,7 +4863,7 @@ CREATE TABLE IF NOT EXISTS imports (
4693
4863
  createRun(input) {
4694
4864
  const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
4695
4865
  const res = this.db.prepare(
4696
- "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
4866
+ "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
4697
4867
  ).run(
4698
4868
  input.task_slug,
4699
4869
  name,
@@ -4706,6 +4876,8 @@ CREATE TABLE IF NOT EXISTS imports (
4706
4876
  input.area ?? null,
4707
4877
  input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
4708
4878
  input.model ?? null,
4879
+ input.effort ?? null,
4880
+ // усилие рассуждения (TASWM-395); null → разрешит спавн
4709
4881
  input.decision ? JSON.stringify(input.decision) : null,
4710
4882
  input.resolves_run_id ?? null,
4711
4883
  input.reviews_run_id ?? null,
@@ -5630,6 +5802,8 @@ var SwarmClient = class _SwarmClient {
5630
5802
  // предикат «репо есть локально» для решения, брать ли свободный прогон.
5631
5803
  host;
5632
5804
  repoExists;
5805
+ // TASWM-398: ЧЕМ эта машина исполняет прогоны — id адаптера (claude-code|codex).
5806
+ executorId;
5633
5807
  // Зеркало.
5634
5808
  runs = /* @__PURE__ */ new Map();
5635
5809
  runtime = /* @__PURE__ */ new Map();
@@ -5707,6 +5881,7 @@ var SwarmClient = class _SwarmClient {
5707
5881
  this.host = opts.host ?? null;
5708
5882
  this.repoExists = opts.repoExists ?? existsSync5;
5709
5883
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
5884
+ this.executorId = opts.executorId ?? EXECUTOR.id;
5710
5885
  this.outboxPath = opts.outboxPath;
5711
5886
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
5712
5887
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
@@ -5820,8 +5995,10 @@ var SwarmClient = class _SwarmClient {
5820
5995
  runFields(patch) {
5821
5996
  const out = {};
5822
5997
  if ("status" in patch && patch.status !== void 0) out.status = patch.status;
5998
+ if (patch.status === "running") out.executor = this.executorId;
5823
5999
  if ("area" in patch) out.area = patch.area ?? null;
5824
6000
  if ("model" in patch) out.model = patch.model ?? null;
6001
+ if ("effort" in patch) out.effort = patch.effort ?? null;
5825
6002
  if ("name" in patch) out.droneName = patch.name ?? null;
5826
6003
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
5827
6004
  if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
@@ -5892,6 +6069,9 @@ var SwarmClient = class _SwarmClient {
5892
6069
  area: input.area ?? null,
5893
6070
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
5894
6071
  model: input.model ?? null,
6072
+ // Усилие рассуждения (TASWM-395): пин, если его поставили на спавне; иначе null —
6073
+ // разрешённый уровень впечатает launcher, когда действительно поднимет дрона.
6074
+ effort: input.effort ?? null,
5895
6075
  cost_usd: 0,
5896
6076
  tokens_in: 0,
5897
6077
  // копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
@@ -5944,6 +6124,8 @@ var SwarmClient = class _SwarmClient {
5944
6124
  status: "queued",
5945
6125
  area: d.area,
5946
6126
  model: d.model,
6127
+ effort: d.effort,
6128
+ // усилие рассуждения (TASWM-395), если пинили на спавне
5947
6129
  name: d.name,
5948
6130
  repo: d.repo,
5949
6131
  depends_on: d.depends_on,
@@ -6721,11 +6903,24 @@ var SwarmClient = class _SwarmClient {
6721
6903
  const n = obj?.maxDrones;
6722
6904
  if (!(typeof n === "number" && Number.isFinite(n) && n > 0)) return null;
6723
6905
  const str2 = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
6906
+ const cost = obj?.swarmSettings?.maxCostUsd;
6724
6907
  return {
6725
6908
  maxDrones: Math.trunc(n),
6726
6909
  // Старый прод (до TASWM-364) поля swarmSettings не отдаёт вовсе — обе модели null,
6727
6910
  // то есть прежнее поведение: env машины и дефолт харнесса.
6728
- models: { drone: str2(obj?.swarmSettings?.droneModel), reviewer: str2(obj?.swarmSettings?.reviewerModel) }
6911
+ models: { drone: str2(obj?.swarmSettings?.droneModel), reviewer: str2(obj?.swarmSettings?.reviewerModel) },
6912
+ // TASWM-395: уровни усилия — из того же ответа и с той же семантикой, что модели:
6913
+ // старый прод поля не отдаёт → оба null, то есть env машины и дефолт харнесса.
6914
+ // toEffortLevel заодно отсекает мусор, записанный в uiPrefs мимо схемы.
6915
+ efforts: {
6916
+ drone: toEffortLevel(obj?.swarmSettings?.droneEffort),
6917
+ reviewer: toEffortLevel(obj?.swarmSettings?.reviewerEffort)
6918
+ },
6919
+ // TASWM-377: потолок трат — из того же ответа, тем же одним чтением. НОЛЬ здесь
6920
+ // значим («гарантия выключена осознанно»), поэтому пропускаем и его, а не только
6921
+ // положительные: null остаётся ровно за «ручка не задана / старый прод», и только
6922
+ // он уводит спавн на env-фолбэк и дефолт ядра.
6923
+ maxCostUsd: typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : null
6729
6924
  };
6730
6925
  }
6731
6926
  /**
@@ -6864,19 +7059,25 @@ var SwarmClient = class _SwarmClient {
6864
7059
  if (res?.isError) return [];
6865
7060
  const raw = (res.structuredContent ?? {}).hosts;
6866
7061
  if (!Array.isArray(raw)) return [];
6867
- return raw.filter((h) => typeof h === "object" && h !== null).filter((h) => typeof h.hostId === "string" && h.hostId).map((h) => ({
6868
- hostId: h.hostId,
6869
- hostName: typeof h.hostName === "string" && h.hostName ? h.hostName : h.hostId,
6870
- lastSeenAt: typeof h.lastSeenAt === "string" ? h.lastSeenAt : null,
6871
- // TASWM-298: репозитории машины — «на связи» ещё не значит «возьмёт эту задачу».
6872
- // Прод старой версии поля не пришлёт → пустой список, строка просто без хвоста.
6873
- reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : [],
6874
- // TASWM-326: машина на паузе — «на связи, но работу не берёт». Прод старой
6875
- // версии поля не пришлёт → undefined, строка выглядит как раньше.
6876
- ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
6877
- ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
6878
- ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {}
6879
- }));
7062
+ return raw.filter((h) => typeof h === "object" && h !== null).filter((h) => typeof h.hostId === "string" && h.hostId).map((h) => {
7063
+ const resources = parseHostResources(h.resources);
7064
+ return {
7065
+ hostId: h.hostId,
7066
+ hostName: typeof h.hostName === "string" && h.hostName ? h.hostName : h.hostId,
7067
+ lastSeenAt: typeof h.lastSeenAt === "string" ? h.lastSeenAt : null,
7068
+ // TASWM-298: репозитории машины — «на связи» ещё не значит «возьмёт эту задачу».
7069
+ // Прод старой версии поля не пришлёт → пустой список, строка просто без хвоста.
7070
+ reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : [],
7071
+ // TASWM-326: машина на паузе — «на связи, но работу не берёт». Прод старой
7072
+ // версии поля не пришлёт → undefined, строка выглядит как раньше.
7073
+ ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
7074
+ ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
7075
+ ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {},
7076
+ // Показаний нет (старый прод, машина со старым демоном) — ключа не заводим вовсе,
7077
+ // ровно как с паузой выше: «не знаем» и так значит «не знаем».
7078
+ ...resources ? { resources } : {}
7079
+ };
7080
+ });
6880
7081
  }
6881
7082
  /**
6882
7083
  * TASWM-326: убрать ЭТУ машину из парка (`swarm remove`). Прод удаляет строку
@@ -6942,13 +7143,22 @@ var SwarmClient = class _SwarmClient {
6942
7143
  * разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
6943
7144
  * трогает прошлое значение — та же логика, что у repoFullNames.
6944
7145
  *
7146
+ * TASWM-394: тем же пульсом уезжают РЕСУРСЫ машины (`resources` — процессор, память,
7147
+ * диск и время замера). Это не новое измерение, а ровно то, что демон УЖЕ посчитал для
7148
+ * своего гейта спавна: числа в интерфейсе обязаны совпадать с теми, по которым он
7149
+ * отказывается поднимать дрона. Без них «машина на связи, свободна» и «машина физически
7150
+ * не может поднять дрона» выглядят в проде одинаково — 12.08 это стоило полудня
7151
+ * простоя, видимого только в логе на самой машине. Не передали (нечем измерить) → ключ
7152
+ * не шлём, прод прошлое не трогает — та же логика, что у capacity/repoFullNames.
7153
+ *
6945
7154
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
6946
7155
  */
6947
- async heartbeat(daemonVersion, pause, capacity) {
7156
+ async heartbeat(daemonVersion, pause, capacity, resources) {
6948
7157
  try {
6949
7158
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
6950
7159
  if (daemonVersion) args.daemonVersion = daemonVersion;
6951
7160
  if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
7161
+ if (resources) args.resources = resources;
6952
7162
  if (pause !== void 0) {
6953
7163
  args.paused = !!pause?.paused;
6954
7164
  if (pause?.reason) args.pauseReason = pause.reason;
@@ -7155,6 +7365,8 @@ var SwarmClient = class _SwarmClient {
7155
7365
  area: null,
7156
7366
  depends_on: [],
7157
7367
  model: null,
7368
+ effort: null,
7369
+ // усилие рассуждения (TASWM-395) — прод-поле, заполняется ниже из ответа
7158
7370
  cost_usd: 0,
7159
7371
  tokens_in: 0,
7160
7372
  // токены и время работы (TASWM-353) — прод-поля, заполняются ниже
@@ -7223,6 +7435,7 @@ var SwarmClient = class _SwarmClient {
7223
7435
  local.area = pr.area ?? null;
7224
7436
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
7225
7437
  local.model = pr.model ?? null;
7438
+ local.effort = toEffortLevel(pr.effort);
7226
7439
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
7227
7440
  local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
7228
7441
  local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
@@ -8218,17 +8431,25 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
8218
8431
  }
8219
8432
 
8220
8433
  // src/swarmModels.ts
8434
+ init_src();
8435
+ function pick(v) {
8436
+ const s = typeof v === "string" ? v.trim() : "";
8437
+ return s.length > 0 ? s : null;
8438
+ }
8439
+ function forKind(kind, swarm, norm) {
8440
+ if (!swarm) return null;
8441
+ return kind === "reviewer" ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
8442
+ }
8221
8443
  function resolveRunModel(run, swarm, env = process.env) {
8222
- const pick2 = (v) => {
8223
- const s = typeof v === "string" ? v.trim() : "";
8224
- return s.length > 0 ? s : null;
8225
- };
8226
- const fromSwarm = run.kind === "reviewer" ? pick2(swarm?.reviewer) ?? pick2(swarm?.drone) : pick2(swarm?.drone);
8227
- return pick2(run.model) ?? fromSwarm ?? pick2(env.SWARM_DRONE_MODEL) ?? null;
8444
+ return pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL) ?? null;
8445
+ }
8446
+ function resolveRunEffort(run, swarm, env = process.env) {
8447
+ return toEffortLevel(run.effort) ?? forKind(run.kind, swarm, toEffortLevel) ?? toEffortLevel(env.SWARM_DRONE_EFFORT) ?? null;
8228
8448
  }
8229
8449
 
8230
8450
  // src/launcher.ts
8231
8451
  init_executors();
8452
+ init_src();
8232
8453
  var requireFromHere = createRequire(import.meta.url);
8233
8454
  function resolveTsxCli() {
8234
8455
  try {
@@ -8240,6 +8461,15 @@ function resolveTsxCli() {
8240
8461
  }
8241
8462
  }
8242
8463
  var MCP_SERVER_NAME = "overmind";
8464
+ function launchBudgetUsd(limit, spentUsd, env = process.env) {
8465
+ return remainingRunBudgetUsd(limit ?? envCapUsd(env), spentUsd);
8466
+ }
8467
+ function envCapUsd(env) {
8468
+ const raw = env.SWARM_MAX_COST_USD;
8469
+ if (raw === void 0 || raw === "") return DEFAULT_RUN_LIMITS.maxCostUsd;
8470
+ const n = Number(raw);
8471
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_RUN_LIMITS.maxCostUsd;
8472
+ }
8243
8473
  function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
8244
8474
  const env = { ...base3 };
8245
8475
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
@@ -8365,6 +8595,12 @@ function makeLauncher(cfg) {
8365
8595
  }
8366
8596
  };
8367
8597
  const model = resolveRunModel(run, cfg.swarmModels?.() ?? null);
8598
+ const effort = resolveRunEffort(run, cfg.swarmEfforts?.() ?? null);
8599
+ try {
8600
+ cfg.recordEffort?.(run.id, effort);
8601
+ } catch (e) {
8602
+ 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)}`);
8603
+ }
8368
8604
  const realHome = cfg.droneIsolation?.realHome ?? homedir4();
8369
8605
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
8370
8606
  if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join12(cfg.droneIsolation.swarmHome, "token"))) {
@@ -8383,7 +8619,14 @@ function makeLauncher(cfg) {
8383
8619
  // Закрытые дрону пути имеют смысл только при теневом HOME: без него дрон и так
8384
8620
  // сидит в HOME демона, и запирать половину дверей — самообман.
8385
8621
  denyPaths: cfg.droneIsolation ? dronePrivatePaths(realHome, cfg.droneIsolation.swarmHome) : [],
8386
- realHome
8622
+ realHome,
8623
+ // TASWM-377: остаток бюджета ЭТОГО прогона — харнессу, чтобы он резал трату у себя, в
8624
+ // момент её совершения. Прод-сторож остаётся страховкой, но он по устройству видит
8625
+ // стоимость только по записанному итогу: живой прогон стоит для него ноль, сколько бы
8626
+ // ни жёг, — отсюда и $74 при лимите $20.
8627
+ maxBudgetUsd: launchBudgetUsd(cfg.maxCostUsd?.() ?? null, run.cost_usd),
8628
+ // TASWM-395: усилие рассуждения — харнессу флагом (claude `--effort`). null = флага нет.
8629
+ effort
8387
8630
  });
8388
8631
  let stdio = "ignore";
8389
8632
  let outFd;
@@ -8482,6 +8725,9 @@ function makeCostAccountant(deps) {
8482
8725
  init_executors();
8483
8726
  import { readdirSync as readdirSync9, readFileSync as readFileSync11 } from "fs";
8484
8727
  import { join as join14 } from "path";
8728
+ function endedItself(outcome) {
8729
+ return outcome === "graceful" || outcome === "budget";
8730
+ }
8485
8731
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
8486
8732
  function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
8487
8733
  const dir = join14(runLogsDir, String(runId));
@@ -8505,7 +8751,9 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
8505
8751
  } catch {
8506
8752
  return "unknown";
8507
8753
  }
8508
- return executor.parseResult(raw, null) ? "graceful" : "crash";
8754
+ const result = executor.parseResult(raw, null);
8755
+ if (!result) return "crash";
8756
+ return result.stoppedByBudget ? "budget" : "graceful";
8509
8757
  }
8510
8758
  function makeExitClassifier(runLogsDir, executor) {
8511
8759
  const exec = executor ?? resolveExecutor();
@@ -9207,6 +9455,9 @@ async function mergedPrForBranch(branch, repoDir, run = realRepoRunner) {
9207
9455
  async function closePR(prUrl, run = realRunner) {
9208
9456
  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)"]);
9209
9457
  }
9458
+ async function postPrComment(prUrl, body, run = realRunner) {
9459
+ await run(["pr", "comment", prUrl, "--body", body]);
9460
+ }
9210
9461
  var realCommentsRunner = async (prUrl) => (await $3`gh pr view ${prUrl} --json comments`).stdout;
9211
9462
  var VERDICT_RE = /^[^A-Za-z]*(APPROVED?|LGTM|(?:REQUEST(?:ED)?\s+)?CHANGES(?:\s+REQUESTED)?)\b/i;
9212
9463
  var APPROVE_RE = /^(APPROVE|LGTM)/i;
@@ -11113,7 +11364,7 @@ async function changedPaths(worktree, opts = {}) {
11113
11364
  // package.json
11114
11365
  var package_default = {
11115
11366
  name: "@taskless-app/swarm",
11116
- version: "0.6.5",
11367
+ version: "0.6.7",
11117
11368
  type: "module",
11118
11369
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
11119
11370
  keywords: [
@@ -11163,7 +11414,7 @@ var package_default = {
11163
11414
  execa: "^10.0.0",
11164
11415
  zod: "3.25.76"
11165
11416
  },
11166
- gitHead: "76ff5edd816a2b9dd7f10db62c8fde89eec7771f"
11417
+ gitHead: "08ee2a1a4bdb53ba1c25c92066104f0de3a23433"
11167
11418
  };
11168
11419
 
11169
11420
  // src/version.ts
@@ -11386,7 +11637,8 @@ function timelineState(db, now = Date.now(), overmindLog = []) {
11386
11637
  heroRunId: r.hero_run_id,
11387
11638
  reviewVerdict: r.review_verdict,
11388
11639
  reviewPhase: r.review_phase,
11389
- model: r.model
11640
+ model: r.model,
11641
+ effort: r.effort
11390
11642
  });
11391
11643
  }
11392
11644
  const liveLanes = lanes.filter((l) => l.live);
@@ -13160,9 +13412,9 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
13160
13412
  )
13161
13413
  );
13162
13414
  return {
13163
- since: pick(since, fresh),
13164
- lastFiredAt: pick(lastFiredAt, fresh),
13165
- fires: pick(fires, fresh),
13415
+ since: pick2(since, fresh),
13416
+ lastFiredAt: pick2(lastFiredAt, fresh),
13417
+ fires: pick2(fires, fresh),
13166
13418
  // TAS-485: очередь гашения НЕ фильтруем тем же TTL, что ключи. Она про
13167
13419
  // уведомление, которое УЖЕ висит у человека в инбоксе: пролежав неделю
13168
13420
  // недоставленным, гашение не становится лишним — оно становится нужнее.
@@ -13246,7 +13498,7 @@ function strings(raw) {
13246
13498
  if (!Array.isArray(raw)) return [];
13247
13499
  return raw.filter((v) => typeof v === "string" && v.length > 0);
13248
13500
  }
13249
- function pick(src, keys) {
13501
+ function pick2(src, keys) {
13250
13502
  const out = {};
13251
13503
  for (const k of keys) if (src[k] != null) out[k] = src[k];
13252
13504
  return out;
@@ -13644,13 +13896,20 @@ var CHANGES_WAKE_COOLDOWN_MS = Number(process.env.SWARM_CHANGES_WAKE_COOLDOWN_MS
13644
13896
  var CHANGES_WAKE_LIMIT = Number(process.env.SWARM_CHANGES_WAKE_LIMIT) || 3;
13645
13897
  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";
13646
13898
  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";
13899
+ 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";
13900
+ var NO_AUTO_WAKE_REASONS = [EXITED_NO_DONE_REASON, BUDGET_STOP_REASON];
13901
+ 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";
13902
+ function reviewBudgetMark(cycle) {
13903
+ return `${REVIEW_BUDGET_MARK} (\u043A\u0440\u0443\u0433 ${cycle})`;
13904
+ }
13647
13905
  var NEEDS_HUMAN_MARKS = [
13648
13906
  RESOLVE_ESCALATION_MARK,
13649
13907
  REVIEW_ESCALATION_MARK,
13650
13908
  REVIEW_UNRECOVERABLE_MARK,
13651
13909
  REVIEW_CYCLE_ESCALATION_MARK,
13652
13910
  CI_WAKE_ESCALATION_MARK,
13653
- CHANGES_WAKE_ESCALATION_MARK
13911
+ CHANGES_WAKE_ESCALATION_MARK,
13912
+ REVIEW_BUDGET_MARK
13654
13913
  ];
13655
13914
  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";
13656
13915
  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)";
@@ -14032,6 +14291,12 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
14032
14291
  `\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"}.`,
14033
14292
  "\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).",
14034
14293
  "\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).",
14294
+ // TASWM-377: у сессии есть ПОТОЛОК ТРАТ, и он режет её без предупреждения, прямо посреди
14295
+ // работы. Дрон об этом знать обязан: цена сессии растёт от ЧИСЛА ХОДОВ (каждый ход заново
14296
+ // оплачивает весь накопленный контекст), поэтому «просканирую репозиторий, чтобы вникнуть»
14297
+ // — это не тщательность, а способ не доехать до PR. Замер по прогонам роя: дорогие сессии
14298
+ // отличаются от дешёвых числом заходов, а не размером задачи.
14299
+ "\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.",
14035
14300
  // TASWM-293: живой случай SCE-5 — ревьюер заблокировал PR за отсутствие CI (у проекта
14036
14301
  // его нет и не предполагается), автора разбудили «закрывать замечания», замечаний по коду
14037
14302
  // не было, и дрон завёл в ЧУЖОМ репозитории .github/workflows/check.yml, чтобы пройти
@@ -14175,6 +14440,13 @@ function lastChangesSummary(db, reviewRunId) {
14175
14440
  }
14176
14441
  return null;
14177
14442
  }
14443
+ var REVIEW_SCOPE_BLOCK = [
14444
+ "\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:",
14445
+ "\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.",
14446
+ "\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.",
14447
+ "\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.",
14448
+ "\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."
14449
+ ].join("\n");
14178
14450
  function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachmentsBlock, verifyMode = SWARM_VERIFY) {
14179
14451
  const name = reviewer.name || `#${reviewer.id}`;
14180
14452
  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)";
@@ -14192,6 +14464,8 @@ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachments
14192
14464
  ] : [],
14193
14465
  "",
14194
14466
  "\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.",
14467
+ "",
14468
+ REVIEW_SCOPE_BLOCK,
14195
14469
  // TASWM-283: ревьюер читает ТУ ЖЕ задачу и так же слеп к её картинкам — без макета
14196
14470
  // «сделано ли как просили» он проверить не может, а UI-пункт чек-листа держится
14197
14471
  // ровно на этом. Кладём ему тот же каталог вложений, что и исполнителю.
@@ -15185,6 +15459,67 @@ async function salvageVerdictFromPrComment(deps, rv, reviewsRunId, since, ctx) {
15185
15459
  }
15186
15460
  return true;
15187
15461
  }
15462
+ async function failReviewerRun(deps, reviewer, reviewsRunId, opts) {
15463
+ if (!opts.keepWorktree && reviewer.worktree_path) {
15464
+ await deps.removeWorktree(reviewer.repo, reviewer.worktree_path).catch(() => {
15465
+ });
15466
+ }
15467
+ deps.db.updateRun(reviewer.id, { status: "failed", stop_reason: opts.reason });
15468
+ deps.db.addEvent(reviewer.id, "failed", opts.reason);
15469
+ deps.db.addEvent(reviewsRunId, "step", opts.noteForReviewed);
15470
+ narrate(deps, opts.narration);
15471
+ }
15472
+ async function handleBudgetExhausted(deps, run, ctx) {
15473
+ const spentUsd = deps.db.getRun(run.id)?.cost_usd ?? run.cost_usd;
15474
+ const spent = `$${spentUsd.toFixed(2)}`;
15475
+ const reason = `${BUDGET_STOP_REASON} (\u043F\u043E\u0442\u0440\u0430\u0447\u0435\u043D\u043E ${spent})`;
15476
+ if (run.reviews_run_id != null) {
15477
+ const salvaged = await salvageVerdictFromPrComment(deps, run, run.reviews_run_id, run.created_at, {
15478
+ 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})`,
15479
+ now: ctx.now,
15480
+ droneCap: ctx.droneCap,
15481
+ stormActive: ctx.stormActive
15482
+ });
15483
+ if (salvaged) return;
15484
+ const reviewed = deps.db.getRun(run.reviews_run_id);
15485
+ const cycle = reviewed?.review_cycle ?? 0;
15486
+ const refusal = [
15487
+ `\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}).`,
15488
+ "",
15489
+ "\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.",
15490
+ "\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.",
15491
+ "",
15492
+ "\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."
15493
+ ].join("\n");
15494
+ await Promise.all([
15495
+ run.pr_url && deps.prComment ? deps.prComment(run.pr_url, refusal).catch((e) => {
15496
+ 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)}`);
15497
+ }) : Promise.resolve(),
15498
+ run.worktree_path ? deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
15499
+ }) : Promise.resolve()
15500
+ ]);
15501
+ await failReviewerRun(deps, run, run.reviews_run_id, {
15502
+ reason,
15503
+ 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`,
15504
+ 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.`,
15505
+ // worktree уже снесён выше — параллельно с отказом в PR.
15506
+ keepWorktree: true
15507
+ });
15508
+ return;
15509
+ }
15510
+ if (run.resolves_run_id != null) {
15511
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
15512
+ });
15513
+ deps.db.updateRun(run.id, { status: "failed", stop_reason: reason });
15514
+ deps.db.addEvent(run.id, "failed", reason);
15515
+ 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`);
15516
+ 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.`);
15517
+ return;
15518
+ }
15519
+ deps.db.updateRun(run.id, { status: "paused", stop_reason: reason });
15520
+ 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`);
15521
+ 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.`);
15522
+ }
15188
15523
  function buildEscalationCandidates(db) {
15189
15524
  const candidates = [];
15190
15525
  for (const run of db.listRuns("review")) {
@@ -15208,6 +15543,7 @@ async function tickOnce(deps) {
15208
15543
  const now = deps.now ? deps.now() : Date.now();
15209
15544
  const worktreeExists = deps.worktreeExists ?? existsSync16;
15210
15545
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
15546
+ deps.sampleHost?.();
15211
15547
  if (deps.rehydrate) {
15212
15548
  const rh = deps.rehydrate;
15213
15549
  const sinceLast = rh.lastAt == null ? Infinity : now - rh.lastAt;
@@ -15319,7 +15655,7 @@ async function tickOnce(deps) {
15319
15655
  continue;
15320
15656
  }
15321
15657
  }
15322
- if (deps.exitOutcome && deps.exitOutcome(run) === "graceful") continue;
15658
+ if (deps.exitOutcome && endedItself(deps.exitOutcome(run))) continue;
15323
15659
  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)`);
15324
15660
  }
15325
15661
  stormActive = await manageStorm(deps, now, worktreeExists, hasTranscript);
@@ -15354,12 +15690,11 @@ async function tickOnce(deps) {
15354
15690
  stormActive
15355
15691
  });
15356
15692
  if (salvaged) continue;
15357
- if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
15693
+ await failReviewerRun(deps, run, run.reviews_run_id, {
15694
+ reason: REVIEW_NO_VERDICT_REASON,
15695
+ 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`,
15696
+ 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.`
15358
15697
  });
15359
- deps.db.updateRun(run.id, { status: "failed", stop_reason: REVIEW_NO_VERDICT_REASON });
15360
- deps.db.addEvent(run.id, "failed", REVIEW_NO_VERDICT_REASON);
15361
- 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`);
15362
- 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.`);
15363
15698
  continue;
15364
15699
  }
15365
15700
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
@@ -15448,6 +15783,8 @@ async function tickOnce(deps) {
15448
15783
  const attempts = deps.db.reviewerAttemptCountFor(run.id);
15449
15784
  const runEvents = deps.db.events(run.id);
15450
15785
  const unrecoverableMark = reviewUnrecoverableMark(run.branch || run.task_slug);
15786
+ const budgetMark = reviewBudgetMark(run.review_cycle ?? 0);
15787
+ const LATCHES = [unrecoverableMark, budgetMark];
15451
15788
  let verdictFromComment = null;
15452
15789
  let lastAttempt = null;
15453
15790
  if (attempts > 0 && deps.verdictComment) {
@@ -15467,7 +15804,7 @@ async function tickOnce(deps) {
15467
15804
  await wakeAuthorOnChanges(deps, run, now, droneCap);
15468
15805
  }
15469
15806
  }
15470
- } else if (runEvents.some((e) => (e.text ?? "").includes(unrecoverableMark))) {
15807
+ } else if (runEvents.some((e) => LATCHES.some((mark) => (e.text ?? "").includes(mark)))) {
15471
15808
  } else if (attempts >= REVIEW_SPAWN_LIMIT) {
15472
15809
  if (!runEvents.some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
15473
15810
  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`);
@@ -15550,6 +15887,10 @@ async function tickOnce(deps) {
15550
15887
  }
15551
15888
  continue;
15552
15889
  }
15890
+ if (!alive && outcome === "budget") {
15891
+ await handleBudgetExhausted(deps, run, { now, droneCap, stormActive });
15892
+ continue;
15893
+ }
15553
15894
  if (!alive && run.kind === "hero") {
15554
15895
  const young = now - lastActivityMs(deps.db, run) < RESPAWN_YOUNG_MS;
15555
15896
  if (young && deps.respawn && run.respawns < RESPAWN_LIMIT) {
@@ -15707,7 +16048,7 @@ async function tickOnce(deps) {
15707
16048
  if (run.kind === "hero") continue;
15708
16049
  if (run.reviews_run_id != null) continue;
15709
16050
  if (run.stop_reason === RELIEF_PAUSE_REASON) continue;
15710
- if (run.stop_reason != null && run.stop_reason.startsWith(EXITED_NO_DONE_REASON)) continue;
16051
+ if (NO_AUTO_WAKE_REASONS.some((r) => run.stop_reason?.startsWith(r))) continue;
15711
16052
  if (run.pid != null && deps.isAlive(run.pid)) continue;
15712
16053
  if (!await projectQuotaGateAllows(deps, run)) continue;
15713
16054
  const wt = run.worktree_path;
@@ -16194,6 +16535,8 @@ async function runDaemon(opts) {
16194
16535
  } : void 0;
16195
16536
  let lastDroneCap = null;
16196
16537
  let lastSwarmModels = null;
16538
+ let lastSwarmEfforts = null;
16539
+ let lastMaxCostUsd = null;
16197
16540
  let lastOwnCapacity = null;
16198
16541
  const hostCommandState = hostCommandStatePath(SWARM_HOME);
16199
16542
  let hostMaxDrones = loadMaxDronesOverride(hostCommandState);
@@ -16207,7 +16550,15 @@ async function runDaemon(opts) {
16207
16550
  droneIsolation: DRONE_ISOLATION,
16208
16551
  channel: droneChannelLaunch,
16209
16552
  droneCap: () => lastDroneCap,
16210
- swarmModels: () => lastSwarmModels
16553
+ swarmModels: () => lastSwarmModels,
16554
+ swarmEfforts: () => lastSwarmEfforts,
16555
+ // TASWM-395: разрешённый уровень усилия впечатывается в прогон на спавне — иначе в учёте
16556
+ // виден только пин (то есть null у всех, кто шёл на настройке роя). Оттуда же его берут
16557
+ // респавн и `--continue`: прогон, начатый трудным, не должен подешеветь на середине.
16558
+ recordEffort: (runId, effort) => {
16559
+ db.updateRun(runId, { effort });
16560
+ },
16561
+ maxCostUsd: () => lastMaxCostUsd
16211
16562
  });
16212
16563
  let prepareReader;
16213
16564
  const zoneModels = new ZoneModels({ manualAreas: () => new Map(db.listRuns().map((r) => [r.task_slug, r.area])) });
@@ -16253,7 +16604,7 @@ async function runDaemon(opts) {
16253
16604
  const client = db;
16254
16605
  heartbeat = {
16255
16606
  push: async () => {
16256
- const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastOwnCapacity);
16607
+ const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastOwnCapacity, hostResourcesForProd());
16257
16608
  if (reply.maxDrones !== void 0) operatorMaxDrones = reply.maxDrones;
16258
16609
  if (reply.commands.length) onHostCommands(reply.commands);
16259
16610
  if (reply.logRequests.length) {
@@ -16348,7 +16699,11 @@ async function runDaemon(opts) {
16348
16699
  // что и кап ниже: второго сетевого канала за ними заводить незачем.
16349
16700
  fetch: async () => {
16350
16701
  const settings = await client.ownerSwarmSettings();
16351
- if (settings) lastSwarmModels = settings.models;
16702
+ if (settings) {
16703
+ lastSwarmModels = settings.models;
16704
+ lastSwarmEfforts = settings.efforts;
16705
+ lastMaxCostUsd = settings.maxCostUsd;
16706
+ }
16352
16707
  return settings?.maxDrones ?? null;
16353
16708
  },
16354
16709
  fallback: SWARM_MAX_IN_FLIGHT > 0 ? SWARM_MAX_IN_FLIGHT : 3,
@@ -16607,6 +16962,9 @@ async function runDaemon(opts) {
16607
16962
  transcriptMtimeMs: (p) => EXECUTOR.session.mtimeMs(p),
16608
16963
  // Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
16609
16964
  resourcesOk: hostResourcesOk,
16965
+ // Замер занятости CPU раз в тик (TASWM-397): окно фиксированной длины набирает пульс
16966
+ // тика, гейт ниже только читает готовое показание.
16967
+ sampleHost: () => void sampleCpuBusy(),
16610
16968
  // Ресурс-гейт v2 (TASWM-151): жёсткий порог активной разгрузки — те же сырые показания
16611
16969
  // хоста (общий кэш в resources.ts), но заведомо строже настроенные пороги.
16612
16970
  reliefNeeded: hostReliefNeeded,
@@ -16737,6 +17095,11 @@ async function runDaemon(opts) {
16737
17095
  // Сторож прогресса роя (TASWM-270): в отличие от anomalies выше — заведён БЕЗУСЛОВНО
16738
17096
  // (см. докблок конструирования watchdog выше — рунги а/б/в работают и без client-режима).
16739
17097
  watchdog,
17098
+ // TASWM-377: отказ в PR за мёртвого (ревьюер, срезанный потолком трат). Заведён
17099
+ // БЕЗУСЛОВНО, в отличие от ключей авто-ревью ниже: разбирать уже поднятого ревьюера
17100
+ // приходится и после того, как авто-ревью выключили, — а молчать за него тем более
17101
+ // некому.
17102
+ prComment: postPrComment,
16740
17103
  // Авто-ревью (TASWM-148): ключи есть только когда SWARM_AUTO_REVIEW включён (дефолт да) —
16741
17104
  // spawnReviewerFn/verdictCommentFn/reviewCutoffAtValue выставляются В ОДНОМ if(SWARM_AUTO_REVIEW)
16742
17105
  // блоке выше, поэтому один и тот же гейт на все три (без отдельных тернарных условий).
@@ -17062,6 +17425,7 @@ if (isEntrypoint) {
17062
17425
  }
17063
17426
  export {
17064
17427
  AWAITING_CI_REASON,
17428
+ BUDGET_STOP_REASON,
17065
17429
  CHANGES_WAKE_COOLDOWN_MS,
17066
17430
  CHANGES_WAKE_ESCALATION_MARK,
17067
17431
  CHANGES_WAKE_LIMIT,
@@ -17089,6 +17453,7 @@ export {
17089
17453
  RESOLVE_LIMIT,
17090
17454
  RESPAWN_LIMIT,
17091
17455
  RESPAWN_YOUNG_MS,
17456
+ REVIEW_BUDGET_MARK,
17092
17457
  REVIEW_CYCLE_ESCALATION_MARK,
17093
17458
  REVIEW_CYCLE_LIMIT,
17094
17459
  REVIEW_ESCALATION_MARK,
@@ -17118,6 +17483,7 @@ export {
17118
17483
  repoHasCiWorkflows,
17119
17484
  resolveConflict,
17120
17485
  respawnRun,
17486
+ reviewBudgetMark,
17121
17487
  reviewUnrecoverableMark,
17122
17488
  reviveStuck,
17123
17489
  runDaemon,