@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/cli.js CHANGED
@@ -638,6 +638,52 @@ var init_hosts = __esm({
638
638
  }
639
639
  });
640
640
 
641
+ // ../swarm-core/src/hostResources.ts
642
+ function parseHostResources(raw) {
643
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
644
+ const v = raw;
645
+ const num = (x) => typeof x === "number" && Number.isFinite(x) ? x : null;
646
+ const memFreeGb = num(v.memFreeGb);
647
+ const memTotalGb = num(v.memTotalGb);
648
+ if (memFreeGb == null || memTotalGb == null) return null;
649
+ if (typeof v.measuredAt !== "string" || Number.isNaN(Date.parse(v.measuredAt))) return null;
650
+ const cpu = num(v.cpuBusyPct);
651
+ return {
652
+ cpuBusyPct: cpu == null ? null : Math.min(100, Math.max(0, cpu)),
653
+ memFreeGb,
654
+ memTotalGb,
655
+ diskFreeGb: num(v.diskFreeGb),
656
+ diskTotalGb: num(v.diskTotalGb),
657
+ measuredAt: v.measuredAt
658
+ };
659
+ }
660
+ function hostResourcesFresh(resources, nowMs, staleMs = HOST_RESOURCES_STALE_MS) {
661
+ if (!resources) return false;
662
+ const at = Date.parse(resources.measuredAt);
663
+ if (Number.isNaN(at)) return false;
664
+ return nowMs - at <= staleMs;
665
+ }
666
+ function gb(value) {
667
+ return value < 10 ? value.toFixed(1) : String(Math.round(value));
668
+ }
669
+ function hostResourcesText(resources, nowMs = Date.now(), staleMs = HOST_RESOURCES_STALE_MS) {
670
+ if (!hostResourcesFresh(resources, nowMs, staleMs)) return null;
671
+ const r = resources;
672
+ const parts = [];
673
+ if (r.cpuBusyPct != null) parts.push(`CPU ${Math.round(r.cpuBusyPct)}%`);
674
+ parts.push(`\u043F\u0430\u043C\u044F\u0442\u044C ${gb(r.memFreeGb)} \u0438\u0437 ${gb(r.memTotalGb)} \u0413\u0411`);
675
+ if (r.diskFreeGb != null && r.diskTotalGb != null) parts.push(`\u0434\u0438\u0441\u043A ${gb(r.diskFreeGb)} \u0438\u0437 ${gb(r.diskTotalGb)} \u0413\u0411`);
676
+ return parts.join(" \xB7 ");
677
+ }
678
+ var HOST_RESOURCES_STALE_MS;
679
+ var init_hostResources = __esm({
680
+ "../swarm-core/src/hostResources.ts"() {
681
+ "use strict";
682
+ init_hosts();
683
+ HOST_RESOURCES_STALE_MS = HOST_HEARTBEAT_STALE_MS;
684
+ }
685
+ });
686
+
641
687
  // ../swarm-core/src/hostPool.ts
