@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/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,36 @@ 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
+
1156
+ // ../swarm-core/src/queuePlan.ts
1157
+ var MIN_MS, QUEUE_NOMINAL_RUN_MS, QUEUE_MIN_TAIL_MS, QUEUE_REVIEW_TAIL_MS;
1158
+ var init_queuePlan = __esm({
1159
+ "../swarm-core/src/queuePlan.ts"() {
1160
+ "use strict";
1161
+ init_status();
1162
+ init_hostPool();
1163
+ init_hosts();
1164
+ init_time();
1165
+ MIN_MS = 6e4;
1166
+ QUEUE_NOMINAL_RUN_MS = 25 * MIN_MS;
1167
+ QUEUE_MIN_TAIL_MS = 3 * MIN_MS;
1168
+ QUEUE_REVIEW_TAIL_MS = 10 * MIN_MS;
1169
+ }
1170
+ });
1171
+
1090
1172
  // ../swarm-core/src/index.ts
1091
1173
  var init_src = __esm({
1092
1174
  "../swarm-core/src/index.ts"() {
@@ -1104,12 +1186,15 @@ var init_src = __esm({
1104
1186
  init_names();
1105
1187
  init_baseBranch();
1106
1188
  init_hosts();
1189
+ init_hostResources();
1107
1190
  init_hostPool();
1108
1191
  init_hostCommands();
1109
1192
  init_relief();
1110
1193
  init_cost();
1111
1194
  init_runGuards();
1112
1195
  init_codeZones();
1196
+ init_effort();
1197
+ init_queuePlan();
1113
1198
  }
1114
1199
  });
1115
1200
 
@@ -1253,14 +1338,32 @@ function launchCostUsd(result, model) {
1253
1338
  return costFromUsage(result.usage, model);
1254
1339
  }
1255
1340
  function launchSpend(result) {
1256
- const u = result.usage ?? {};
1257
1341
  const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1258
- return {
1342
+ const sum = (u) => ({
1259
1343
  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)
1344
+ tokensOut: num(u.output_tokens)
1345
+ });
1346
+ const spend = sum(result.usage ?? {});
1347
+ if (spend.tokensIn === 0 && spend.tokensOut === 0 && result.modelUsage) {
1348
+ for (const m of Object.values(result.modelUsage)) {
1349
+ const perModel = sum(asTokenUsage(m));
1350
+ spend.tokensIn += perModel.tokensIn;
1351
+ spend.tokensOut += perModel.tokensOut;
1352
+ }
1353
+ }
1354
+ return { ...spend, durationMs: num(result.duration_ms) };
1355
+ }
1356
+ function asTokenUsage(m) {
1357
+ return {
1358
+ ...m?.inputTokens !== void 0 ? { input_tokens: m.inputTokens } : {},
1359
+ ...m?.outputTokens !== void 0 ? { output_tokens: m.outputTokens } : {},
1360
+ ...m?.cacheCreationInputTokens !== void 0 ? { cache_creation_input_tokens: m.cacheCreationInputTokens } : {},
1361
+ ...m?.cacheReadInputTokens !== void 0 ? { cache_read_input_tokens: m.cacheReadInputTokens } : {}
1262
1362
  };
1263
1363
  }
1364
+ function budgetExhausted(result) {
1365
+ return result.subtype === "error_max_budget_usd" || result.terminal_reason === "budget_exhausted";
1366
+ }
1264
1367
  var init_claudeResult = __esm({
1265
1368
  "src/claudeResult.ts"() {
1266
1369
  "use strict";
@@ -1272,8 +1375,10 @@ var init_claudeResult = __esm({
1272
1375
  import { readdirSync as readdirSync2, statSync, writeFileSync } from "fs";
1273
1376
  import { homedir } from "os";
1274
1377
  import { join } from "path";
1275
- function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
1378
+ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings, maxBudgetUsd, effort) {
1276
1379
  const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
1380
+ const budgetFlags = typeof maxBudgetUsd === "number" && maxBudgetUsd > 0 ? ["--max-budget-usd", String(maxBudgetUsd)] : [];
1381
+ const effortFlags = effort ? ["--effort", effort] : [];
1277
1382
  const base3 = [
1278
1383
  "--dangerously-skip-permissions",
1279
1384
  ...ISOLATION_FLAGS,
@@ -1281,6 +1386,8 @@ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs
1281
1386
  cfgPath,
1282
1387
  ...settings ? ["--settings", settings] : [],
1283
1388
  ...pluginFlags,
1389
+ ...budgetFlags,
1390
+ ...effortFlags,
1284
1391
  "--output-format",
1285
1392
  "json",
1286
1393
  "-p",
@@ -1357,7 +1464,7 @@ var init_claudeCode = __esm({
1357
1464
  const settings = spec.denyPaths.length > 0 ? droneSettingsJson(spec.denyPaths) : void 0;
1358
1465
  return {
1359
1466
  bin: resolveClaudeBin(),
1360
- args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings)
1467
+ args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings, spec.maxBudgetUsd, spec.effort)
1361
1468
  };
1362
1469
  },
1363
1470
  parseResult(raw, model) {
@@ -1370,7 +1477,10 @@ var init_claudeCode = __esm({
1370
1477
  ...est.warning ? { warning: est.warning } : {},
1371
1478
  tokensIn: spend.tokensIn,
1372
1479
  tokensOut: spend.tokensOut,
1373
- durationMs: spend.durationMs
1480
+ durationMs: spend.durationMs,
1481
+ // TASWM-377: «упёрся в потолок» — отдельный факт об этом же итоге, а не отдельный
1482
+ // разбор файла: у claude он лежит в тех же полях, что деньги и токены.
1483
+ ...budgetExhausted(result) ? { stoppedByBudget: true } : {}
1374
1484
  };
1375
1485
  },
1376
1486
  session: {
@@ -1530,6 +1640,13 @@ ${notice}` : spec.prompt;
1530
1640
  SKIP_GIT_CHECK_FLAG,
1531
1641
  BYPASS_FLAG,
1532
1642
  ...spec.model ? ["-m", spec.model] : [],
1643
+ // TASWM-395: УСИЛИЕ РАССУЖДЕНИЯ (spec.effort) codex пока НЕ получает — и это сказано
1644
+ // вслух, как требует контракт адаптера (см. ExecutorLaunchSpec.effort), а не забыто.
1645
+ // Кандидат есть — `-c model_reasoning_effort=<low|medium|high>` — но словари не совпадают
1646
+ // (у нас пять уровней, xhigh/max пришлось бы схлопывать в high), а главное: непонятый
1647
+ // ключ в `-c` роняет НЕ один флаг, а весь спавн. Проверять это надо на живом codex, и
1648
+ // до такой проверки правильный размен — работающий codex на дефолтном усилии, а не
1649
+ // сломанный на настроенном. Провода до сюда доведены: останется дописать одну строку.
1533
1650
  ...mcpConfigArgs(spec),
1534
1651
  prompt
1535
1652
  ];
@@ -1635,7 +1752,7 @@ function tokenFromFile() {
1635
1752
  return "";
1636
1753
  }
1637
1754
  }
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;
1755
+ 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
1756
  var init_config = __esm({
1640
1757
  "src/config.ts"() {
1641
1758
  "use strict";
@@ -1665,6 +1782,10 @@ var init_config = __esm({
1665
1782
  SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
1666
1783
  SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
1667
1784
  SWARM_MAX_CPU_BUSY = envNum("SWARM_MAX_CPU_BUSY", 0.85);
1785
+ SWARM_CPU_WINDOW_MS = Math.max(1e3, envNum("SWARM_CPU_WINDOW_MS", 3e3));
1786
+ SWARM_CPU_SANITY_BUSY = envNum("SWARM_CPU_SANITY_BUSY", 0.9);
1787
+ SWARM_CPU_SANITY_LOAD_FACTOR = envNum("SWARM_CPU_SANITY_LOAD_FACTOR", 0.5);
1788
+ SWARM_CPU_SANITY_LAG_MS = envNum("SWARM_CPU_SANITY_LAG_MS", 6e4);
1668
1789
  SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
1669
1790
  SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
1670
1791
  SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
@@ -1884,7 +2005,7 @@ var init_package = __esm({
1884
2005
  "package.json"() {
1885
2006
  package_default = {
1886
2007
  name: "@taskless-app/swarm",
1887
- version: "0.6.5",
2008
+ version: "0.6.7",
1888
2009
  type: "module",
1889
2010
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
1890
2011
  keywords: [
@@ -1934,7 +2055,7 @@ var init_package = __esm({
1934
2055
  execa: "^10.0.0",
1935
2056
  zod: "3.25.76"
1936
2057
  },
1937
- gitHead: "76ff5edd816a2b9dd7f10db62c8fde89eec7771f"
2058
+ gitHead: "08ee2a1a4bdb53ba1c25c92066104f0de3a23433"
1938
2059
  };
1939
2060
  }
1940
2061
  });
@@ -2301,6 +2422,10 @@ function rowToRun(row) {
2301
2422
  area: row.area ?? null,
2302
2423
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
2303
2424
  model: row.model ?? null,
2425
+ // TASWM-395: уровень усилия — через toEffortLevel, а не сырой строкой: в зеркале может
2426
+ // лежать значение, записанное демоном другой версии (словарь уровней задаёт CLI), и
2427
+ // непонятное здесь означает «уровень не задан», а не сломанный тип.
2428
+ effort: toEffortLevel(row.effort),
2304
2429
  cost_usd: row.cost_usd ?? 0,
2305
2430
  // Токены/время работы (TASWM-353) — старые прогоны без колонок → 0.
2306
2431
  tokens_in: row.tokens_in ?? 0,
@@ -2370,6 +2495,7 @@ CREATE TABLE IF NOT EXISTS runs (
2370
2495
  area TEXT,
2371
2496
  depends_on TEXT,
2372
2497
  model TEXT,
2498
+ effort TEXT,
2373
2499
  cost_usd REAL NOT NULL DEFAULT 0,
2374
2500
  tokens_in INTEGER NOT NULL DEFAULT 0,
2375
2501
  tokens_out INTEGER NOT NULL DEFAULT 0,
@@ -2471,7 +2597,10 @@ CREATE TABLE IF NOT EXISTS imports (
2471
2597
  // Чем обошёлся прогон, кроме денег (TASWM-353): токены вход/выход и время работы.
2472
2598
  "ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
2473
2599
  "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"
2600
+ "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0",
2601
+ // TASWM-395: усилие рассуждения прогона (`claude --effort`). Nullable без DEFAULT —
2602
+ // NULL это и все прогоны до фичи, и прогоны на дефолте харнесса.
2603
+ "ALTER TABLE runs ADD COLUMN effort TEXT"
2475
2604
  ]) {
2476
2605
  try {
2477
2606
  this.db.exec(alter);
@@ -2491,7 +2620,7 @@ CREATE TABLE IF NOT EXISTS imports (
2491
2620
  createRun(input) {
2492
2621
  const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
2493
2622
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
2623
+ "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
2624
  ).run(
2496
2625
  input.task_slug,
2497
2626
  name,
@@ -2504,6 +2633,8 @@ CREATE TABLE IF NOT EXISTS imports (
2504
2633
  input.area ?? null,
2505
2634
  input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
2506
2635
  input.model ?? null,
2636
+ input.effort ?? null,
2637
+ // усилие рассуждения (TASWM-395); null → разрешит спавн
2507
2638
  input.decision ? JSON.stringify(input.decision) : null,
2508
2639
  input.resolves_run_id ?? null,
2509
2640
  input.reviews_run_id ?? null,
@@ -4105,25 +4236,66 @@ function cpuBusyFromTicks(prev, cur) {
4105
4236
  if (dTotal <= 0) return null;
4106
4237
  return Math.min(1, Math.max(0, 1 - dIdle / dTotal));
4107
4238
  }
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;
4239
+ function cpuBusyFromSamples(prev, cur, minWindowMs = SWARM_CPU_WINDOW_MS) {
4240
+ if (!prev) return null;
4241
+ if (cur.at - prev.at < minWindowMs) return null;
4242
+ return cpuBusyFromTicks(prev, cur);
4243
+ }
4244
+ function cpuBusySane(busy, load1, cpus, highForMs, sanityBusy = SWARM_CPU_SANITY_BUSY, sanityLoadFactor = SWARM_CPU_SANITY_LOAD_FACTOR, lagMs = SWARM_CPU_SANITY_LAG_MS) {
4245
+ if (busy < sanityBusy) return true;
4246
+ if (highForMs < lagMs) return true;
4247
+ const cores = cpus > 0 ? cpus : 1;
4248
+ if (!Number.isFinite(load1) || load1 < 0) return true;
4249
+ return load1 >= cores * sanityLoadFactor;
4250
+ }
4251
+ var cpuWindowBase = null;
4252
+ var cpuBusyValue = null;
4253
+ var cpuHighSince = null;
4254
+ var cpuSanityWarnAt = 0;
4255
+ var CPU_SANITY_WARN_EVERY_MS = 5 * 6e4;
4256
+ function sampleCpuBusy(now = Date.now(), ticks = readCpuTicks(os.cpus())) {
4257
+ const cur = { at: now, ...ticks };
4258
+ if (!cpuWindowBase) {
4259
+ cpuWindowBase = cur;
4260
+ return cpuBusyValue;
4261
+ }
4262
+ if (cur.at - cpuWindowBase.at < SWARM_CPU_WINDOW_MS) return cpuBusyValue;
4263
+ cpuBusyValue = cpuBusyFromSamples(cpuWindowBase, cur);
4264
+ cpuWindowBase = cur;
4265
+ return cpuBusyValue;
4266
+ }
4267
+ function cpuBusyReading() {
4268
+ return cpuBusyValue;
4269
+ }
4270
+ function trustedCpuBusy(busy, load1, cpus, now) {
4271
+ if (busy == null || busy < SWARM_CPU_SANITY_BUSY) {
4272
+ cpuHighSince = null;
4273
+ return busy;
4274
+ }
4275
+ cpuHighSince ??= now;
4276
+ if (cpuBusySane(busy, load1, cpus, now - cpuHighSince)) return busy;
4277
+ if (now - cpuSanityWarnAt >= CPU_SANITY_WARN_EVERY_MS) {
4278
+ cpuSanityWarnAt = now;
4279
+ console.warn(
4280
+ `[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.`
4281
+ );
4282
+ }
4283
+ return null;
4114
4284
  }
4115
4285
  var RESOURCES_CACHE_MS = 250;
4116
4286
  var readingCache = null;
4117
4287
  function hostReading() {
4118
4288
  const now = Date.now();
4119
4289
  if (readingCache && now - readingCache.at < RESOURCES_CACHE_MS) return readingCache.reading;
4290
+ const load1 = os.loadavg()[0] ?? 0;
4291
+ const cpus = os.cpus().length;
4292
+ sampleCpuBusy(now);
4120
4293
  const reading = {
4121
- load1: os.loadavg()[0] ?? 0,
4122
- cpus: os.cpus().length,
4294
+ load1,
4295
+ cpus,
4123
4296
  freeGb: availableMemBytes() / 1e9,
4124
4297
  fdLoadRatio: fileDescriptorLoadRatio(),
4125
- cpuBusy: cpuBusyRatio(),
4126
- // TASWM-234: раз в тик (кэш выше) — иначе окно дельты схлопнется
4298
+ cpuBusy: trustedCpuBusy(cpuBusyReading(), load1, cpus, now),
4127
4299
  totalGb: os.totalmem() / 1e9,
4128
4300
  disk: diskGb()
4129
4301
  };
@@ -4224,6 +4396,8 @@ var SwarmClient = class _SwarmClient {
4224
4396
  // предикат «репо есть локально» для решения, брать ли свободный прогон.
4225
4397
  host;
4226
4398
  repoExists;
4399
+ // TASWM-398: ЧЕМ эта машина исполняет прогоны — id адаптера (claude-code|codex).
4400
+ executorId;
4227
4401
  // Зеркало.
4228
4402
  runs = /* @__PURE__ */ new Map();
4229
4403
  runtime = /* @__PURE__ */ new Map();
@@ -4301,6 +4475,7 @@ var SwarmClient = class _SwarmClient {
4301
4475
  this.host = opts.host ?? null;
4302
4476
  this.repoExists = opts.repoExists ?? existsSync4;
4303
4477
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
4478
+ this.executorId = opts.executorId ?? EXECUTOR.id;
4304
4479
  this.outboxPath = opts.outboxPath;
4305
4480
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
4306
4481
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
@@ -4414,8 +4589,10 @@ var SwarmClient = class _SwarmClient {
4414
4589
  runFields(patch) {
4415
4590
  const out = {};
4416
4591
  if ("status" in patch && patch.status !== void 0) out.status = patch.status;
4592
+ if (patch.status === "running") out.executor = this.executorId;
4417
4593
  if ("area" in patch) out.area = patch.area ?? null;
4418
4594
  if ("model" in patch) out.model = patch.model ?? null;
4595
+ if ("effort" in patch) out.effort = patch.effort ?? null;
4419
4596
  if ("name" in patch) out.droneName = patch.name ?? null;
4420
4597
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
4421
4598
  if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
@@ -4486,6 +4663,9 @@ var SwarmClient = class _SwarmClient {
4486
4663
  area: input.area ?? null,
4487
4664
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
4488
4665
  model: input.model ?? null,
4666
+ // Усилие рассуждения (TASWM-395): пин, если его поставили на спавне; иначе null —
4667
+ // разрешённый уровень впечатает launcher, когда действительно поднимет дрона.
4668
+ effort: input.effort ?? null,
4489
4669
  cost_usd: 0,
4490
4670
  tokens_in: 0,
4491
4671
  // копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
@@ -4538,6 +4718,8 @@ var SwarmClient = class _SwarmClient {
4538
4718
  status: "queued",
4539
4719
  area: d.area,
4540
4720
  model: d.model,
4721
+ effort: d.effort,
4722
+ // усилие рассуждения (TASWM-395), если пинили на спавне
4541
4723
  name: d.name,
4542
4724
  repo: d.repo,
4543
4725
  depends_on: d.depends_on,
@@ -5315,11 +5497,24 @@ var SwarmClient = class _SwarmClient {
5315
5497
  const n = obj?.maxDrones;
5316
5498
  if (!(typeof n === "number" && Number.isFinite(n) && n > 0)) return null;
5317
5499
  const str = (v) => typeof v === "string" && v.trim() ? v.trim() : null;
5500
+ const cost = obj?.swarmSettings?.maxCostUsd;
5318
5501
  return {
5319
5502
  maxDrones: Math.trunc(n),
5320
5503
  // Старый прод (до TASWM-364) поля swarmSettings не отдаёт вовсе — обе модели null,
5321
5504
  // то есть прежнее поведение: env машины и дефолт харнесса.
5322
- models: { drone: str(obj?.swarmSettings?.droneModel), reviewer: str(obj?.swarmSettings?.reviewerModel) }
5505
+ models: { drone: str(obj?.swarmSettings?.droneModel), reviewer: str(obj?.swarmSettings?.reviewerModel) },
5506
+ // TASWM-395: уровни усилия — из того же ответа и с той же семантикой, что модели:
5507
+ // старый прод поля не отдаёт → оба null, то есть env машины и дефолт харнесса.
5508
+ // toEffortLevel заодно отсекает мусор, записанный в uiPrefs мимо схемы.
5509
+ efforts: {
5510
+ drone: toEffortLevel(obj?.swarmSettings?.droneEffort),
5511
+ reviewer: toEffortLevel(obj?.swarmSettings?.reviewerEffort)
5512
+ },
5513
+ // TASWM-377: потолок трат — из того же ответа, тем же одним чтением. НОЛЬ здесь
5514
+ // значим («гарантия выключена осознанно»), поэтому пропускаем и его, а не только
5515
+ // положительные: null остаётся ровно за «ручка не задана / старый прод», и только
5516
+ // он уводит спавн на env-фолбэк и дефолт ядра.
5517
+ maxCostUsd: typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : null
5323
5518
  };
5324
5519
  }
5325
5520
  /**
@@ -5458,19 +5653,25 @@ var SwarmClient = class _SwarmClient {
5458
5653
  if (res?.isError) return [];
5459
5654
  const raw = (res.structuredContent ?? {}).hosts;
5460
5655
  if (!Array.isArray(raw)) return [];
5461
- return raw.filter((h) => typeof h === "object" && h !== null).filter((h) => typeof h.hostId === "string" && h.hostId).map((h) => ({
5462
- hostId: h.hostId,
5463
- hostName: typeof h.hostName === "string" && h.hostName ? h.hostName : h.hostId,
5464
- lastSeenAt: typeof h.lastSeenAt === "string" ? h.lastSeenAt : null,
5465
- // TASWM-298: репозитории машины — «на связи» ещё не значит «возьмёт эту задачу».
5466
- // Прод старой версии поля не пришлёт → пустой список, строка просто без хвоста.
5467
- reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : [],
5468
- // TASWM-326: машина на паузе — «на связи, но работу не берёт». Прод старой
5469
- // версии поля не пришлёт → undefined, строка выглядит как раньше.
5470
- ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
5471
- ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
5472
- ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {}
5473
- }));
5656
+ return raw.filter((h) => typeof h === "object" && h !== null).filter((h) => typeof h.hostId === "string" && h.hostId).map((h) => {
5657
+ const resources = parseHostResources(h.resources);
5658
+ return {
5659
+ hostId: h.hostId,
5660
+ hostName: typeof h.hostName === "string" && h.hostName ? h.hostName : h.hostId,
5661
+ lastSeenAt: typeof h.lastSeenAt === "string" ? h.lastSeenAt : null,
5662
+ // TASWM-298: репозитории машины — «на связи» ещё не значит «возьмёт эту задачу».
5663
+ // Прод старой версии поля не пришлёт → пустой список, строка просто без хвоста.
5664
+ reposServed: Array.isArray(h.reposServed) ? h.reposServed.filter((r) => typeof r === "string") : [],
5665
+ // TASWM-326: машина на паузе — «на связи, но работу не берёт». Прод старой
5666
+ // версии поля не пришлёт → undefined, строка выглядит как раньше.
5667
+ ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
5668
+ ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
5669
+ ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {},
5670
+ // Показаний нет (старый прод, машина со старым демоном) — ключа не заводим вовсе,
5671
+ // ровно как с паузой выше: «не знаем» и так значит «не знаем».
5672
+ ...resources ? { resources } : {}
5673
+ };
5674
+ });
5474
5675
  }
5475
5676
  /**
5476
5677
  * TASWM-326: убрать ЭТУ машину из парка (`swarm remove`). Прод удаляет строку
@@ -5536,13 +5737,22 @@ var SwarmClient = class _SwarmClient {
5536
5737
  * разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
5537
5738
  * трогает прошлое значение — та же логика, что у repoFullNames.
5538
5739
  *
5740
+ * TASWM-394: тем же пульсом уезжают РЕСУРСЫ машины (`resources` — процессор, память,
5741
+ * диск и время замера). Это не новое измерение, а ровно то, что демон УЖЕ посчитал для
5742
+ * своего гейта спавна: числа в интерфейсе обязаны совпадать с теми, по которым он
5743
+ * отказывается поднимать дрона. Без них «машина на связи, свободна» и «машина физически
5744
+ * не может поднять дрона» выглядят в проде одинаково — 12.08 это стоило полудня
5745
+ * простоя, видимого только в логе на самой машине. Не передали (нечем измерить) → ключ
5746
+ * не шлём, прод прошлое не трогает — та же логика, что у capacity/repoFullNames.
5747
+ *
5539
5748
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
5540
5749
  */
5541
- async heartbeat(daemonVersion, pause, capacity) {
5750
+ async heartbeat(daemonVersion, pause, capacity, resources) {
5542
5751
  try {
5543
5752
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
5544
5753
  if (daemonVersion) args.daemonVersion = daemonVersion;
5545
5754
  if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
5755
+ if (resources) args.resources = resources;
5546
5756
  if (pause !== void 0) {
5547
5757
  args.paused = !!pause?.paused;
5548
5758
  if (pause?.reason) args.pauseReason = pause.reason;
@@ -5749,6 +5959,8 @@ var SwarmClient = class _SwarmClient {
5749
5959
  area: null,
5750
5960
  depends_on: [],
5751
5961
  model: null,
5962
+ effort: null,
5963
+ // усилие рассуждения (TASWM-395) — прод-поле, заполняется ниже из ответа
5752
5964
  cost_usd: 0,
5753
5965
  tokens_in: 0,
5754
5966
  // токены и время работы (TASWM-353) — прод-поля, заполняются ниже
@@ -5817,6 +6029,7 @@ var SwarmClient = class _SwarmClient {
5817
6029
  local.area = pr.area ?? null;
5818
6030
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
5819
6031
  local.model = pr.model ?? null;
6032
+ local.effort = toEffortLevel(pr.effort);
5820
6033
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
5821
6034
  local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
5822
6035
  local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
@@ -6654,7 +6867,10 @@ function hostLines(hosts, opts = {}) {
6654
6867
  const here = selfHostId && h.hostId === selfHostId ? " \u2190 \u044D\u0442\u0430 \u043C\u0430\u0448\u0438\u043D\u0430" : "";
6655
6868
  const repos = h.reposServed?.length ? `
6656
6869
  \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438: ${h.reposServed.join(", ")}` : "";
6657
- return ` ${h.hostName} ${hostStateText(h, nowMs)} (${h.hostId})${here}${repos}`;
6870
+ const res = hostResourcesText(h.resources, nowMs);
6871
+ const load = res ? `
6872
+ ${res}` : "";
6873
+ return ` ${h.hostName} ${hostStateText(h, nowMs)} (${h.hostId})${here}${load}${repos}`;
6658
6874
  });
6659
6875
  }
6660
6876
 
@@ -7254,7 +7470,8 @@ function timelineState(db2, now = Date.now(), overmindLog = []) {
7254
7470
  heroRunId: r.hero_run_id,
7255
7471
  reviewVerdict: r.review_verdict,
7256
7472
  reviewPhase: r.review_phase,
7257
- model: r.model
7473
+ model: r.model,
7474
+ effort: r.effort
7258
7475
  });
7259
7476
  }
7260
7477
  const liveLanes = lanes.filter((l) => l.live);
@@ -8730,17 +8947,25 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
8730
8947
  }
8731
8948
 
8732
8949
  // src/swarmModels.ts
8950
+ init_src();
8951
+ function pick(v) {
8952
+ const s = typeof v === "string" ? v.trim() : "";
8953
+ return s.length > 0 ? s : null;
8954
+ }
8955
+ function forKind(kind, swarm, norm) {
8956
+ if (!swarm) return null;
8957
+ return kind === "reviewer" ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
8958
+ }
8733
8959
  function resolveRunModel(run, swarm, env = process.env) {
8734
- const pick = (v) => {
8735
- const s = typeof v === "string" ? v.trim() : "";
8736
- return s.length > 0 ? s : null;
8737
- };
8738
- const fromSwarm = run.kind === "reviewer" ? pick(swarm?.reviewer) ?? pick(swarm?.drone) : pick(swarm?.drone);
8739
- return pick(run.model) ?? fromSwarm ?? pick(env.SWARM_DRONE_MODEL) ?? null;
8960
+ return pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL) ?? null;
8961
+ }
8962
+ function resolveRunEffort(run, swarm, env = process.env) {
8963
+ return toEffortLevel(run.effort) ?? forKind(run.kind, swarm, toEffortLevel) ?? toEffortLevel(env.SWARM_DRONE_EFFORT) ?? null;
8740
8964
  }
8741
8965
 
8742
8966
  // src/launcher.ts
8743
8967
  init_executors();
8968
+ init_src();
8744
8969
  var requireFromHere = createRequire(import.meta.url);
8745
8970
  function resolveTsxCli() {
8746
8971
  try {
@@ -8752,6 +8977,15 @@ function resolveTsxCli() {
8752
8977
  }
8753
8978
  }
8754
8979
  var MCP_SERVER_NAME = "overmind";
8980
+ function launchBudgetUsd(limit, spentUsd, env = process.env) {
8981
+ return remainingRunBudgetUsd(limit ?? envCapUsd(env), spentUsd);
8982
+ }
8983
+ function envCapUsd(env) {
8984
+ const raw = env.SWARM_MAX_COST_USD;
8985
+ if (raw === void 0 || raw === "") return DEFAULT_RUN_LIMITS.maxCostUsd;
8986
+ const n = Number(raw);
8987
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_RUN_LIMITS.maxCostUsd;
8988
+ }
8755
8989
  function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
8756
8990
  const env = { ...base3 };
8757
8991
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
@@ -8877,6 +9111,12 @@ function makeLauncher(cfg) {
8877
9111
  }
8878
9112
  };
8879
9113
  const model = resolveRunModel(run, cfg.swarmModels?.() ?? null);
9114
+ const effort = resolveRunEffort(run, cfg.swarmEfforts?.() ?? null);
9115
+ try {
9116
+ cfg.recordEffort?.(run.id, effort);
9117
+ } catch (e) {
9118
+ 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)}`);
9119
+ }
8880
9120
  const realHome = cfg.droneIsolation?.realHome ?? homedir4();
8881
9121
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
8882
9122
  if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync7(join12(cfg.droneIsolation.swarmHome, "token"))) {
@@ -8895,7 +9135,14 @@ function makeLauncher(cfg) {
8895
9135
  // Закрытые дрону пути имеют смысл только при теневом HOME: без него дрон и так
8896
9136
  // сидит в HOME демона, и запирать половину дверей — самообман.
8897
9137
  denyPaths: cfg.droneIsolation ? dronePrivatePaths(realHome, cfg.droneIsolation.swarmHome) : [],
8898
- realHome
9138
+ realHome,
9139
+ // TASWM-377: остаток бюджета ЭТОГО прогона — харнессу, чтобы он резал трату у себя, в
9140
+ // момент её совершения. Прод-сторож остаётся страховкой, но он по устройству видит
9141
+ // стоимость только по записанному итогу: живой прогон стоит для него ноль, сколько бы
9142
+ // ни жёг, — отсюда и $74 при лимите $20.
9143
+ maxBudgetUsd: launchBudgetUsd(cfg.maxCostUsd?.() ?? null, run.cost_usd),
9144
+ // TASWM-395: усилие рассуждения — харнессу флагом (claude `--effort`). null = флага нет.
9145
+ effort
8899
9146
  });
8900
9147
  let stdio = "ignore";
8901
9148
  let outFd;