642
688
  var init_hostPool = __esm({
643
689
  "../swarm-core/src/hostPool.ts"() {
@@ -690,7 +736,12 @@ var init_cost = __esm({
690
736
  });
691
737
 
692
738
  // ../swarm-core/src/runGuards.ts
693
- var DEFAULT_RUN_LIMITS;
739
+ function remainingRunBudgetUsd(maxCostUsd, spentUsd) {
740
+ if (!(maxCostUsd > 0)) return null;
741
+ const spent = Number.isFinite(spentUsd) && spentUsd > 0 ? spentUsd : 0;
742
+ return Math.max(MIN_RUN_BUDGET_USD, Math.round((maxCostUsd - spent) * 100) / 100);
743
+ }
744
+ var DEFAULT_RUN_LIMITS, MIN_RUN_BUDGET_USD;
694
745
  var init_runGuards = __esm({
695
746
  "../swarm-core/src/runGuards.ts"() {
696
747
  "use strict";
@@ -700,6 +751,7 @@ var init_runGuards = __esm({
700
751
  stepTimeoutMs: 90 * 6e4,
701
752
  lostMs: 30 * 6e4
702
753
  };
754
+ MIN_RUN_BUDGET_USD = 0.05;
703
755
  }
704
756
  });
705
757
 
@@ -1087,6 +1139,20 @@ var init_codeZones = __esm({
1087
1139
  }
1088
1140
  });
1089
1141
 
1142
+ // ../swarm-core/src/effort.ts
1143
+ function toEffortLevel(raw) {
1144
+ const s = typeof raw === "string" ? raw.trim().toLowerCase() : "";
1145
+ return EFFORT_SET.has(s) ? s : null;
1146
+ }
1147
+ var EFFORT_LEVELS, EFFORT_SET;
1148
+ var init_effort = __esm({
1149
+ "../swarm-core/src/effort.ts"() {
1150
+ "use strict";
1151
+ EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
1152
+ EFFORT_SET = new Set(EFFORT_LEVELS);
1153
+ }
1154
+ });
1155
+
1090
1156
  // ../swarm-core/src/index.ts
1091
1157
  var init_src = __esm({
1092
1158
  "../swarm-core/src/index.ts"() {
@@ -1104,12 +1170,14 @@ var init_src = __esm({
1104
1170
  init_names();
1105
1171
  init_baseBranch();
1106
1172
  init_hosts();
1173
+ init_hostResources();
1107
1174
  init_hostPool();
1108
1175
  init_hostCommands();
1109
1176
  init_relief();
1110
1177
  init_cost();
1111
1178
  init_runGuards();
1112
1179
  init_codeZones();
1180
+ init_effort();
1113
1181
  }
1114
1182
  });
1115
1183
 
@@ -1253,14 +1321,32 @@ function launchCostUsd(result, model) {
1253
1321
  return costFromUsage(result.usage, model);
1254
1322
  }
1255
1323
  function launchSpend(result) {
1256
- const u = result.usage ?? {};
1257
1324
  const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1258
- return {
1325
+ const sum = (u) => ({
1259
1326
  tokensIn: num(u.input_tokens) + num(u.cache_creation_input_tokens) + num(u.cache_read_input_tokens),
1260
- tokensOut: num(u.output_tokens),
1261
- durationMs: num(result.duration_ms)
1327
+ tokensOut: num(u.output_tokens)
1328
+ });
1329
+ const spend = sum(result.usage ?? {});
1330
+ if (spend.tokensIn === 0 && spend.tokensOut === 0 && result.modelUsage) {
1331
+ for (const m of Object.values(result.modelUsage)) {
1332
+ const perModel = sum(asTokenUsage(m));
1333
+ spend.tokensIn += perModel.tokensIn;
1334
+ spend.tokensOut += perModel.tokensOut;
1335
+ }
1336
+ }
1337
+ return { ...spend, durationMs: num(result.duration_ms) };
1338
+ }
1339
+ function asTokenUsage(m) {
1340
+ return {
1341
+ ...m?.inputTokens !== void 0 ? { input_tokens: m.inputTokens } : {},
1342
+ ...m?.outputTokens !== void 0 ? { output_tokens: m.outputTokens } : {},
1343
+ ...m?.cacheCreationInputTokens !== void 0 ? { cache_creation_input_tokens: m.cacheCreationInputTokens } : {},
1344
+ ...m?.cacheReadInputTokens !== void 0 ? { cache_read_input_tokens: m.cacheReadInputTokens } : {}
1262
1345
  };
1263
1346
  }
1347
+ function budgetExhausted(result) {
1348
+ return result.subtype === "error_max_budget_usd" || result.terminal_reason === "budget_exhausted";
1349
+ }
1264
1350
  var init_claudeResult = __esm({
1265
1351
  "src/claudeResult.ts"() {
1266
1352
  "use strict";
@@ -1272,8 +1358,10 @@ var init_claudeResult = __esm({
1272
1358
  import { readdirSync as readdirSync2, statSync, writeFileSync } from "fs";
1273
1359
  import { homedir } from "os";
1274
1360
  import { join } from "path";
1275
- function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
1361
+ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings, maxBudgetUsd, effort) {
1276
1362
  const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
1363
+ const budgetFlags = typeof maxBudgetUsd === "number" && maxBudgetUsd > 0 ? ["--max-budget-usd", String(maxBudgetUsd)] : [];
1364
+ const effortFlags = effort ? ["--effort", effort] : [];
1277
1365
  const base3 = [
1278
1366
  "--dangerously-skip-permissions",
1279
1367
  ...ISOLATION_FLAGS,
@@ -1281,6 +1369,8 @@ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs
1281
1369
  cfgPath,
1282
1370
  ...settings ? ["--settings", settings] : [],
1283
1371
  ...pluginFlags,
1372
+ ...budgetFlags,
1373
+ ...effortFlags,
1284
1374
  "--output-format",
1285
1375
  "json",
1286
1376
  "-p",
@@ -1357,7 +1447,7 @@ var init_claudeCode = __esm({
1357
1447
  const settings = spec.denyPaths.length > 0 ? droneSettingsJson(spec.denyPaths) : void 0;
1358
1448
  return {
1359
1449
  bin: resolveClaudeBin(),
1360
- args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings)
1450
+ args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings, spec.maxBudgetUsd, spec.effort)
1361
1451
  };
1362
1452
  },
1363
1453
  parseResult(raw, model) {
@@ -1370,7 +1460,10 @@ var init_claudeCode = __esm({
1370
1460
  ...est.warning ? { warning: est.warning } : {},
1371
1461
  tokensIn: spend.tokensIn,
1372
1462
  tokensOut: spend.tokensOut,
1373
- durationMs: spend.durationMs
1463
+ durationMs: spend.durationMs,
1464
+ // TASWM-377: «упёрся в потолок» — отдельный факт об этом же итоге, а не отдельный
1465
+ // разбор файла: у claude он лежит в тех же полях, что деньги и токены.
1466
+ ...budgetExhausted(result) ? { stoppedByBudget: true } : {}
1374
1467
  };
1375
1468
  },
1376
1469
  session: {
@@ -1530,6 +1623,13 @@ ${notice}` : spec.prompt;
1530
1623
  SKIP_GIT_CHECK_FLAG,
1531
1624
  BYPASS_FLAG,
1532
1625
  ...spec.model ? ["-m", spec.model] : [],
1626
+ // TASWM-395: УСИЛИЕ РАССУЖДЕНИЯ (spec.effort) codex пока НЕ получает — и это сказано
1627
+ // вслух, как требует контракт адаптера (см. ExecutorLaunchSpec.effort), а не забыто.
1628
+ // Кандидат есть — `-c model_reasoning_effort=<low|medium|high>` — но словари не совпадают
1629
+ // (у нас пять уровней, xhigh/max пришлось бы схлопывать в high), а главное: непонятый
1630
+ // ключ в `-c` роняет НЕ один флаг, а весь спавн. Проверять это надо на живом codex, и
1631
+ // до такой проверки правильный размен — работающий codex на дефолтном усилии, а не
1632
+ // сломанный на настроенном. Провода до сюда доведены: останется дописать одну строку.
1533
1633
  ...mcpConfigArgs(spec),
1534
1634
  prompt
1535
1635
  ];
@@ -1635,7 +1735,7 @@ function tokenFromFile() {
1635
1735
  return "";
1636
1736
  }
1637
1737
  }
1638
- 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;
1738
+ 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;
1639
1739
  var init_config = __esm({
1640
1740
  "src/config.ts"() {
1641
1741
  "use strict";
@@ -1665,6 +1765,10 @@ var init_config = __esm({
1665
1765
  SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
1666
1766
  SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
1667
1767
  SWARM_MAX_CPU_BUSY = envNum("SWARM_MAX_CPU_BUSY", 0.85);
1768
+ SWARM_CPU_WINDOW_MS = Math.max(1e3, envNum("SWARM_CPU_WINDOW_MS", 3e3));
1769
+ SWARM_CPU_SANITY_BUSY = envNum("SWARM_CPU_SANITY_BUSY", 0.9);
1770
+ SWARM_CPU_SANITY_LOAD_FACTOR = envNum("SWARM_CPU_SANITY_LOAD_FACTOR", 0.5);
1771
+ SWARM_CPU_SANITY_LAG_MS = envNum("SWARM_CPU_SANITY_LAG_MS", 6e4);
1668
1772
  SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
1669
1773
  SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
1670
1774
  SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
@@ -1884,7 +1988,7 @@ var init_package = __esm({
1884
1988
  "package.json"() {
1885
1989
  package_default = {
1886
1990
  name: "@taskless-app/swarm",
1887
- version: "0.6.4",
1991
+ version: "0.6.6",
1888
1992
  type: "module",
1889
1993
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
1890
1994
  keywords: [
@@ -1934,7 +2038,7 @@ var init_package = __esm({
1934
2038
  execa: "^10.0.0",
1935
2039
  zod: "3.25.76"
1936
2040
  },
1937
- gitHead: "12ecebcf1e60baab07e68cae3e78a29d78cb7fde"
2041
+ gitHead: "89e3fe0078755d130512c75ac85f195e6b950392"
1938
2042
  };
1939
2043
  }
1940
2044
  });
@@ -2301,6 +2405,10 @@ function rowToRun(row) {
2301
2405
  area: row.area ?? null,
2302
2406
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
2303
2407
  model: row.model ?? null,
2408
+ // TASWM-395: уровень усилия — через toEffortLevel, а не сырой строкой: в зеркале может
2409
+ // лежать значение, записанное демоном другой версии (словарь уровней задаёт CLI), и
2410
+ // непонятное здесь означает «уровень не задан», а не сломанный тип.
2411
+ effort: toEffortLevel(row.effort),
2304
2412
  cost_usd: row.cost_usd ?? 0,
2305
2413
  // Токены/время работы (TASWM-353) — старые прогоны без колонок → 0.
2306
2414
  tokens_in: row.tokens_in ?? 0,
@@ -2370,6 +2478,7 @@ CREATE TABLE IF NOT EXISTS runs (
2370
2478
  area TEXT,
2371
2479
  depends_on TEXT,
2372
2480
  model TEXT,
2481
+ effort TEXT,
2373
2482
  cost_usd REAL NOT NULL DEFAULT 0,
2374
2483
  tokens_in INTEGER NOT NULL DEFAULT 0,
2375
2484
  tokens_out INTEGER NOT NULL DEFAULT 0,
@@ -2471,7 +2580,10 @@ CREATE TABLE IF NOT EXISTS imports (
2471
2580
  // Чем обошёлся прогон, кроме денег (TASWM-353): токены вход/выход и время работы.
2472
2581
  "ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
2473
2582
  "ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
2474
- "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0"
2583
+ "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0",
2584
+ // TASWM-395: усилие рассуждения прогона (`claude --effort`). Nullable без DEFAULT —
2585
+ // NULL это и все прогоны до фичи, и прогоны на дефолте харнесса.
2586
+ "ALTER TABLE runs ADD COLUMN effort TEXT"
2475
2587
  ]) {
2476
2588
  try {
2477
2589
  this.db.exec(alter);
@@ -2491,7 +2603,7 @@ CREATE TABLE IF NOT EXISTS imports (
2491
2603
  createRun(input) {
2492
2604
  const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
2493
2605
  const res = this.db.prepare(
2494
- "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
2606
+ "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
2495
2607
  ).run(
2496
2608
  input.task_slug,
2497
2609
  name,
@@ -2504,6 +2616,8 @@ CREATE TABLE IF NOT EXISTS imports (
2504
2616
  input.area ?? null,
2505
2617
  input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
2506
2618
  input.model ?? null,
2619
+ input.effort ?? null,
2620
+ // усилие рассуждения (TASWM-395); null → разрешит спавн
2507
2621
  input.decision ? JSON.stringify(input.decision) : null,
2508
2622
  input.resolves_run_id ?? null,
2509
2623
  input.reviews_run_id ?? null,
@@ -4105,25 +4219,66 @@ function cpuBusyFromTicks(prev, cur) {
4105
4219
  if (dTotal <= 0) return null;
4106
4220
  return Math.min(1, Math.max(0, 1 - dIdle / dTotal));
4107
4221
  }
4108
- var prevCpuTicks = null;
4109
- function cpuBusyRatio() {
4110
- const cur = readCpuTicks(os.cpus());
4111
- const busy = cpuBusyFromTicks(prevCpuTicks, cur);
4112
- prevCpuTicks = cur;
4113
- return busy;
4222
+ function cpuBusyFromSamples(prev, cur, minWindowMs = SWARM_CPU_WINDOW_MS) {
4223
+ if (!prev) return null;
4224
+ if (cur.at - prev.at < minWindowMs) return null;
4225
+ return cpuBusyFromTicks(prev, cur);
4226
+ }
4227
+ function cpuBusySane(busy, load1, cpus, highForMs, sanityBusy = SWARM_CPU_SANITY_BUSY, sanityLoadFactor = SWARM_CPU_SANITY_LOAD_FACTOR, lagMs = SWARM_CPU_SANITY_LAG_MS) {
4228
+ if (busy < sanityBusy) return true;
4229
+ if (highForMs < lagMs) return true;
4230
+ const cores = cpus > 0 ? cpus : 1;
4231
+ if (!Number.isFinite(load1) || load1 < 0) return true;
4232
+ return load1 >= cores * sanityLoadFactor;
4233
+ }
4234
+ var cpuWindowBase = null;
4235
+ var cpuBusyValue = null;
4236
+ var cpuHighSince = null;
4237
+ var cpuSanityWarnAt = 0;
4238
+ var CPU_SANITY_WARN_EVERY_MS = 5 * 6e4;
4239
+ function sampleCpuBusy(now = Date.now(), ticks = readCpuTicks(os.cpus())) {
4240
+ const cur = { at: now, ...ticks };
4241
+ if (!cpuWindowBase) {
4242
+ cpuWindowBase = cur;
4243
+ return cpuBusyValue;
4244
+ }
4245
+ if (cur.at - cpuWindowBase.at < SWARM_CPU_WINDOW_MS) return cpuBusyValue;
4246
+ cpuBusyValue = cpuBusyFromSamples(cpuWindowBase, cur);
4247
+ cpuWindowBase = cur;
4248
+ return cpuBusyValue;
4249
+ }
4250
+ function cpuBusyReading() {
4251
+ return cpuBusyValue;
4252
+ }
4253
+ function trustedCpuBusy(busy, load1, cpus, now) {
4254
+ if (busy == null || busy < SWARM_CPU_SANITY_BUSY) {
4255
+ cpuHighSince = null;
4256
+ return busy;
4257
+ }
4258
+ cpuHighSince ??= now;
4259
+ if (cpuBusySane(busy, load1, cpus, now - cpuHighSince)) return busy;
4260
+ if (now - cpuSanityWarnAt >= CPU_SANITY_WARN_EVERY_MS) {
4261
+ cpuSanityWarnAt = now;
4262
+ console.warn(
4263
+ `[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.`
4264
+ );
4265
+ }
4266
+ return null;
4114
4267
  }
4115
4268
  var RESOURCES_CACHE_MS = 250;
4116
4269
  var readingCache = null;
4117
4270
  function hostReading() {
4118
4271
  const now = Date.now();
4119
4272
  if (readingCache && now - readingCache.at < RESOURCES_CACHE_MS) return readingCache.reading;
4273
+ const load1 = os.loadavg()[0] ?? 0;
4274
+ const cpus = os.cpus().length;
4275
+ sampleCpuBusy(now);
4120
4276
  const reading = {
4121
- load1: os.loadavg()[0] ?? 0,
4122
- cpus: os.cpus().length,
4277
+ load1,
4278
+ cpus,
4123
4279
  freeGb: availableMemBytes() / 1e9,
4124
4280
  fdLoadRatio: fileDescriptorLoadRatio(),
4125
- cpuBusy: cpuBusyRatio(),
4126
- // TASWM-234: раз в тик (кэш выше) — иначе окно дельты схлопнется
4281
+ cpuBusy: trustedCpuBusy(cpuBusyReading(), load1, cpus, now),
4127
4282
  totalGb: os.totalmem() / 1e9,
4128
4283
  disk: diskGb()
4129
4284
  };
@@ -4224,6 +4379,8 @@ var SwarmClient = class _SwarmClient {
4224
4379
  // предикат «репо есть локально» для решения, брать ли свободный прогон.
4225
4380
  host;
4226
4381
  repoExists;
4382
+ // TASWM-398: ЧЕМ эта машина исполняет прогоны — id адаптера (claude-code|codex).
4383
+ executorId;
4227
4384
  // Зеркало.
4228
4385
  runs = /* @__PURE__ */ new Map();
4229
4386
  runtime = /* @__PURE__ */ new Map();
@@ -4301,6 +4458,7 @@ var SwarmClient = class _SwarmClient {
4301
4458
  this.host = opts.host ?? null;
4302
4459
  this.repoExists = opts.repoExists ?? existsSync4;
4303
4460
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
4461
+ this.executorId = opts.executorId ?? EXECUTOR.id;
4304
4462
  this.outboxPath = opts.outboxPath;
4305
4463
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
4306
4464
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
@@ -4414,8 +4572,10 @@ var SwarmClient = class _SwarmClient {
4414
4572
  runFields(patch) {
4415
4573
  const out = {};
4416
4574
  if ("status" in patch && patch.status !== void 0) out.status = patch.status;
4575
+ if (patch.status === "running") out.executor = this.executorId;
4417
4576
  if ("area" in patch) out.area = patch.area ?? null;
4418
4577
  if ("model" in patch) out.model = patch.model ?? null;
4578
+ if ("effort" in patch) out.effort = patch.effort ?? null;
4419
4579
  if ("name" in patch) out.droneName = patch.name ?? null;
4420
4580
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
4421
4581
  if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
@@ -4486,6 +4646,9 @@ var SwarmClient = class _SwarmClient {
4486
4646
  area: input.area ?? null,
4487
4647
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
4488
4648
  model: input.model ?? null,
4649
+ // Усилие рассуждения (TASWM-395): пин, если его поставили на спавне; иначе null —
4650
+ // разрешённый уровень впечатает launcher, когда действительно поднимет дрона.
4651
+ effort: input.effort ?? null,
4489
4652
  cost_usd: 0,
4490
4653
  tokens_in: 0,
4491
4654
  // копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
@@ -4538,6 +4701,8 @@ var SwarmClient = class _SwarmClient {
4538
4701
  status: "queued",
4539
4702
  area: d.area,
4540
4703
  model: d.model,
4704
+ effort: d.effort,
4705
+ // усилие рассуждения (TASWM-395), если пинили на спавне
4541
4706
  name: d.name,
4542
4707
  repo: d.repo,
4543
4708
  depends_on: d.depends_on,
@@ -5291,6 +5456,19 @@ var SwarmClient = class _SwarmClient {
5291
5456
  * → null: ридер (makeMaxDronesReader) берёт env-фолбэк.
5292
5457
  */
5293
5458
  async ownerMaxDrones() {
5459
+ return (await this.ownerSwarmSettings())?.maxDrones ?? null;
5460
+ }
5461
+ /**
5462
+ * TASWM-364: настройки роя ЦЕЛИКОМ из того же who_am_i — живой кап дронов И модели, на
5463
+ * которых спавнятся дроны и ревьюеры (раздел «Рой» в настройках). Одним вызовом, а не
5464
+ * двумя: читатель у них один (ридер maxDrones раз в TTL), и второй сетевой поход за
5465
+ * соседним полем того же ответа был бы платой ни за что.
5466
+ *
5467
+ * Прод недоступен / ответ без валидного капа → null: вызывающий берёт env-фолбэки
5468
+ * (SWARM_MAX_IN_FLIGHT для капа, SWARM_DRONE_MODEL для модели). Модель без капа отдельно
5469
+ * не отдаём СОЗНАТЕЛЬНО — «ответ прода не разобрался» это одно событие, а не два.
5470
+ */
5471
+ async ownerSwarmSettings() {
5294
5472
  let res;
5295
5473
  try {
5296
5474
  res = await this.call("who_am_i", {});
@@ -5300,7 +5478,27 @@ var SwarmClient = class _SwarmClient {
5300
5478
  if (res?.isError) return null;
5301
5479
  const obj = jsonFromTextContent(res);
5302
5480
  const n = obj?.maxDrones;
5303
- return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
5481
+ if (!(typeof n === "number" && Number.isFinite(n) && n > 0)) return null;
5482
+ const str = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
5483
+ const cost = obj?.swarmSettings?.maxCostUsd;
5484
+ return {
5485
+ maxDrones: Math.trunc(n),
5486
+ // Старый прод (до TASWM-364) поля swarmSettings не отдаёт вовсе — обе модели null,
5487
+ // то есть прежнее поведение: env машины и дефолт харнесса.
5488
+ models: { drone: str(obj?.swarmSettings?.droneModel), reviewer: str(obj?.swarmSettings?.reviewerModel) },
5489
+ // TASWM-395: уровни усилия — из того же ответа и с той же семантикой, что модели:
5490
+ // старый прод поля не отдаёт → оба null, то есть env машины и дефолт харнесса.
5491
+ // toEffortLevel заодно отсекает мусор, записанный в uiPrefs мимо схемы.
5492
+ efforts: {
5493
+ drone: toEffortLevel(obj?.swarmSettings?.droneEffort),
5494
+ reviewer: toEffortLevel(obj?.swarmSettings?.reviewerEffort)
5495
+ },
5496
+ // TASWM-377: потолок трат — из того же ответа, тем же одним чтением. НОЛЬ здесь
5497
+ // значим («гарантия выключена осознанно»), поэтому пропускаем и его, а не только
5498
+ // положительные: null остаётся ровно за «ручка не задана / старый прод», и только
5499
+ // он уводит спавн на env-фолбэк и дефолт ядра.
5500
+ maxCostUsd: typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : null
5501
+ };
5304
5502
  }
5305
5503
  /**
5306
5504
  * Live verifyMode проекта из Taskless (TASWM-171): читает swarm_repo_config по
@@ -5438,19 +5636,25 @@ var SwarmClient = class _SwarmClient {
5438
5636
  if (res?.isError) return [];
5439
5637
  const raw = (res.structuredContent ?? {}).hosts;
5440
5638
  if (!Array.isArray(raw)) return [];
5441
- return raw.filter((h) => typeof h === "object" && h !== null).filter((h) => typeof h.hostId === "string" && h.hostId).map((h) => ({
5442
- hostId: h.hostId,
5443
- hostName: typeof h.hostName === "string" && h.hostName ? h.hostName : h.hostId,
5444
- lastSeenAt: typeof h.lastSeenAt === "string" ? h.lastSeenAt : null,
5445
- // TASWM-298: репозитории машины — «на связи» ещё не значит «возьмёт эту задачу».
5446
- // Прод старой версии поля не пришлёт → пустой список, строка просто без хвоста.
5447
- reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : [],
5448
- // TASWM-326: машина на паузе — «на связи, но работу не берёт». Прод старой
5449
- // версии поля не пришлёт → undefined, строка выглядит как раньше.
5450
- ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
5451
- ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
5452
- ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {}
5453
- }));
5639
+ return raw.filter((h) => typeof h === "object" && h !== null).filter((h) => typeof h.hostId === "string" && h.hostId).map((h) => {
5640
+ const resources = parseHostResources(h.resources);
5641
+ return {
5642
+ hostId: h.hostId,
5643
+ hostName: typeof h.hostName === "string" && h.hostName ? h.hostName : h.hostId,
5644
+ lastSeenAt: typeof h.lastSeenAt === "string" ? h.lastSeenAt : null,
5645
+ // TASWM-298: репозитории машины — «на связи» ещё не значит «возьмёт эту задачу».
5646
+ // Прод старой версии поля не пришлёт → пустой список, строка просто без хвоста.
5647
+ reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : [],
5648
+ // TASWM-326: машина на паузе — «на связи, но работу не берёт». Прод старой
5649
+ // версии поля не пришлёт → undefined, строка выглядит как раньше.
5650
+ ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
5651
+ ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
5652
+ ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {},
5653
+ // Показаний нет (старый прод, машина со старым демоном) — ключа не заводим вовсе,
5654
+ // ровно как с паузой выше: «не знаем» и так значит «не знаем».
5655
+ ...resources ? { resources } : {}
5656
+ };
5657
+ });
5454
5658
  }
5455
5659
  /**
5456
5660
  * TASWM-326: убрать ЭТУ машину из парка (`swarm remove`). Прод удаляет строку
@@ -5516,13 +5720,22 @@ var SwarmClient = class _SwarmClient {
5516
5720
  * разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
5517
5721
  * трогает прошлое значение — та же логика, что у repoFullNames.
5518
5722
  *
5723
+ * TASWM-394: тем же пульсом уезжают РЕСУРСЫ машины (`resources` — процессор, память,
5724
+ * диск и время замера). Это не новое измерение, а ровно то, что демон УЖЕ посчитал для
5725
+ * своего гейта спавна: числа в интерфейсе обязаны совпадать с теми, по которым он
5726
+ * отказывается поднимать дрона. Без них «машина на связи, свободна» и «машина физически
5727
+ * не может поднять дрона» выглядят в проде одинаково — 12.08 это стоило полудня
5728
+ * простоя, видимого только в логе на самой машине. Не передали (нечем измерить) → ключ
5729
+ * не шлём, прод прошлое не трогает — та же логика, что у capacity/repoFullNames.
5730
+ *
5519
5731
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
5520
5732
  */
5521
- async heartbeat(daemonVersion, pause, capacity) {
5733
+ async heartbeat(daemonVersion, pause, capacity, resources) {
5522
5734
  try {
5523
5735
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
5524
5736
  if (daemonVersion) args.daemonVersion = daemonVersion;
5525
5737
  if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
5738
+ if (resources) args.resources = resources;
5526
5739
  if (pause !== void 0) {
5527
5740
  args.paused = !!pause?.paused;
5528
5741
  if (pause?.reason) args.pauseReason = pause.reason;
@@ -5729,6 +5942,8 @@ var SwarmClient = class _SwarmClient {
5729
5942
  area: null,
5730
5943
  depends_on: [],
5731
5944
  model: null,
5945
+ effort: null,
5946
+ // усилие рассуждения (TASWM-395) — прод-поле, заполняется ниже из ответа
5732
5947
  cost_usd: 0,
5733
5948
  tokens_in: 0,
5734
5949
  // токены и время работы (TASWM-353) — прод-поля, заполняются ниже
@@ -5797,6 +6012,7 @@ var SwarmClient = class _SwarmClient {
5797
6012
  local.area = pr.area ?? null;
5798
6013
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
5799
6014
  local.model = pr.model ?? null;
6015
+ local.effort = toEffortLevel(pr.effort);
5800
6016
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
5801
6017
  local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
5802
6018
  local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
@@ -6634,7 +6850,10 @@ function hostLines(hosts, opts = {}) {
6634
6850
  const here = selfHostId && h.hostId === selfHostId ? " \u2190 \u044D\u0442\u0430 \u043C\u0430\u0448\u0438\u043D\u0430" : "";
6635
6851
  const repos = h.reposServed?.length ? `
6636
6852
  \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438: ${h.reposServed.join(", ")}` : "";
6637
- return ` ${h.hostName} ${hostStateText(h, nowMs)} (${h.hostId})${here}${repos}`;
6853
+ const res = hostResourcesText(h.resources, nowMs);
6854
+ const load = res ? `
6855
+ ${res}` : "";
6856
+ return ` ${h.hostName} ${hostStateText(h, nowMs)} (${h.hostId})${here}${load}${repos}`;
6638
6857
  });
6639
6858
  }
6640
6859
 
@@ -7234,7 +7453,8 @@ function timelineState(db2, now = Date.now(), overmindLog = []) {
7234
7453
  heroRunId: r.hero_run_id,
7235
7454
  reviewVerdict: r.review_verdict,
7236
7455
  reviewPhase: r.review_phase,
7237
- model: r.model
7456
+ model: r.model,
7457
+ effort: r.effort
7238
7458
  });
7239
7459
  }
7240
7460
  const liveLanes = lanes.filter((l) => l.live);
@@ -8709,8 +8929,26 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
8709
8929
  };
8710
8930
  }
8711
8931
 
8932
+ // src/swarmModels.ts
8933
+ init_src();
8934
+ function pick(v) {
8935
+ const s = typeof v === "string" ? v.trim() : "";
8936
+ return s.length > 0 ? s : null;
8937
+ }
8938
+ function forKind(kind, swarm, norm) {
8939
+ if (!swarm) return null;
8940
+ return kind === "reviewer" ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
8941
+ }
8942
+ function resolveRunModel(run, swarm, env = process.env) {
8943
+ return pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL) ?? null;
8944
+ }
8945
+ function resolveRunEffort(run, swarm, env = process.env) {
8946
+ return toEffortLevel(run.effort) ?? forKind(run.kind, swarm, toEffortLevel) ?? toEffortLevel(env.SWARM_DRONE_EFFORT) ?? null;
8947
+ }
8948
+
8712
8949
  // src/launcher.ts
8713
8950
  init_executors();
8951
+ init_src();
8714
8952
  var requireFromHere = createRequire(import.meta.url);
8715
8953
  function resolveTsxCli() {
8716
8954
  try {
@@ -8722,6 +8960,15 @@ function resolveTsxCli() {
8722
8960
  }
8723
8961
  }
8724
8962
  var MCP_SERVER_NAME = "overmind";
8963
+ function launchBudgetUsd(limit, spentUsd, env = process.env) {
8964
+ return remainingRunBudgetUsd(limit ?? envCapUsd(env), spentUsd);
8965
+ }
8966
+ function envCapUsd(env) {
8967
+ const raw = env.SWARM_MAX_COST_USD;
8968
+ if (raw === void 0 || raw === "") return DEFAULT_RUN_LIMITS.maxCostUsd;
8969
+ const n = Number(raw);
8970
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_RUN_LIMITS.maxCostUsd;
8971
+ }
8725
8972
  function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
8726
8973
  const env = { ...base3 };
8727
8974
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
@@ -8846,7 +9093,13 @@ function makeLauncher(cfg) {
8846
9093
  ...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
8847
9094
  }
8848
9095
  };
8849
- const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
9096
+ const model = resolveRunModel(run, cfg.swarmModels?.() ?? null);
9097
+ const effort = resolveRunEffort(run, cfg.swarmEfforts?.() ?? null);
9098
+ try {
9099
+ cfg.recordEffort?.(run.id, effort);
9100
+ } catch (e) {
9101
+ 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)}`);
9102
+ }
8850
9103
  const realHome = cfg.droneIsolation?.realHome ?? homedir4();
8851
9104
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
8852
9105
  if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync7(join12(cfg.droneIsolation.swarmHome, "token"))) {
@@ -8865,7 +9118,14 @@ function makeLauncher(cfg) {
8865
9118
  // Закрытые дрону пути имеют смысл только при теневом HOME: без него дрон и так
8866
9119
  // сидит в HOME демона, и запирать половину дверей — самообман.
8867
9120
  denyPaths: cfg.droneIsolation ? dronePrivatePaths(realHome, cfg.droneIsolation.swarmHome) : [],
8868
- realHome
9121
+ realHome,
9122
+ // TASWM-377: остаток бюджета ЭТОГО прогона — харнессу, чтобы он резал трату у себя, в
9123
+ // момент её совершения. Прод-сторож остаётся страховкой, но он по устройству видит
9124
+ // стоимость только по записанному итогу: живой прогон стоит для него ноль, сколько бы
9125
+ // ни жёг, — отсюда и $74 при лимите $20.
9126
+ maxBudgetUsd: launchBudgetUsd(cfg.maxCostUsd?.() ?? null, run.cost_usd),
9127
+ // TASWM-395: усилие рассуждения — харнессу флагом (claude `--effort`). null = флага нет.
9128
+ effort
8869
9129
  });
8870
9130
  let stdio = "ignore";
8871
9131
  let outFd;