@taskless-app/swarm 0.6.13 → 0.6.15

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.
package/dist/cli.js CHANGED
@@ -332,9 +332,22 @@ var init_timeline = __esm({
332
332
  });
333
333
 
334
334
  // ../swarm-core/src/reviewPhase.ts
335
+ var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
335
336
  var init_reviewPhase = __esm({
336
337
  "../swarm-core/src/reviewPhase.ts"() {
337
338
  "use strict";
339
+ REVIEW_PHASE_TEXT = {
340
+ checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
341
+ awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
342
+ ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
343
+ in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
344
+ changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
345
+ ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
346
+ reviewer_dead: "\u0420\u0435\u0432\u044C\u044E\u0435\u0440 \u0443\u043C\u0435\u0440 \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A"
347
+ };
348
+ PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
349
+ (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
350
+ );
338
351
  }
339
352
  });
340
353
 
@@ -452,6 +465,40 @@ var init_mergeGate = __esm({
452
465
  }
453
466
  });
454
467
 
468
+ // ../swarm-core/src/modelPolicy.ts
469
+ function findProvider(id) {
470
+ const key = typeof id === "string" ? id.trim().toLowerCase() : "";
471
+ return MODEL_PROVIDERS.find((p) => p.id === key) ?? null;
472
+ }
473
+ function parseModelRef(ref) {
474
+ const s = typeof ref === "string" ? ref.trim() : "";
475
+ if (!s) return null;
476
+ const slash = s.indexOf("/");
477
+ if (slash < 0) return { providerId: DEFAULT_PROVIDER_ID, model: s };
478
+ const providerId = s.slice(0, slash).trim().toLowerCase();
479
+ const model = s.slice(slash + 1).trim();
480
+ if (!model) return null;
481
+ return { providerId: providerId || DEFAULT_PROVIDER_ID, model };
482
+ }
483
+ function defaultModelFor(role, providerId = null) {
484
+ const provider = findProvider(providerId) ?? findProvider(DEFAULT_PROVIDER_ID);
485
+ return provider.roles[role];
486
+ }
487
+ var MODEL_PROVIDERS, DEFAULT_PROVIDER_ID;
488
+ var init_modelPolicy = __esm({
489
+ "../swarm-core/src/modelPolicy.ts"() {
490
+ "use strict";
491
+ MODEL_PROVIDERS = [
492
+ {
493
+ id: "anthropic",
494
+ label: "Anthropic",
495
+ roles: { executor: "claude-opus-5", review: "claude-sonnet-5" }
496
+ }
497
+ ];
498
+ DEFAULT_PROVIDER_ID = MODEL_PROVIDERS[0].id;
499
+ }
500
+ });
501
+
455
502
  // ../swarm-core/src/status.ts
456
503
  function statusMeta(status) {
457
504
  return STATUS_META[status] ?? FALLBACK;
@@ -500,7 +547,7 @@ var init_status = __esm({
500
547
  // ../swarm-core/src/deps.ts
501
548
  function judgeDep(runs) {
502
549
  const real = runs.filter((r) => !r.service);
503
- if (real.length === 0) return { ...PENDING };
550
+ if (real.length === 0) return { ...UNKNOWN };
504
551
  if (real.some((r) => !isTerminalStatus(r.status))) return { ...PENDING };
505
552
  if (real.some((r) => r.status === "done")) {
506
553
  return { outcome: "done", met: true, status: "done", name: null, stopReason: null };
@@ -514,12 +561,13 @@ function judgeDep(runs) {
514
561
  stopReason: last.stopReason ?? null
515
562
  };
516
563
  }
517
- var PENDING;
564
+ var PENDING, UNKNOWN;
518
565
  var init_deps = __esm({
519
566
  "../swarm-core/src/deps.ts"() {
520
567
  "use strict";
521
568
  init_status();
522
569
  PENDING = { outcome: "pending", met: false, status: null, name: null, stopReason: null };
570
+ UNKNOWN = { outcome: "unknown", met: false, status: null, name: null, stopReason: null };
523
571
  }
524
572
  });
525
573
 
@@ -687,6 +735,13 @@ var init_baseBranch = __esm({
687
735
  });
688
736
 
689
737
  // ../swarm-core/src/hosts.ts
738
+ function hostSignInHint(webUrl) {
739
+ const url = (webUrl ?? "").trim();
740
+ return url ? `\u0420\u043E\u0439 \u043D\u0435 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u043D. \u041E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 ${url} \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u0440\u043E\u044F \u0438 \u0432\u043E\u0439\u0434\u0438\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 Taskless` : "\u0420\u043E\u0439 \u043D\u0435 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u043D. \u041E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0443 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u0440\u043E\u044F \u0438 \u0432\u043E\u0439\u0434\u0438\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 Taskless";
741
+ }
742
+ function hostNotSignedIn(host) {
743
+ return host.signedIn === false;
744
+ }
690
745
  function normalizeHostRef(value) {
691
746
  if (value == null) return null;
692
747
  const trimmed = value.trim();
@@ -720,6 +775,10 @@ function groupByHost(items, ref, opts = {}) {
720
775
  offline: false,
721
776
  paused: unknown ? false : info?.paused === true,
722
777
  pauseReason: unknown ? null : info?.pauseReason ?? null,
778
+ // Поле ставим, ТОЛЬКО если машина о входе сказала: undefined здесь — «не знаем»,
779
+ // и превращать молчание в отказ значило бы объявить невойденным весь парк машин
780
+ // со старым демоном.
781
+ ...unknown || info?.signedIn == null ? {} : { signedIn: info.signedIn },
723
782
  runs: []
724
783
  };
725
784
  byKey.set(key, g);
@@ -752,6 +811,7 @@ function hostStateText(host, nowMs = Date.now(), staleMs2 = HOST_HEARTBEAT_STALE
752
811
  const at = Date.parse(host.lastSeenAt);
753
812
  if (Number.isNaN(at)) return "\u043F\u0443\u043B\u044C\u0441\u0430 \u043D\u0435 \u0431\u044B\u043B\u043E";
754
813
  if (!isHostOffline(host.lastSeenAt, nowMs, staleMs2)) {
814
+ if (hostNotSignedIn(host)) return hostSignInHint(host.webUrl);
755
815
  if (host.paused) return host.pauseReason ? `\u043D\u0430 \u043F\u0430\u0443\u0437\u0435 \u2014 ${host.pauseReason}` : "\u043D\u0430 \u043F\u0430\u0443\u0437\u0435";
756
816
  if (host.storm) return host.stormReason ? `\u0448\u0442\u043E\u0440\u043C\u0438\u0442 \u2014 ${host.stormReason}` : "\u0448\u0442\u043E\u0440\u043C\u0438\u0442 \u2014 \u043D\u043E\u0432\u044B\u0435 \u0441\u0435\u0441\u0441\u0438\u0438 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442";
757
817
  return "\u043D\u0430 \u0441\u0432\u044F\u0437\u0438";
@@ -873,6 +933,42 @@ var init_cost = __esm({
873
933
  }
874
934
  });
875
935
 
936
+ // ../swarm-core/src/modelPricing.ts
937
+ function findModelPricing(model, table = MODEL_PRICING) {
938
+ if (!model) return void 0;
939
+ const exact = table[model];
940
+ if (exact) return exact;
941
+ let bestKey = "";
942
+ for (const key of Object.keys(table)) {
943
+ if (model.startsWith(key) && key.length > bestKey.length) bestKey = key;
944
+ }
945
+ return bestKey ? table[bestKey] : void 0;
946
+ }
947
+ var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
948
+ var init_modelPricing = __esm({
949
+ "../swarm-core/src/modelPricing.ts"() {
950
+ "use strict";
951
+ MODEL_PRICING = {
952
+ // Sonnet 5 — 2/10. Цена ПОСТОЯННАЯ: 18.08.2026 владелец подтвердил, что вводной
953
+ // она быть перестала. Даты годности у тарифа тут больше нет и заводить её заново не
954
+ // нужно: тариф со сроком означает, что в календарный день расчёт молча меняет цифру,
955
+ // и «почему прогон вчера стоил иначе» приходится выяснять по коду, а не по таблице.
956
+ // Меняется цена — меняется строка, одним видимым коммитом.
957
+ "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
958
+ "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
959
+ "claude-fable-5": { inputPerMTok: 10, outputPerMTok: 50 },
960
+ // Прогоны и вызовы на 4.x остаются в истории — строки держим ради их стоимости.
961
+ "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
962
+ "claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25 },
963
+ "claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25 },
964
+ "claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
965
+ "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
966
+ };
967
+ CACHE_READ_FACTOR = 0.1;
968
+ CACHE_WRITE_5M_FACTOR = 1.25;
969
+ }
970
+ });
971
+
876
972
  // ../swarm-core/src/runGuards.ts
877
973
  function remainingRunBudgetUsd(maxCostUsd, spentUsd) {
878
974
  if (!(maxCostUsd > 0)) return null;
@@ -1316,6 +1412,7 @@ var init_src = __esm({
1316
1412
  init_reviewPhase();
1317
1413
  init_mergeGate();
1318
1414
  init_verifyVerdict();
1415
+ init_modelPolicy();
1319
1416
  init_status();
1320
1417
  init_deps();
1321
1418
  init_time();
@@ -1331,6 +1428,7 @@ var init_src = __esm({
1331
1428
  init_hostCommands();
1332
1429
  init_relief();
1333
1430
  init_cost();
1431
+ init_modelPricing();
1334
1432
  init_runGuards();
1335
1433
  init_codeZones();
1336
1434
  init_effort();
@@ -1428,18 +1526,12 @@ var init_swarmFiles = __esm({
1428
1526
  });
1429
1527
 
1430
1528
  // src/pricing.ts
1431
- function pricingAt(pricing, at) {
1432
- const intro = pricing.intro;
1433
- if (!intro) return pricing;
1434
- const day = at.toISOString().slice(0, 10);
1435
- return day <= intro.until ? { inputPerMTok: intro.inputPerMTok, outputPerMTok: intro.outputPerMTok } : pricing;
1436
- }
1437
- function costFromUsage(usage2, model, table = MODEL_PRICING, at = /* @__PURE__ */ new Date()) {
1438
- const entry = model ? table[model] : void 0;
1529
+ function costFromUsage(usage2, model, table = MODEL_PRICING) {
1530
+ const entry = findModelPricing(model, table);
1439
1531
  if (!entry) {
1440
1532
  return { costUsd: 0, warning: `\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u0430\u0439\u0441\u0438\u043D\u0433\u0430: "${model ?? ""}" \u2014 cost \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D` };
1441
1533
  }
1442
- const pricing = pricingAt(entry, at);
1534
+ const pricing = entry;
1443
1535
  const input = usage2.input_tokens ?? 0;
1444
1536
  const output = usage2.output_tokens ?? 0;
1445
1537
  const cacheWrite = usage2.cache_creation_input_tokens ?? 0;
@@ -1447,24 +1539,10 @@ function costFromUsage(usage2, model, table = MODEL_PRICING, at = /* @__PURE__ *
1447
1539
  const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
1448
1540
  return { costUsd };
1449
1541
  }
1450
- var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
1451
1542
  var init_pricing = __esm({
1452
1543
  "src/pricing.ts"() {
1453
1544
  "use strict";
1454
- MODEL_PRICING = {
1455
- // Sonnet 5: вводные 2/10 по 31.08.2026 включительно, дальше базовые 3/15.
1456
- "claude-sonnet-5": {
1457
- inputPerMTok: 3,
1458
- outputPerMTok: 15,
1459
- intro: { inputPerMTok: 2, outputPerMTok: 10, until: "2026-08-31" }
1460
- },
1461
- "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
1462
- // Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
1463
- "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
1464
- "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
1465
- };
1466
- CACHE_READ_FACTOR = 0.1;
1467
- CACHE_WRITE_5M_FACTOR = 1.25;
1545
+ init_src();
1468
1546
  }
1469
1547
  });
1470
1548
 
@@ -1875,7 +1953,10 @@ function normalizeRepo(repo) {
1875
1953
  }
1876
1954
  }
1877
1955
  function droneWorktreePath(worktreesDir, repo, slug) {
1878
- return join4(worktreesDir, basename(normalizeRepo(repo)), slug);
1956
+ return join4(droneWorktreeRoot(worktreesDir, repo), slug);
1957
+ }
1958
+ function droneWorktreeRoot(worktreesDir, repo) {
1959
+ return join4(worktreesDir, basename(normalizeRepo(repo)));
1879
1960
  }
1880
1961
  function resolveEffectiveWorktreesDir(deps) {
1881
1962
  if (deps.envOverride) return deps.envOverride;
@@ -1901,7 +1982,7 @@ function tokenFromFile() {
1901
1982
  return "";
1902
1983
  }
1903
1984
  }
1904
- var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_WATCHDOG_STALL_MS;
1985
+ 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, SWARM_REPO_SCAN_TTL_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_ANOMALY_DEP_MISSING_MS, SWARM_WATCHDOG_STALL_MS;
1905
1986
  var init_config = __esm({
1906
1987
  "src/config.ts"() {
1907
1988
  "use strict";
@@ -1963,6 +2044,7 @@ var init_config = __esm({
1963
2044
  SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
1964
2045
  REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join4(SWARM_HOME, "repo-map.json");
1965
2046
  SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
2047
+ SWARM_REPO_SCAN_TTL_MS = envNum("SWARM_REPO_SCAN_TTL_MS", 24 * 60 * 6e4);
1966
2048
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
1967
2049
  SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
1968
2050
  TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
@@ -1989,6 +2071,7 @@ var init_config = __esm({
1989
2071
  SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
1990
2072
  SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
1991
2073
  SWARM_ANOMALY_RESOURCE_GATE_STALL_MS = envNum("SWARM_ANOMALY_RESOURCE_GATE_STALL_MS", 5 * 6e4);
2074
+ SWARM_ANOMALY_DEP_MISSING_MS = envNum("SWARM_ANOMALY_DEP_MISSING_MS", 2 * 6e4);
1992
2075
  SWARM_WATCHDOG_STALL_MS = envNum("SWARM_WATCHDOG_STALL_MS", 15 * 6e4);
1993
2076
  }
1994
2077
  });
@@ -2103,7 +2186,7 @@ var init_package = __esm({
2103
2186
  "package.json"() {
2104
2187
  package_default = {
2105
2188
  name: "@taskless-app/swarm",
2106
- version: "0.6.13",
2189
+ version: "0.6.15",
2107
2190
  type: "module",
2108
2191
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
2109
2192
  keywords: [
@@ -2153,7 +2236,7 @@ var init_package = __esm({
2153
2236
  execa: "^10.0.0",
2154
2237
  zod: "3.25.76"
2155
2238
  },
2156
- gitHead: "ea3913bf66c52d730c25ad62b7090621a5006ee7"
2239
+ gitHead: "5bc8e0c067de58fd72b0eb434e80ad29c7a00223"
2157
2240
  };
2158
2241
  }
2159
2242
  });
@@ -2171,7 +2254,7 @@ var init_version = __esm({
2171
2254
 
2172
2255
  // src/droneChannel.ts
2173
2256
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
2174
- import { chmodSync, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
2257
+ import { chmodSync, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
2175
2258
  import { dirname as dirname2 } from "path";
2176
2259
  import { serve } from "@hono/node-server";
2177
2260
  function loadChannelSecret(path) {
@@ -2181,7 +2264,7 @@ function loadChannelSecret(path) {
2181
2264
  } catch {
2182
2265
  }
2183
2266
  const secret = randomBytes(32).toString("hex");
2184
- mkdirSync5(dirname2(path), { recursive: true });
2267
+ mkdirSync6(dirname2(path), { recursive: true });
2185
2268
  writeFileSync5(path, secret, { mode: 384 });
2186
2269
  try {
2187
2270
  chmodSync(path, 384);
@@ -2223,7 +2306,7 @@ var init_droneChannel = __esm({
2223
2306
 
2224
2307
  // src/agentToken.ts
2225
2308
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2 } from "fs";
2226
- import { join as join8 } from "path";
2309
+ import { join as join9 } from "path";
2227
2310
  function loadAgentToken() {
2228
2311
  try {
2229
2312
  const s = JSON.parse(readFileSync7(STORE_PATH, "utf8"));
@@ -2304,7 +2387,7 @@ var init_agentToken = __esm({
2304
2387
  "src/agentToken.ts"() {
2305
2388
  "use strict";
2306
2389
  init_config();
2307
- STORE_PATH = join8(SWARM_HOME, "agent.json");
2390
+ STORE_PATH = join9(SWARM_HOME, "agent.json");
2308
2391
  base = (api) => api.replace(/\/$/, "");
2309
2392
  TOKEN_PROBE_TIMEOUT_MS = 8e3;
2310
2393
  }
@@ -2555,13 +2638,13 @@ var init_daemonLink = __esm({
2555
2638
  });
2556
2639
 
2557
2640
  // src/pauseState.ts
2558
- import { existsSync as existsSync9, readFileSync as readFileSync10, renameSync as renameSync4, unlinkSync as unlinkSync5, writeFileSync as writeFileSync9 } from "fs";
2559
- import { join as join16 } from "path";
2641
+ import { existsSync as existsSync10, readFileSync as readFileSync10, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync9 } from "fs";
2642
+ import { join as join17 } from "path";
2560
2643
  function pauseStatePath(home) {
2561
- return join16(home, "pause.json");
2644
+ return join17(home, "pause.json");
2562
2645
  }
2563
2646
  function loadPauseState(home, io = {}) {
2564
- const exists = io.exists ?? existsSync9;
2647
+ const exists = io.exists ?? existsSync10;
2565
2648
  const readFile2 = io.readFile ?? ((p) => readFileSync10(p, "utf8"));
2566
2649
  const path = pauseStatePath(home);
2567
2650
  if (!exists(path)) return ACTIVE;
@@ -2581,13 +2664,13 @@ function loadPauseState(home, io = {}) {
2581
2664
  function savePauseState(home, state, io = {}) {
2582
2665
  const path = pauseStatePath(home);
2583
2666
  if (!state.paused) {
2584
- const remove = io.remove ?? ((p) => existsSync9(p) && unlinkSync5(p));
2667
+ const remove = io.remove ?? ((p) => existsSync10(p) && unlinkSync5(p));
2585
2668
  remove(path);
2586
2669
  return;
2587
2670
  }
2588
2671
  const writeFile3 = io.writeFile ?? ((p, data) => {
2589
2672
  writeFileSync9(`${p}.tmp`, data);
2590
- renameSync4(`${p}.tmp`, p);
2673
+ renameSync5(`${p}.tmp`, p);
2591
2674
  });
2592
2675
  writeFile3(path, JSON.stringify(state, null, 2));
2593
2676
  }
@@ -2623,7 +2706,7 @@ var init_pauseState = __esm({
2623
2706
  });
2624
2707
 
2625
2708
  // src/heartbeat.ts
2626
- import { readFileSync as readFileSync11, renameSync as renameSync5, writeFileSync as writeFileSync10 } from "fs";
2709
+ import { readFileSync as readFileSync11, renameSync as renameSync6, writeFileSync as writeFileSync10 } from "fs";
2627
2710
  function readHeartbeat(path) {
2628
2711
  try {
2629
2712
  const ms = Number(readFileSync11(path, "utf8").trim());
@@ -2644,7 +2727,8 @@ __export(db_exports, {
2644
2727
  SwarmDb: () => SwarmDb
2645
2728
  });
2646
2729
  import { createRequire as createRequire2 } from "module";
2647
- import { existsSync as existsSync10 } from "fs";
2730
+ import { randomUUID as randomUUID3 } from "crypto";
2731
+ import { existsSync as existsSync11 } from "fs";
2648
2732
  import { resolve as resolve2 } from "path";
2649
2733
  function rowToQuestion(row) {
2650
2734
  return {
@@ -2677,6 +2761,7 @@ function rowToRun(row) {
2677
2761
  area: row.area ?? null,
2678
2762
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
2679
2763
  model: row.model ?? null,
2764
+ review_model: row.review_model ?? null,
2680
2765
  // TASWM-395: уровень усилия — через toEffortLevel, а не сырой строкой: в зеркале может
2681
2766
  // лежать значение, записанное демоном другой версии (словарь уровней задаёт CLI), и
2682
2767
  // непонятное здесь означает «уровень не задан», а не сломанный тип.
@@ -2754,6 +2839,7 @@ CREATE TABLE IF NOT EXISTS runs (
2754
2839
  area TEXT,
2755
2840
  depends_on TEXT,
2756
2841
  model TEXT,
2842
+ review_model TEXT,
2757
2843
  effort TEXT,
2758
2844
  cost_usd REAL NOT NULL DEFAULT 0,
2759
2845
  tokens_in INTEGER NOT NULL DEFAULT 0,
@@ -2804,6 +2890,12 @@ CREATE INDEX IF NOT EXISTS idx_runs_area ON runs(area);
2804
2890
  CREATE INDEX IF NOT EXISTS idx_questions_run ON questions(run_id);
2805
2891
  CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
2806
2892
  CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id);
2893
+ -- \u043A\u043B\u044E\u0447-\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u043F\u0440\u043E \u0421\u0410\u041C \u0424\u0410\u0419\u041B \u0431\u0430\u0437\u044B (\u043D\u0435 \u043F\u0440\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u044B). \u041F\u043E\u043A\u0430 \u0432 \u043D\u0451\u043C \u043E\u0434\u043D\u0430 \u0437\u0430\u043F\u0438\u0441\u044C \u2014 instance_id
2894
+ -- (TASWM-419): \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u042D\u0422\u041E\u0419 \u0436\u0438\u0437\u043D\u0438 \u0431\u0430\u0437\u044B, \u043A\u043E\u0442\u043E\u0440\u044B\u043C \u043D\u0435\u0439\u043C\u0441\u043F\u0435\u0439\u0441\u044F\u0442\u0441\u044F \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0438 run-log'\u043E\u0432.
2895
+ CREATE TABLE IF NOT EXISTS meta (
2896
+ key TEXT PRIMARY KEY,
2897
+ value TEXT NOT NULL
2898
+ );
2807
2899
  -- \u043C\u0430\u0440\u043A\u0435\u0440 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u043D\u044B\u0445 \u0438\u043C\u043F\u043E\u0440\u0442\u043E\u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0438\u0437 \u0434\u0440\u0443\u0433\u0438\u0445 \u0411\u0414 (\u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C importDb)
2808
2900
  CREATE TABLE IF NOT EXISTS imports (
2809
2901
  source TEXT PRIMARY KEY,
@@ -2816,6 +2908,8 @@ CREATE TABLE IF NOT EXISTS imports (
2816
2908
  defaultRepo;
2817
2909
  databaseCtor;
2818
2910
  // для importDb (второй Database, source)
2911
+ /** Идентификатор этой жизни файла базы (TASWM-419) — им неймспейсятся каталоги run-log'ов. */
2912
+ instanceId;
2819
2913
  constructor(path = "swarm.db", defaultRepo = "") {
2820
2914
  this.defaultRepo = normalizeRepo(defaultRepo);
2821
2915
  this.databaseCtor = loadBetterSqlite3();
@@ -2867,7 +2961,11 @@ CREATE TABLE IF NOT EXISTS imports (
2867
2961
  "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0",
2868
2962
  // TASWM-395: усилие рассуждения прогона (`claude --effort`). Nullable без DEFAULT —
2869
2963
  // NULL это и все прогоны до фичи, и прогоны на дефолте харнесса.
2870
- "ALTER TABLE runs ADD COLUMN effort TEXT"
2964
+ "ALTER TABLE runs ADD COLUMN effort TEXT",
2965
+ // TASWM-451: чем проверять работу этого прогона — модель ревью, приехавшая с задачи.
2966
+ // Живёт на прогоне ИСПОЛНИТЕЛЯ: ревьюер и верификатор заводятся локально и читают её
2967
+ // оттуда, а не ходят за задачей в прод в момент спавна.
2968
+ "ALTER TABLE runs ADD COLUMN review_model TEXT"
2871
2969
  ]) {
2872
2970
  try {
2873
2971
  this.db.exec(alter);
@@ -2879,6 +2977,22 @@ CREATE TABLE IF NOT EXISTS imports (
2879
2977
  if (this.defaultRepo) {
2880
2978
  this.db.prepare("UPDATE runs SET repo = ? WHERE repo IS NULL").run(this.defaultRepo);
2881
2979
  }
2980
+ this.instanceId = this.ensureInstanceId();
2981
+ }
2982
+ /**
2983
+ * Идентификатор ЭТОЙ жизни файла базы (TASWM-419). Заводится при первом открытии и живёт
2984
+ * в самой базе, поэтому переживает рестарт демона, но НЕ переживает пересоздание файла:
2985
+ * снесли базу — новый id, и каталоги run-log'ов нового прогона #1 не подселяются к файлам
2986
+ * прогона #1 из прошлой жизни. Именно это и было корнем «лог не того прогона»: номер там
2987
+ * был вся информация об адресе.
2988
+ */
2989
+ ensureInstanceId() {
2990
+ const read = () => this.db.prepare("SELECT value FROM meta WHERE key = 'instance_id'").get()?.value;
2991
+ const existing = read();
2992
+ if (existing) return existing;
2993
+ const fresh = randomUUID3().replace(/-/g, "").slice(0, 12);
2994
+ this.db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES ('instance_id', ?)").run(fresh);
2995
+ return read() ?? fresh;
2882
2996
  }
2883
2997
  close() {
2884
2998
  this.db.close();
@@ -2887,7 +3001,7 @@ CREATE TABLE IF NOT EXISTS imports (
2887
3001
  createRun(input) {
2888
3002
  const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
2889
3003
  const res = this.db.prepare(
2890
- "INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, effort, decision, resolves_run_id, reviews_run_id, verifies_run_id, review_cycle, priority, base_branch, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
3004
+ "INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, review_model, effort, decision, resolves_run_id, reviews_run_id, verifies_run_id, review_cycle, priority, base_branch, workspace_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
2891
3005
  ).run(
2892
3006
  input.task_slug,
2893
3007
  name,
@@ -2900,6 +3014,7 @@ CREATE TABLE IF NOT EXISTS imports (
2900
3014
  input.area ?? null,
2901
3015
  input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
2902
3016
  input.model ?? null,
3017
+ input.review_model ?? null,
2903
3018
  input.effort ?? null,
2904
3019
  // усилие рассуждения (TASWM-395); null → разрешит спавн
2905
3020
  input.decision ? JSON.stringify(input.decision) : null,
@@ -2972,6 +3087,15 @@ CREATE TABLE IF NOT EXISTS imports (
2972
3087
  const m = /^local:(\d+)$/.exec(key);
2973
3088
  return m ? this.getRun(Number(m[1])) : null;
2974
3089
  }
3090
+ /**
3091
+ * TASWM-419: адрес каталога run-log'ов — номер, НЕЙМСПЕЙСНУТЫЙ идентификатором файла базы
3092
+ * (`local:<instance>:<id>`). Тот же номер в пересозданной базе даёт другой instance, то
3093
+ * есть другой каталог: подселиться к файлам чужого прогона с тем же номером больше нечем.
3094
+ * Псевдонимов у sqlite-прогона нет — ключ не меняется, пока жив файл базы.
3095
+ */
3096
+ runLogKeys(id) {
3097
+ return this.getRun(id) ? [`local:${this.instanceId}:${id}`] : [];
3098
+ }
2975
3099
  listRuns(status) {
2976
3100
  const rows = status ? this.db.prepare("SELECT * FROM runs WHERE status = ? ORDER BY id DESC").all(status) : this.db.prepare("SELECT * FROM runs ORDER BY id DESC").all();
2977
3101
  return rows.map(rowToRun);
@@ -3212,7 +3336,7 @@ CREATE TABLE IF NOT EXISTS imports (
3212
3336
  const repoN = normalizeRepo(repo);
3213
3337
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
3214
3338
  if (already) return { imported: false, runs: already.run_count };
3215
- if (!existsSync10(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
3339
+ if (!existsSync11(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
3216
3340
  const src = new this.databaseCtor(abs);
3217
3341
  try {
3218
3342
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -3539,8 +3663,8 @@ CREATE TABLE IF NOT EXISTS imports (
3539
3663
  });
3540
3664
 
3541
3665
  // src/repoScan.ts
3542
- import { readdirSync as readdirSync8, readFileSync as readFileSync12, statSync as statSync8 } from "fs";
3543
- import { join as join17 } from "path";
3666
+ import { readdirSync as readdirSync9, readFileSync as readFileSync12, statSync as statSync9 } from "fs";
3667
+ import { join as join18 } from "path";
3544
3668
  var init_repoScan = __esm({
3545
3669
  "src/repoScan.ts"() {
3546
3670
  "use strict";
@@ -3550,8 +3674,8 @@ var init_repoScan = __esm({
3550
3674
 
3551
3675
  // src/repoDiscovery.ts
3552
3676
  import { homedir as homedir4 } from "os";
3553
- import { readFileSync as readFileSync13, renameSync as renameSync6, writeFileSync as writeFileSync11 } from "fs";
3554
- import { isAbsolute as isAbsolute3, join as join18, resolve as resolve3 } from "path";
3677
+ import { readFileSync as readFileSync13, renameSync as renameSync7, writeFileSync as writeFileSync11 } from "fs";
3678
+ import { isAbsolute as isAbsolute3, join as join19, resolve as resolve3 } from "path";
3555
3679
  function repoMapLines(rows) {
3556
3680
  if (!rows?.length) return [];
3557
3681
  const missing = rows.filter((r) => !r.path).length;
@@ -3564,18 +3688,24 @@ function repoMapLines(rows) {
3564
3688
  });
3565
3689
  return [head, ...body];
3566
3690
  }
3567
- function readRepoMapCache(path) {
3691
+ function loadRepoMapCache(path) {
3568
3692
  try {
3569
3693
  const raw = JSON.parse(readFileSync13(path, "utf8"));
3570
- if (!Array.isArray(raw?.entries)) return [];
3571
- return raw.entries.filter(
3572
- (e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
3573
- );
3694
+ if (!Array.isArray(raw?.entries)) return { scannedAt: null, entries: [] };
3695
+ return {
3696
+ scannedAt: typeof raw.scannedAt === "string" && raw.scannedAt ? raw.scannedAt : null,
3697
+ entries: raw.entries.filter(
3698
+ (e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
3699
+ )
3700
+ };
3574
3701
  } catch {
3575
- return [];
3702
+ return { scannedAt: null, entries: [] };
3576
3703
  }
3577
3704
  }
3578
- var SOURCE_TEXT;
3705
+ function readRepoMapCache(path) {
3706
+ return loadRepoMapCache(path).entries;
3707
+ }
3708
+ var REPO_SCAN_TTL_MS, UNKNOWN_LIST_SCAN_TTL_MS, SOURCE_TEXT;
3579
3709
  var init_repoDiscovery = __esm({
3580
3710
  "src/repoDiscovery.ts"() {
3581
3711
  "use strict";
@@ -3583,6 +3713,8 @@ var init_repoDiscovery = __esm({
3583
3713
  init_config();
3584
3714
  init_linkFailure();
3585
3715
  init_repoScan();
3716
+ REPO_SCAN_TTL_MS = 24 * 60 * 6e4;
3717
+ UNKNOWN_LIST_SCAN_TTL_MS = 60 * 6e4;
3586
3718
  SOURCE_TEXT = {
3587
3719
  configured: "\u043F\u0443\u0442\u044C \u0437\u0430\u0434\u0430\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C",
3588
3720
  cache: "\u0438\u0437 \u043A\u044D\u0448\u0430 \u043C\u0430\u0448\u0438\u043D\u044B",
@@ -3927,10 +4059,10 @@ __export(statusCli_exports, {
3927
4059
  probeLink: () => probeLink,
3928
4060
  runStatusCli: () => runStatusCli
3929
4061
  });
3930
- import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
4062
+ import { existsSync as existsSync12, readFileSync as readFileSync14 } from "fs";
3931
4063
  function outboxPending(path) {
3932
4064
  try {
3933
- if (!existsSync11(path)) return 0;
4065
+ if (!existsSync12(path)) return 0;
3934
4066
  return readFileSync14(path, "utf8").split("\n").filter((l) => l.trim()).length;
3935
4067
  } catch {
3936
4068
  return null;
@@ -4069,8 +4201,8 @@ var init_statusCli = __esm({
4069
4201
  });
4070
4202
 
4071
4203
  // src/platform.ts
4072
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
4073
- import { delimiter, join as join19 } from "path";
4204
+ import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
4205
+ import { delimiter, join as join20 } from "path";
4074
4206
  function safeReadText(path) {
4075
4207
  try {
4076
4208
  return readFileSync15(path, "utf8");
@@ -4079,13 +4211,13 @@ function safeReadText(path) {
4079
4211
  }
4080
4212
  }
4081
4213
  function resolveCommandPath(name, env = process.env, platform = process.platform) {
4082
- if (name.includes("/") || name.includes("\\")) return existsSync12(name) ? name : null;
4214
+ if (name.includes("/") || name.includes("\\")) return existsSync13(name) ? name : null;
4083
4215
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
4084
4216
  const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
4085
4217
  for (const dir of dirs) {
4086
4218
  for (const ext of exts) {
4087
- const candidate = join19(dir, name + ext);
4088
- if (existsSync12(candidate)) return candidate;
4219
+ const candidate = join20(dir, name + ext);
4220
+ if (existsSync13(candidate)) return candidate;
4089
4221
  }
4090
4222
  }
4091
4223
  return null;
@@ -4188,13 +4320,13 @@ var init_platform = __esm({
4188
4320
  });
4189
4321
 
4190
4322
  // src/envGate.ts
4191
- import { existsSync as existsSync13 } from "fs";
4192
- import { join as join20 } from "path";
4323
+ import { existsSync as existsSync14 } from "fs";
4324
+ import { join as join21 } from "path";
4193
4325
  import { createConnection } from "net";
4194
4326
  import { execa as execa3 } from "execa";
4195
4327
  function detectRequirements(repo, io = {}) {
4196
- const exists = io.exists ?? existsSync13;
4197
- const marker = COMPOSE_MARKERS.find((f) => exists(join20(repo, f)));
4328
+ const exists = io.exists ?? existsSync14;
4329
+ const marker = COMPOSE_MARKERS.find((f) => exists(join21(repo, f)));
4198
4330
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
4199
4331
  }
4200
4332
  async function dockerAlive() {
@@ -4226,7 +4358,7 @@ var init_envGate = __esm({
4226
4358
  });
4227
4359
 
4228
4360
  // src/doctor.ts
4229
- import { isAbsolute as isAbsolute4, join as join21 } from "path";
4361
+ import { isAbsolute as isAbsolute4, join as join22 } from "path";
4230
4362
  function ok(id, title, detail) {
4231
4363
  return { id, title, status: "ok", detail, remedies: [], critical: false };
4232
4364
  }
@@ -4433,7 +4565,7 @@ function checkRepos(io, snap) {
4433
4565
  critical: true
4434
4566
  };
4435
4567
  }
4436
- if (env && !io.exists(join21(env, ".git"))) {
4568
+ if (env && !io.exists(join22(env, ".git"))) {
4437
4569
  return {
4438
4570
  id: "repos",
4439
4571
  title,
@@ -4443,7 +4575,7 @@ function checkRepos(io, snap) {
4443
4575
  critical: true
4444
4576
  };
4445
4577
  }
4446
- const broken = snap.paths.filter((p) => !io.exists(join21(p, ".git")));
4578
+ const broken = snap.paths.filter((p) => !io.exists(join22(p, ".git")));
4447
4579
  if (broken.length) {
4448
4580
  return {
4449
4581
  id: "repos",
@@ -4608,11 +4740,11 @@ var init_doctor = __esm({
4608
4740
 
4609
4741
  // src/repoMemory.ts
4610
4742
  import { readFileSync as readFileSync16, writeFileSync as writeFileSync12 } from "fs";
4611
- import { join as join22 } from "path";
4743
+ import { join as join23 } from "path";
4612
4744
  function loadRememberedRepos(home) {
4613
4745
  let raw;
4614
4746
  try {
4615
- raw = readFileSync16(join22(home, FILE), "utf8");
4747
+ raw = readFileSync16(join23(home, FILE), "utf8");
4616
4748
  } catch {
4617
4749
  return [];
4618
4750
  }
@@ -4647,7 +4779,7 @@ __export(installCli_exports, {
4647
4779
  runInstallCli: () => runInstallCli,
4648
4780
  shortenError: () => shortenError
4649
4781
  });
4650
- import { existsSync as existsSync14 } from "fs";
4782
+ import { existsSync as existsSync15 } from "fs";
4651
4783
  function classifyChromiumError(message) {
4652
4784
  if (/playwright-core не установлен|Cannot find (module|package)/i.test(message)) return "no-module";
4653
4785
  if (/error while loading shared libraries|cannot open shared object file|missing dependencies|\blib[\w.+-]*\.so[.\d]*\b/i.test(message)) return "no-libs";
@@ -4702,7 +4834,7 @@ function defaultDoctorIO(packageVersion) {
4702
4834
  packageVersion,
4703
4835
  entry,
4704
4836
  hasCommand: (name) => hasCommandInPath(name),
4705
- exists: existsSync14,
4837
+ exists: existsSync15,
4706
4838
  run: (command) => runQuiet(command),
4707
4839
  latestVersion: () => latestMemo ??= fetchLatestVersion(),
4708
4840
  probeChromium: () => probeChromium(),
@@ -4911,13 +5043,13 @@ var init_updateCli = __esm({
4911
5043
 
4912
5044
  // src/cli.ts
4913
5045
  init_cliIo();
4914
- import { basename as basename4, join as join23 } from "path";
4915
- import { existsSync as existsSync15, readFileSync as readFileSync17, readdirSync as readdirSync9, statSync as statSync9, openSync as openSync4, readSync as readSync2, closeSync as closeSync4, rmSync as rmSync2 } from "fs";
5046
+ import { basename as basename4, join as join24 } from "path";
5047
+ import { existsSync as existsSync16, readFileSync as readFileSync17, readdirSync as readdirSync10, statSync as statSync10, openSync as openSync4, readSync as readSync2, closeSync as closeSync4, rmSync as rmSync2 } from "fs";
4916
5048
  import { setTimeout as sleep2 } from "timers/promises";
4917
5049
 
4918
5050
  // src/swarmClient.ts
4919
5051
  init_config();
4920
- import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
5052
+ import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
4921
5053
  import { dirname } from "path";
4922
5054
  import { randomUUID } from "crypto";
4923
5055
 
@@ -5124,8 +5256,34 @@ init_names2();
5124
5256
  init_storm();
5125
5257
 
5126
5258
  // src/logTail.ts
5127
- import { closeSync, existsSync as existsSync3, openSync, readSync, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
5259
+ import { closeSync, existsSync as existsSync4, openSync, readSync, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
5260
+ import { join as join6 } from "path";
5261
+
5262
+ // src/runLogDir.ts
5263
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, readdirSync as readdirSync4, renameSync as renameSync2, statSync as statSync3 } from "fs";
5128
5264
  import { join as join5 } from "path";
5265
+ function runLogDirName(runLogKey) {
5266
+ const safe = runLogKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
5267
+ if (!safe) throw new Error(`\u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435 \u0433\u043E\u0434\u0438\u0442\u0441\u044F \u0432 \u0438\u043C\u044F \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430 run-log'\u043E\u0432: ${JSON.stringify(runLogKey)}`);
5268
+ return /^\d+$/.test(safe) ? `k-${safe}` : safe;
5269
+ }
5270
+ function runLogDirPath(runLogsDir, runLogKey) {
5271
+ return join5(runLogsDir, runLogDirName(runLogKey));
5272
+ }
5273
+ function findRunLogDir(runLogsDir, keys) {
5274
+ for (const key of keys) {
5275
+ const dir = runLogDirPath(runLogsDir, key);
5276
+ if (existsSync3(dir)) return dir;
5277
+ }
5278
+ return null;
5279
+ }
5280
+ function runLogDirForWrite(runLogsDir, keys) {
5281
+ const canonical = keys[0];
5282
+ if (!canonical) return null;
5283
+ return findRunLogDir(runLogsDir, keys) ?? runLogDirPath(runLogsDir, canonical);
5284
+ }
5285
+
5286
+ // src/logTail.ts
5129
5287
  var TAIL_LINES_FALLBACK = 200;
5130
5288
  var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
5131
5289
 
@@ -5212,6 +5370,11 @@ var SwarmClient = class _SwarmClient {
5212
5370
  // Живёт в памяти: рестарт демона просто переспросит прод.
5213
5371
  depsProd = /* @__PURE__ */ new Map();
5214
5372
  depsAskedAt = /* @__PURE__ */ new Map();
5373
+ // TASWM-399: ссылка dependsOn → человеко-слаг блокера, как его назвал прод. В колонке
5374
+ // лежит cuid прогона или задачи, а в лог демона, в событие прогона и в аномалию должен
5375
+ // уходить «TASWM-392», а не «cmsqqpphd011y2xtbckkut6bj» — иначе оператору всё равно
5376
+ // нечего искать. Резолв делает прод (см. resolveDepRefs), здесь только запоминаем.
5377
+ depSlug = /* @__PURE__ */ new Map();
5215
5378
  static DEPS_TTL_MS = 6e4;
5216
5379
  // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
5217
5380
  // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
@@ -5271,25 +5434,25 @@ var SwarmClient = class _SwarmClient {
5271
5434
  this.repoFullNames = registry.map((e) => e.repoFullName);
5272
5435
  this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
5273
5436
  this.host = opts.host ?? null;
5274
- this.repoExists = opts.repoExists ?? existsSync4;
5437
+ this.repoExists = opts.repoExists ?? existsSync5;
5275
5438
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
5276
5439
  this.executorId = opts.executorId ?? EXECUTOR.id;
5277
5440
  this.outboxPath = opts.outboxPath;
5278
5441
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
5279
5442
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
5280
- mkdirSync4(dirname(this.outboxPath), { recursive: true });
5443
+ mkdirSync5(dirname(this.outboxPath), { recursive: true });
5281
5444
  this.recover();
5282
5445
  }
5283
5446
  // ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
5284
5447
  recover() {
5285
- if (existsSync4(this.mapPath)) {
5448
+ if (existsSync5(this.mapPath)) {
5286
5449
  try {
5287
5450
  const obj = JSON.parse(readFileSync3(this.mapPath, "utf8"));
5288
5451
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
5289
5452
  } catch {
5290
5453
  }
5291
5454
  }
5292
- if (existsSync4(this.outboxPath)) {
5455
+ if (existsSync5(this.outboxPath)) {
5293
5456
  const txt = readFileSync3(this.outboxPath, "utf8");
5294
5457
  for (const line of txt.split("\n")) {
5295
5458
  const s = line.trim();
@@ -5310,7 +5473,7 @@ var SwarmClient = class _SwarmClient {
5310
5473
  // eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
5311
5474
  // переприсваиваем локально (id событий — только порядок, не корреляция с продом).
5312
5475
  replayGlobalEvents() {
5313
- if (!existsSync4(this.stormPath)) return;
5476
+ if (!existsSync5(this.stormPath)) return;
5314
5477
  let txt;
5315
5478
  try {
5316
5479
  txt = readFileSync3(this.stormPath, "utf8");
@@ -5390,6 +5553,7 @@ var SwarmClient = class _SwarmClient {
5390
5553
  if (patch.status === "running") out.executor = this.executorId;
5391
5554
  if ("area" in patch) out.area = patch.area ?? null;
5392
5555
  if ("model" in patch) out.model = patch.model ?? null;
5556
+ if ("review_model" in patch) out.reviewModel = patch.review_model ?? null;
5393
5557
  if ("effort" in patch) out.effort = patch.effort ?? null;
5394
5558
  if ("name" in patch) out.droneName = patch.name ?? null;
5395
5559
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
@@ -5463,6 +5627,10 @@ var SwarmClient = class _SwarmClient {
5463
5627
  area: input.area ?? null,
5464
5628
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
5465
5629
  model: input.model ?? null,
5630
+ // TASWM-451: чем проверять эту работу. У прогона исполнителя приезжает из задачи
5631
+ // (claim в проде), у локально заведённого ревьюера/верификатора — наследством от
5632
+ // проверяемого прогона, которое ставит spawnReviewer/spawnVerifier.
5633
+ review_model: input.review_model ?? null,
5466
5634
  // Усилие рассуждения (TASWM-395): пин, если его поставили на спавне; иначе null —
5467
5635
  // разрешённый уровень впечатает launcher, когда действительно поднимет дрона.
5468
5636
  effort: input.effort ?? null,
@@ -5521,6 +5689,8 @@ var SwarmClient = class _SwarmClient {
5521
5689
  status: "queued",
5522
5690
  area: d.area,
5523
5691
  model: d.model,
5692
+ review_model: d.review_model,
5693
+ // TASWM-451: чем проверять — едет рядом с моделью
5524
5694
  effort: d.effort,
5525
5695
  // усилие рассуждения (TASWM-395), если пинили на спавне
5526
5696
  name: d.name,
@@ -5568,6 +5738,24 @@ var SwarmClient = class _SwarmClient {
5568
5738
  const viaProd = [...this.runs.values()].find((r) => r.runKey === `p:${prodId}` || this.prodIdByKey.get(r.runKey) === prodId);
5569
5739
  return viaProd ? this.mat(viaProd) : null;
5570
5740
  }
5741
+ /**
5742
+ * TASWM-419: адреса каталога run-log'ов прогона. КАНОНИЧЕСКИЙ — прод-вида `p:<cuid>`, как
5743
+ * только cuid известен: он один и тот же во всех жизнях демона, а локальный uuid прогона
5744
+ * после рестарта уже не воскресает (гидрированный прогон приезжает под прод-ключом).
5745
+ * Дальше — псевдонимы: uuid, под которым каталог мог завестись в окно между спавном и
5746
+ * первым флашем в прод. По ним каталог ищется, чтобы запуски одного прогона не расползлись
5747
+ * по двум каталогам (иначе cost недосчитал бы часть, а хвост показал бы не тот запуск).
5748
+ * Номер прогона в адресе не участвует вовсе — он и есть источник «лога чужого прогона».
5749
+ */
5750
+ runLogKeys(id) {
5751
+ const key = this.runs.get(id)?.runKey;
5752
+ if (!key) return [];
5753
+ const prodId = key.startsWith("p:") ? key.slice(2) : this.prodIdByKey.get(key);
5754
+ if (!prodId) return [key];
5755
+ const canonical = `p:${prodId}`;
5756
+ const aliases = [key, ...[...this.prodIdByKey.entries()].filter(([, v]) => v === prodId).map(([k]) => k)];
5757
+ return [canonical, ...new Set(aliases.filter((k) => k !== canonical))];
5758
+ }
5571
5759
  listRuns(status) {
5572
5760
  const all = [...this.runs.values()].filter((r) => status ? r.status === status : true);
5573
5761
  all.sort((a, b) => b.id - a.id);
@@ -5684,16 +5872,25 @@ var SwarmClient = class _SwarmClient {
5684
5872
  /**
5685
5873
  * TASWM-361: вердикт по каждой зависимости — см. докблок SwarmStore.depVerdicts.
5686
5874
  *
5687
- * АСИММЕТРИЯ ЗЕРКАЛА И ПРОДА, и почему она здесь намеренная. Зеркало — ЧАСТИЧНАЯ выборка
5688
- * (только прогоны этой машины, только окно гидрации), и по частичной выборке два вывода
5689
- * стоят разного:
5690
- * • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он не
5691
- * портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
5692
- * • «живых прогонов слага не осталось» — вывод от ОТСУТСТВИЯ: невидимый зеркалу прогон
5693
- * на соседней машине как раз и может быть живым. Поэтому «исчерпана неуспешно»
5694
- * принимаем ТОЛЬКО из ответа прода (refreshDeps) — он видит всех.
5695
- * Прод недоступен → вердикта нет → прогон честно ждёт следующего окна: лишняя минута
5696
- * очереди дешевле дрона, поднятого поверх работающего блокера.
5875
+ * ИСТОЧНИК ПРАВДЫ — ПРОД (TASWM-399), зеркало здесь только быстрый путь. Зеркало —
5876
+ * ЧАСТИЧНАЯ выборка (прогоны этой машины, окно гидрации), и по частичной выборке два
5877
+ * вывода стоят разного:
5878
+ * • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он
5879
+ * не портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
5880
+ * • всё остальное — вывод от ОТСУТСТВИЯ, и его принимаем ТОЛЬКО из ответа прода
5881
+ * (refreshDeps): невидимый зеркалу прогон на соседней машине как раз и может быть
5882
+ * живым, а закрытый с прода — давно смерженным.
5883
+ *
5884
+ * ССЫЛКА — НЕ ОБЯЗАТЕЛЬНО СЛАГ. В `dependsOn` лежит чаще всего cuid ПРОГОНА (так пишет
5885
+ * overmind), реже cuid задачи, и только дев-спавн `--after` пишет человеко-слаг. Быстрый
5886
+ * путь по зеркалу сравнивает `task_slug`, то есть срабатывает лишь на третьей форме —
5887
+ * это нормально: остальные две резолвит прод. А вот считать «раз в зеркале не нашёл —
5888
+ * значит не готово» было корнем TASWM-399.
5889
+ *
5890
+ * Прода ещё не спрашивали (первый тик) или он моргнул → `pending`: прогон честно ждёт
5891
+ * следующего окна. Лишняя минута очереди дешевле дрона, поднятого поверх работающего
5892
+ * блокера. `unknown` (блокера НЕ ЗНАЕТ И ПРОД) ставит только ответ прода — местным
5893
+ * незнанием такой вывод не делается.
5697
5894
  */
5698
5895
  depVerdicts(depends_on) {
5699
5896
  if (!depends_on || depends_on.length === 0) return [];
@@ -5703,57 +5900,89 @@ var SwarmClient = class _SwarmClient {
5703
5900
  doneInMirror.add(r.task_slug);
5704
5901
  }
5705
5902
  }
5706
- return depends_on.map((slug) => {
5707
- if (doneInMirror.has(slug)) {
5708
- return { slug, outcome: "done", met: true, status: "done", name: null, stopReason: null };
5903
+ return depends_on.map((ref) => {
5904
+ if (doneInMirror.has(ref)) {
5905
+ return { slug: ref, outcome: "done", met: true, status: "done", name: null, stopReason: null };
5709
5906
  }
5710
- const fromProd = this.depsProd.get(slug);
5711
- if (fromProd) return { slug, ...fromProd };
5712
- return { slug, outcome: "pending", met: false, status: null, name: null, stopReason: null };
5907
+ const fromProd = this.depsProd.get(ref);
5908
+ if (fromProd) {
5909
+ if (fromProd.outcome === "unknown" && this.knowsDepRef(ref)) {
5910
+ return { slug: this.depSlug.get(ref) ?? ref, outcome: "pending", met: false, status: null, name: null, stopReason: null };
5911
+ }
5912
+ return { slug: this.depSlug.get(ref) ?? ref, ...fromProd };
5913
+ }
5914
+ return { slug: ref, outcome: "pending", met: false, status: null, name: null, stopReason: null };
5713
5915
  });
5714
5916
  }
5715
5917
  /**
5716
- * TASWM-300: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
5918
+ * Знает ли ЗЕРКАЛО хоть один прогон по ссылке `dependsOn` (TASWM-399). Ссылка бывает
5919
+ * слагом, cuid'ом задачи и cuid'ом прогона — последний сверяем с прод-id, как это
5920
+ * делают runByProdId/refreshRun. Служебные сабраны (ревьюер/резолвер/верификатор) не
5921
+ * в счёт: их существование не означает, что жив сам блокер.
5922
+ */
5923
+ knowsDepRef(ref) {
5924
+ for (const r of this.runs.values()) {
5925
+ if (r.resolves_run_id != null || r.reviews_run_id != null || r.verifies_run_id != null) continue;
5926
+ if (r.task_slug === ref) return true;
5927
+ if (r.runKey === `p:${ref}` || this.prodIdByKey.get(r.runKey) === ref) return true;
5928
+ }
5929
+ return false;
5930
+ }
5931
+ /**
5932
+ * TASWM-300 → TASWM-399: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
5717
5933
  *
5718
- * Почему зеркала было мало. В нём лежат только прогоны ЭТОЙ машины (гейт isMine,
5719
- * hydrateRun) и только те, что попали в окно гидрации (`swarm_list_runs limit`, сортировка
5720
- * по updatedAt desc — сутки работы роя). Зависимость, закрытая на соседней машине или
5721
- * просто позавчера, в зеркало не попадала — и `--after` держал прогон в очереди вечно
5722
- * (04.08: снимали руками).
5934
+ * Почему зеркала мало. В нём лежат только прогоны ЭТОЙ машины (гейт isMine, hydrateRun) и
5935
+ * только те, что попали в окно гидрации (`swarm_list_runs limit`, сортировка по updatedAt
5936
+ * desc — сутки работы роя). Зависимость, закрытая на соседней машине, обнулившаяся при
5937
+ * рестарте демона или просто позавчерашняя, в зеркало не попадала — и `dependsOn` держал
5938
+ * прогон в очереди вечно.
5939
+ *
5940
+ * ЧЕМ СПРАШИВАЕМ И ПОЧЕМУ НЕ `swarm_list_runs` (корень TASWM-399). До этой задачи здесь
5941
+ * шёл `swarm_list_runs { taskId: <ref> }` — то есть ссылка трактовалась как ЗАДАЧА. Но в
5942
+ * `dependsOn` чаще всего лежит cuid ПРОГОНА: `resolveTaskRef` его задачей не признаёт,
5943
+ * ответ приходит `isError`, вердикт не появляется НИКОГДА, и прогон стоит молча при
5944
+ * свободной машине (12–13.08 — пять остановок роя за сутки). `swarm_deps_status` понимает
5945
+ * все три формы ссылки и отвечает по единому правилу роя (`judgeDep` на прод-стороне).
5723
5946
  *
5724
5947
  * Кеш. «Смержено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
5725
5948
  * положительный ответ помним навсегда и больше не спрашиваем. Остальные вердикты (включая
5726
- * «исчерпана неуспешно», TASWM-361 — блокер могут переиграть новым прогоном) переспрашиваем
5727
- * не чаще DEPS_TTL_MS: тик идёт раз в ~4с, а ждущих зависимость прогонов может быть
5728
- * много — без TTL это был бы поход в прод на каждый слаг каждый тик.
5949
+ * «исчерпана неуспешно» и «блокера не знает никто») переспрашиваем не чаще DEPS_TTL_MS:
5950
+ * оператор вправе переиграть блокер новым прогоном, а тик идёт раз в ~4 с — без TTL это
5951
+ * был бы поход в прод на каждую ссылку каждый тик.
5952
+ *
5953
+ * Одним вызовом на все просроченные ссылки, а не по вызову на ссылку: очередь ждущих
5954
+ * бывает в десятки прогонов, и N запросов на тик — это тот же самый поход в прод, только
5955
+ * дороже.
5729
5956
  */
5730
- async refreshDeps(slugs, nowMs = Date.now()) {
5731
- for (const slug of new Set(slugs)) {
5732
- if (!slug || this.depsProd.get(slug)?.outcome === "done") continue;
5733
- const asked = this.depsAskedAt.get(slug);
5734
- if (asked !== void 0 && nowMs - asked < _SwarmClient.DEPS_TTL_MS) continue;
5735
- this.depsAskedAt.set(slug, nowMs);
5736
- let res;
5737
- try {
5738
- res = await this.call("swarm_list_runs", { taskId: slug });
5739
- } catch {
5740
- this.depsAskedAt.delete(slug);
5741
- continue;
5742
- }
5743
- if (res?.isError) continue;
5744
- const runs = res.structuredContent?.runs ?? [];
5745
- const facts = runs.slice().sort((a, b) => String(a.createdAt ?? "").localeCompare(String(b.createdAt ?? ""))).map((r) => ({
5746
- status: String(r.status ?? ""),
5747
- // Служебный сабран под тем же слагом (резолвер конфликта / ревьюер): его финал —
5748
- // не финал задачи. Резолвер вычёркивался и до TASWM-361, ревьюер добавлен вместе
5749
- // с расширением критерия — иначе остановленный ревьюер закрывал бы зависимость.
5750
- service: Boolean(r.resolvesRunId) || Boolean(r.reviewsRunId) || Boolean(r.verifiesRunId),
5751
- // В проде имя дрона — колонка droneName (см. hydrateRun); `name` терпим как
5752
- // запасной вариант ради фикстур и старых ответов.
5753
- name: r.droneName ?? r.name ?? null,
5754
- stopReason: r.stopReason ?? null
5755
- }));
5756
- this.depsProd.set(slug, judgeDep(facts));
5957
+ async refreshDeps(refs, nowMs = Date.now()) {
5958
+ const ask = [...new Set(refs)].filter((ref) => {
5959
+ if (!ref || this.depsProd.get(ref)?.outcome === "done") return false;
5960
+ const asked = this.depsAskedAt.get(ref);
5961
+ return asked === void 0 || nowMs - asked >= _SwarmClient.DEPS_TTL_MS;
5962
+ });
5963
+ if (!ask.length) return;
5964
+ for (const ref of ask) this.depsAskedAt.set(ref, nowMs);
5965
+ let res;
5966
+ try {
5967
+ res = await this.call("swarm_deps_status", { refs: ask });
5968
+ } catch {
5969
+ for (const ref of ask) this.depsAskedAt.delete(ref);
5970
+ return;
5971
+ }
5972
+ if (res?.isError) return;
5973
+ const deps = res.structuredContent?.deps ?? [];
5974
+ for (const d of deps) {
5975
+ const ref = typeof d.ref === "string" ? d.ref : null;
5976
+ if (!ref) continue;
5977
+ const outcome = String(d.outcome ?? "pending");
5978
+ this.depsProd.set(ref, {
5979
+ outcome,
5980
+ met: Boolean(d.met),
5981
+ status: d.status ?? null,
5982
+ name: d.name ?? null,
5983
+ stopReason: d.stopReason ?? null
5984
+ });
5985
+ if (typeof d.slug === "string" && d.slug) this.depSlug.set(ref, d.slug);
5757
5986
  }
5758
5987
  }
5759
5988
  /**
@@ -6072,7 +6301,7 @@ var SwarmClient = class _SwarmClient {
6072
6301
  * транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
6073
6302
  */
6074
6303
  reconcileRuntime(opts) {
6075
- const wtExists = opts.worktreeExists ?? existsSync4;
6304
+ const wtExists = opts.worktreeExists ?? existsSync5;
6076
6305
  const pidByWorktree = /* @__PURE__ */ new Map();
6077
6306
  try {
6078
6307
  for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
@@ -6561,6 +6790,12 @@ var SwarmClient = class _SwarmClient {
6561
6790
  ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
6562
6791
  ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
6563
6792
  ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {},
6793
+ // TASWM-424: «доступ отозван» — состояние ровно того же рода, что пауза, и
6794
+ // формулировка у него ОДНА на все поверхности (HOST_ACCESS_REVOKED_TEXT):
6795
+ // `hostLines` зовёт тот же `hostStateText`, что и парк машин в /swarm. Без
6796
+ // этого поля `swarm hosts` показывал отвергнутую машину просто молчащей —
6797
+ // то самое различие, ради которого флаг и заводили (TASWM-324).
6798
+ ...typeof h.accessRevoked === "boolean" ? { accessRevoked: h.accessRevoked } : {},
6564
6799
  // Показаний нет (старый прод, машина со старым демоном) — ключа не заводим вовсе,
6565
6800
  // ровно как с паузой выше: «не знаем» и так значит «не знаем».
6566
6801
  ...resources ? { resources } : {}
@@ -6646,9 +6881,18 @@ var SwarmClient = class _SwarmClient {
6646
6881
  * ВСЕГДА (в т.ч. `storm:false`), как паузу: это состояние, а не команда, и молчание тут
6647
6882
  * означало бы «прод помнит вчерашний шторм», когда рой давно работает.
6648
6883
  *
6884
+ * TASWM-402: тем же пульсом уезжают КОРНИ WORKTREE'ЕВ этой машины (`worktreeRoots` —
6885
+ * `owner/repo` → `<worktreesDir>/<repo-basename>`). Это НЕ рантайм и правило «pid и путь
6886
+ * дрона в прод не уходят» не трогает: корень — статичная конфигурация машины, того же
6887
+ * рода, что реестр репо и ёмкость, он не зависит ни от прогона, ни от живости процесса и
6888
+ * не протухает между пульсами. Прод склеивает `<корень>/<слаг>` сам — и отвечает человеку
6889
+ * на вопрос «куда идти руками», когда команда машине не доиграла (а в этом случае, по
6890
+ * определению, спросить рантайм у молчащей машины уже нельзя). Не передали корень (не
6891
+ * знаем своего каталога) → ключа нет, прод прошлое не трогает — как с capacity/resources.
6892
+ *
6649
6893
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
6650
6894
  */
6651
- async heartbeat(daemonVersion, pause, capacity, resources, storm) {
6895
+ async heartbeat(daemonVersion, pause, capacity, resources, storm, worktreesDir, signIn) {
6652
6896
  try {
6653
6897
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
6654
6898
  if (daemonVersion) args.daemonVersion = daemonVersion;
@@ -6658,10 +6902,19 @@ var SwarmClient = class _SwarmClient {
6658
6902
  args.paused = !!pause?.paused;
6659
6903
  if (pause?.reason) args.pauseReason = pause.reason;
6660
6904
  }
6905
+ if (signIn) {
6906
+ args.signedIn = signIn.signedIn;
6907
+ if (signIn.webUrl) args.webUrl = signIn.webUrl;
6908
+ }
6661
6909
  if (storm !== void 0 && storm !== null) {
6662
6910
  args.storm = storm.active;
6663
6911
  if (storm.reason) args.stormReason = storm.reason;
6664
6912
  }
6913
+ if (worktreesDir) {
6914
+ const roots = {};
6915
+ for (const [full, path] of this.repoPathByFullName) roots[full] = droneWorktreeRoot(worktreesDir, path);
6916
+ if (Object.keys(roots).length) args.worktreeRoots = roots;
6917
+ }
6665
6918
  if (this.host) {
6666
6919
  args.hostId = this.host.hostId;
6667
6920
  args.hostName = this.host.hostName;
@@ -6810,10 +7063,20 @@ var SwarmClient = class _SwarmClient {
6810
7063
  * именно путь. Не нашли в реестре (репо не наш / ручной `--repo <path>` спавн, где
6811
7064
  * repo — ВСЕГДА уже путь, старый формат) → возвращаем как есть без изменений
6812
7065
  * (обратная совместимость: раньше pr.repo ВСЕГДА был локальным путём).
7066
+ *
7067
+ * РЕГИСТР НЕ РАЗЛИЧАЕМ (TASWM-422). Ключи карты приведены к нижнему регистру
7068
+ * (parseRepoFullName — «GitHub сам не различает case в owner/repo»), а прод присылает
7069
+ * `run.repo` тем регистром, каким репозиторий записан в проекте. 17.08 это разошлось:
7070
+ * машина объявляла `ilyasmurov/ansamblist`, задача пришла с `ilyasmurov/Ansamblist`,
7071
+ * `Map.get` промахнулся — и вместо пути в прогон уехало полное имя репозитория. Дальше
7072
+ * git-операции шли в несуществующий каталог, прогон умирал за 5 секунд, а задача
7073
+ * возвращалась в очередь и бралась снова: пять павших прогонов за пять минут.
7074
+ * Возвращаем ИСХОДНУЮ строку, а не приведённую: промах бывает и на локальном пути
7075
+ * (ручной спавн), а путь регистр различает — вернув lowercase, мы сломали бы его.
6813
7076
  */
6814
7077
  resolveRepo(raw) {
6815
7078
  if (!raw) return "";
6816
- return this.repoPathByFullName.get(raw) ?? raw;
7079
+ return this.repoPathByFullName.get(raw) ?? this.repoPathByFullName.get(raw.toLowerCase()) ?? raw;
6817
7080
  }
6818
7081
  /**
6819
7082
  * TASWM-281: МОЯ ли это работа. Три исхода:
@@ -6889,6 +7152,8 @@ var SwarmClient = class _SwarmClient {
6889
7152
  // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
6890
7153
  // в отличие от respawns, — авто-ревью v2, TASWM-162.
6891
7154
  review_cycle: 0,
7155
+ // TASWM-451: модель ПРОВЕРКИ — прод-поле, заполняется ниже из ответа (как base_branch).
7156
+ review_model: null,
6892
7157
  task_title: null,
6893
7158
  task_desc: null,
6894
7159
  task_url: null,
@@ -6941,6 +7206,7 @@ var SwarmClient = class _SwarmClient {
6941
7206
  local.area = pr.area ?? null;
6942
7207
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
6943
7208
  local.model = pr.model ?? null;
7209
+ local.review_model = pr.reviewModel ?? null;
6944
7210
  local.effort = toEffortLevel(pr.effort);
6945
7211
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
6946
7212
  local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
@@ -7408,20 +7674,20 @@ function tailFile(path, lines) {
7408
7674
  }
7409
7675
 
7410
7676
  // src/logRotate.ts
7411
- import { copyFileSync, renameSync as renameSync2, statSync as statSync4, fstatSync, truncateSync, unlinkSync, existsSync as existsSync5 } from "fs";
7677
+ import { copyFileSync, renameSync as renameSync3, statSync as statSync5, fstatSync, truncateSync, unlinkSync, existsSync as existsSync6 } from "fs";
7412
7678
  var realIo = {
7413
7679
  size: (path) => {
7414
7680
  try {
7415
- return statSync4(path).size;
7681
+ return statSync5(path).size;
7416
7682
  } catch {
7417
7683
  return null;
7418
7684
  }
7419
7685
  },
7420
7686
  copy: copyFileSync,
7421
- rename: renameSync2,
7687
+ rename: renameSync3,
7422
7688
  truncate: (path) => truncateSync(path, 0),
7423
7689
  unlink: unlinkSync,
7424
- exists: existsSync5
7690
+ exists: existsSync6
7425
7691
  };
7426
7692
  var STRAY_GENERATIONS = 8;
7427
7693
  function rotateDaemonLog(path, opts, io = realIo) {
@@ -7679,18 +7945,18 @@ init_repoRegistry();
7679
7945
 
7680
7946
  // src/hostIdentity.ts
7681
7947
  import { hostname } from "os";
7682
- import { existsSync as existsSync6, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
7683
- import { join as join6 } from "path";
7948
+ import { existsSync as existsSync7, readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "fs";
7949
+ import { join as join7 } from "path";
7684
7950
  import { randomUUID as randomUUID2 } from "crypto";
7685
7951
  function hostIdentityPath(home) {
7686
- return join6(home, "host.json");
7952
+ return join7(home, "host.json");
7687
7953
  }
7688
7954
  function loadHostIdentity(deps) {
7689
- const exists = deps.exists ?? existsSync6;
7955
+ const exists = deps.exists ?? existsSync7;
7690
7956
  const readFile2 = deps.readFile ?? ((p) => readFileSync5(p, "utf8"));
7691
7957
  const writeFile3 = deps.writeFile ?? ((p, data) => {
7692
7958
  writeFileSync4(`${p}.tmp`, data);
7693
- renameSync3(`${p}.tmp`, p);
7959
+ renameSync4(`${p}.tmp`, p);
7694
7960
  });
7695
7961
  const host = deps.hostname ?? hostname;
7696
7962
  const newId = deps.newId ?? randomUUID2;
@@ -7804,8 +8070,8 @@ import { basename as basename2 } from "path";
7804
8070
  import { serve as serve2 } from "@hono/node-server";
7805
8071
 
7806
8072
  // src/diff.ts
7807
- import { statSync as statSync5 } from "fs";
7808
- import { join as join7 } from "path";
8073
+ import { statSync as statSync6 } from "fs";
8074
+ import { join as join8 } from "path";
7809
8075
  import { $ as $3 } from "execa";
7810
8076
  var MAX_FILES = 200;
7811
8077
  var MAX_LINES_PER_FILE = 1500;
@@ -7948,7 +8214,7 @@ async function resolveBase(wt, baseBranch) {
7948
8214
  }
7949
8215
  function mtime(path) {
7950
8216
  try {
7951
- return statSync5(path).mtimeMs;
8217
+ return statSync6(path).mtimeMs;
7952
8218
  } catch {
7953
8219
  return 0;
7954
8220
  }
@@ -7973,7 +8239,7 @@ async function collectRunDiff(worktree, opts = {}) {
7973
8239
  files.push(pf);
7974
8240
  }
7975
8241
  }
7976
- files.sort((a, b) => mtime(join7(worktree, b.path)) - mtime(join7(worktree, a.path)));
8242
+ files.sort((a, b) => mtime(join8(worktree, b.path)) - mtime(join8(worktree, a.path)));
7977
8243
  const truncatedFiles = files.length > MAX_FILES;
7978
8244
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
7979
8245
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -8126,13 +8392,13 @@ function deniedSinceLine(nowMs) {
8126
8392
  init_config();
8127
8393
  import { createHash, randomBytes as randomBytes2 } from "crypto";
8128
8394
  import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
8129
- import { join as join9 } from "path";
8395
+ import { join as join10 } from "path";
8130
8396
  var REGISTER = "/api/oauth/register";
8131
8397
  var AUTHORIZE = "/api/oauth/authorize";
8132
8398
  var TOKEN = "/api/oauth/token";
8133
8399
  var SCOPE = "openid profile email taskless:full";
8134
- var STORE_PATH2 = join9(SWARM_HOME, "oauth.json");
8135
- var CLIENT_PATH = join9(SWARM_HOME, "oauth-client.json");
8400
+ var STORE_PATH2 = join10(SWARM_HOME, "oauth.json");
8401
+ var CLIENT_PATH = join10(SWARM_HOME, "oauth-client.json");
8136
8402
  function makePkce() {
8137
8403
  const verifier = randomBytes2(48).toString("base64url");
8138
8404
  const challenge = createHash("sha256").update(verifier).digest("base64url");
@@ -9859,15 +10125,15 @@ import { cancelRun, respawnRun, runDaemon, wakeRun, OVERMIND_ENTRY, listDronePro
9859
10125
 
9860
10126
  // src/launcher.ts
9861
10127
  import { spawn as spawn2 } from "child_process";
9862
- import { closeSync as closeSync3, existsSync as existsSync7, mkdirSync as mkdirSync7, openSync as openSync3, readdirSync as readdirSync6 } from "fs";
10128
+ import { closeSync as closeSync3, existsSync as existsSync8, mkdirSync as mkdirSync8, openSync as openSync3, readdirSync as readdirSync7 } from "fs";
9863
10129
  import { createRequire } from "module";
9864
10130
  import { homedir as homedir3 } from "os";
9865
- import { dirname as dirname3, join as join11 } from "path";
10131
+ import { dirname as dirname3, join as join12 } from "path";
9866
10132
  import { fileURLToPath } from "url";
9867
10133
 
9868
10134
  // src/droneHome.ts
9869
- import { lstatSync, mkdirSync as mkdirSync6, readdirSync as readdirSync5, rmSync, statSync as statSync6, symlinkSync as symlinkSync2 } from "fs";
9870
- import { join as join10, relative, sep } from "path";
10135
+ import { lstatSync, mkdirSync as mkdirSync7, readdirSync as readdirSync6, rmSync, statSync as statSync7, symlinkSync as symlinkSync2 } from "fs";
10136
+ import { join as join11, relative, sep } from "path";
9871
10137
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
9872
10138
  function droneHomeDeny(env = process.env) {
9873
10139
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -9885,29 +10151,29 @@ function buildDroneHome(opts) {
9885
10151
  const { realHome, droneHome } = opts;
9886
10152
  const deny = opts.deny ?? droneHomeDeny();
9887
10153
  const platform = opts.platform ?? process.platform;
9888
- mkdirSync6(droneHome, { recursive: true, mode: 448 });
9889
- const wanted = new Set(mirroredEntries(readdirSync5(realHome), deny, platform));
10154
+ mkdirSync7(droneHome, { recursive: true, mode: 448 });
10155
+ const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
9890
10156
  const rel = relative(realHome, droneHome);
9891
10157
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
9892
- for (const existing of readdirSync5(droneHome)) {
10158
+ for (const existing of readdirSync6(droneHome)) {
9893
10159
  if (wanted.has(existing)) continue;
9894
- const p = join10(droneHome, existing);
10160
+ const p = join11(droneHome, existing);
9895
10161
  try {
9896
10162
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
9897
10163
  } catch {
9898
10164
  }
9899
10165
  }
9900
10166
  for (const name of wanted) {
9901
- const link = join10(droneHome, name);
10167
+ const link = join11(droneHome, name);
9902
10168
  try {
9903
10169
  lstatSync(link);
9904
10170
  continue;
9905
10171
  } catch {
9906
10172
  }
9907
- const target = join10(realHome, name);
10173
+ const target = join11(realHome, name);
9908
10174
  let isDir;
9909
10175
  try {
9910
- isDir = statSync6(target).isDirectory();
10176
+ isDir = statSync7(target).isDirectory();
9911
10177
  } catch {
9912
10178
  continue;
9913
10179
  }
@@ -9925,7 +10191,7 @@ var REAL_PATH_ENV_PINS = [
9925
10191
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
9926
10192
  ];
9927
10193
  function realPathEnvPins(realHome) {
9928
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join10(realHome, ...segments)]));
10194
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join11(realHome, ...segments)]));
9929
10195
  }
9930
10196
  function dronePrivatePaths(realHome, swarmHome) {
9931
10197
  return [
@@ -9985,12 +10251,26 @@ function pick(v) {
9985
10251
  const s = typeof v === "string" ? v.trim() : "";
9986
10252
  return s.length > 0 ? s : null;
9987
10253
  }
10254
+ function isReviewKind(kind) {
10255
+ return kind === "reviewer" || kind === "verifier";
10256
+ }
10257
+ function modelRoleOf(kind) {
10258
+ return isReviewKind(kind) ? "review" : "executor";
10259
+ }
9988
10260
  function forKind(kind, swarm, norm) {
9989
10261
  if (!swarm) return null;
9990
- return kind === "reviewer" ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
10262
+ return isReviewKind(kind) ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
10263
+ }
10264
+ function resolveRunModelRef(run, swarm, env = process.env) {
10265
+ const configured = parseModelRef(
10266
+ pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL)
10267
+ );
10268
+ if (configured) return configured;
10269
+ const providerId = pick(env.SWARM_MODEL_PROVIDER) ?? DEFAULT_PROVIDER_ID;
10270
+ return { providerId, model: defaultModelFor(modelRoleOf(run.kind), providerId) };
9991
10271
  }
9992
10272
  function resolveRunModel(run, swarm, env = process.env) {
9993
- return pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL) ?? null;
10273
+ return resolveRunModelRef(run, swarm, env).model;
9994
10274
  }
9995
10275
  function resolveRunEffort(run, swarm, env = process.env) {
9996
10276
  return toEffortLevel(run.effort) ?? forKind(run.kind, swarm, toEffortLevel) ?? toEffortLevel(env.SWARM_DRONE_EFFORT) ?? null;
@@ -10023,7 +10303,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
10023
10303
  const env = { ...base3 };
10024
10304
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
10025
10305
  if (isolate && !base3.TURBO_CACHE_DIR) {
10026
- env.TURBO_CACHE_DIR = join11(worktree, ".turbo", "cache");
10306
+ env.TURBO_CACHE_DIR = join12(worktree, ".turbo", "cache");
10027
10307
  }
10028
10308
  Object.assign(env, droneTestEnv(droneCap, base3));
10029
10309
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -10051,20 +10331,21 @@ var DAEMON_STATE_ENV = [
10051
10331
  "TASKLESS_TOKEN"
10052
10332
  ];
10053
10333
  function bundledRoleDir(kind) {
10054
- return join11(dirname3(fileURLToPath(import.meta.url)), "..", "roles", kind);
10334
+ return join12(dirname3(fileURLToPath(import.meta.url)), "..", "roles", kind);
10055
10335
  }
10056
10336
  function roleDir(worktree, kind) {
10057
- return join11(worktree, ".claude", "roles", kind);
10337
+ return join12(worktree, ".claude", "roles", kind);
10058
10338
  }
10059
10339
  function roleDirs(worktree, kind) {
10060
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync7(dir));
10340
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync8(dir));
10061
10341
  }
10062
- function nextRunLogPath(runLogsDir, runId) {
10063
- const dir = join11(runLogsDir, String(runId));
10064
- mkdirSync7(dir, { recursive: true });
10342
+ function nextRunLogPath(runLogsDir, runLogKeys) {
10343
+ const dir = runLogDirForWrite(runLogsDir, runLogKeys);
10344
+ if (!dir) throw new Error("\u043D\u0435\u0447\u0435\u043C \u0430\u0434\u0440\u0435\u0441\u043E\u0432\u0430\u0442\u044C \u043A\u0430\u0442\u0430\u043B\u043E\u0433 run-log'\u043E\u0432: \u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435\u0442 \u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u043E\u0433\u043E \u043A\u043B\u044E\u0447\u0430 (SwarmStore.runLogKeys)");
10345
+ mkdirSync8(dir, { recursive: true });
10065
10346
  let files = [];
10066
10347
  try {
10067
- files = readdirSync6(dir);
10348
+ files = readdirSync7(dir);
10068
10349
  } catch {
10069
10350
  }
10070
10351
  let next = 0;
@@ -10072,11 +10353,11 @@ function nextRunLogPath(runLogsDir, runId) {
10072
10353
  const m = /^(\d+)\.json/.exec(f);
10073
10354
  if (m) next = Math.max(next, Number(m[1]) + 1);
10074
10355
  }
10075
- return join11(dir, `${next}.json`);
10356
+ return join12(dir, `${next}.json`);
10076
10357
  }
10077
- function openNextRunLog(runLogsDir, runId) {
10358
+ function openNextRunLog(runLogsDir, runLogKeys) {
10078
10359
  for (let attempt = 0; attempt < 50; attempt++) {
10079
- const path = nextRunLogPath(runLogsDir, runId);
10360
+ const path = nextRunLogPath(runLogsDir, runLogKeys);
10080
10361
  let out;
10081
10362
  try {
10082
10363
  out = openSync3(path, "wx");
@@ -10092,7 +10373,7 @@ function openNextRunLog(runLogsDir, runId) {
10092
10373
  }
10093
10374
  return { out, err };
10094
10375
  }
10095
- throw new Error(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0439 run-log \u0444\u0430\u0439\u043B \u0434\u043B\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${runId} (\u0433\u043E\u043D\u043A\u0430?)`);
10376
+ throw new Error(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0439 run-log \u0444\u0430\u0439\u043B \u043F\u0440\u043E\u0433\u043E\u043D\u0430 (${runLogKeys[0]}) \u2014 \u0433\u043E\u043D\u043A\u0430?`);
10096
10377
  }
10097
10378
  function makeLauncher(cfg) {
10098
10379
  return ({ worktree, run, prompt, continueSession }) => {
@@ -10155,9 +10436,9 @@ function makeLauncher(cfg) {
10155
10436
  }
10156
10437
  const realHome = cfg.droneIsolation?.realHome ?? homedir3();
10157
10438
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
10158
- if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync7(join11(cfg.droneIsolation.swarmHome, "token"))) {
10439
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join12(cfg.droneIsolation.swarmHome, "token"))) {
10159
10440
  console.error(
10160
- `[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${join11(cfg.droneIsolation.swarmHome, "token")} \u043D\u0435\u0442. read_task \u0434\u0440\u043E\u043D\u0430 \u0431\u0443\u0434\u0435\u0442 slug-only. \u041F\u043E\u0447\u0438\u043D\u0438: \`swarm login\` (oauth.json) \u0438\u043B\u0438 \u043F\u043E\u043B\u043E\u0436\u0438 \u0442\u043E\u043A\u0435\u043D \u0432 \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B.`
10441
+ `[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${join12(cfg.droneIsolation.swarmHome, "token")} \u043D\u0435\u0442. read_task \u0434\u0440\u043E\u043D\u0430 \u0431\u0443\u0434\u0435\u0442 slug-only. \u041F\u043E\u0447\u0438\u043D\u0438: \`swarm login\` (oauth.json) \u0438\u043B\u0438 \u043F\u043E\u043B\u043E\u0436\u0438 \u0442\u043E\u043A\u0435\u043D \u0432 \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B.`
10161
10442
  );
10162
10443
  }
10163
10444
  const executor = cfg.executor ?? resolveExecutor();
@@ -10184,7 +10465,13 @@ function makeLauncher(cfg) {
10184
10465
  let outFd;
10185
10466
  let errFd;
10186
10467
  if (cfg.runLogsDir) {
10187
- const fds = openNextRunLog(cfg.runLogsDir, run.id);
10468
+ const logKeys = cfg.runLogKeys?.(run) ?? [];
10469
+ if (logKeys.length === 0) {
10470
+ throw new Error(
10471
+ `\u043F\u0440\u043E\u0433\u043E\u043D\u0443 #${run.id} (${run.task_slug}) \u043D\u0435\u0447\u0435\u043C \u0430\u0434\u0440\u0435\u0441\u043E\u0432\u0430\u0442\u044C \u043A\u0430\u0442\u0430\u043B\u043E\u0433 run-log'\u043E\u0432: \u0443 \u0441\u0442\u043E\u0440\u0430 \u043D\u0435\u0442 \u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u043E\u0433\u043E \u043A\u043B\u044E\u0447\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430. \u0414\u0440\u043E\u043D \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0435\u043D \u2014 \u0438\u043D\u0430\u0447\u0435 \u0435\u0433\u043E \u043B\u043E\u0433 \u043B\u0451\u0433 \u0431\u044B \u0432 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F \u043A\u0430\u043A \u0447\u0443\u0436\u043E\u0439 (TASWM-419).`
10472
+ );
10473
+ }
10474
+ const fds = openNextRunLog(cfg.runLogsDir, logKeys);
10188
10475
  outFd = fds.out;
10189
10476
  errFd = fds.err ?? void 0;
10190
10477
  stdio = ["ignore", outFd, errFd ?? "ignore"];
@@ -10212,14 +10499,14 @@ function makeLauncher(cfg) {
10212
10499
  init_droneChannel();
10213
10500
 
10214
10501
  // src/git.ts
10215
- import { existsSync as existsSync8, readdirSync as readdirSync7, statSync as statSync7 } from "fs";
10502
+ import { existsSync as existsSync9, readdirSync as readdirSync8, statSync as statSync8 } from "fs";
10216
10503
  import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
10217
- import { isAbsolute, join as join12, sep as sep2 } from "path";
10504
+ import { isAbsolute, join as join13, sep as sep2 } from "path";
10218
10505
  import { $ as $5 } from "execa";
10219
10506
  init_swarmFiles();
10220
10507
  function isLinkedWorktree(dir) {
10221
10508
  try {
10222
- return statSync7(join12(dir, ".git")).isFile();
10509
+ return statSync8(join13(dir, ".git")).isFile();
10223
10510
  } catch {
10224
10511
  return false;
10225
10512
  }
@@ -10231,7 +10518,7 @@ async function detachWorktree(repo, worktree) {
10231
10518
  }
10232
10519
  }
10233
10520
  async function ensureWorktreeDirGone(worktree) {
10234
- if (!existsSync8(worktree)) return false;
10521
+ if (!existsSync9(worktree)) return false;
10235
10522
  if (!isLinkedWorktree(worktree)) {
10236
10523
  console.error(
10237
10524
  `[git] ${worktree} \u2014 \u043D\u0435 \u043F\u0440\u0438\u0441\u0442\u0451\u0433\u043D\u0443\u0442\u044B\u0439 worktree (.git \u043D\u0435 \u0444\u0430\u0439\u043B): \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u041D\u0415 \u0442\u0440\u043E\u0433\u0430\u044E. \u0415\u0441\u043B\u0438 \u044D\u0442\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E worktree \u0434\u0440\u043E\u043D\u0430 \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043E \u0440\u0443\u043A\u0430\u043C\u0438; \u0435\u0441\u043B\u0438 \u044D\u0442\u043E \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u0447\u0435\u043A\u0430\u0443\u0442, \u0437\u043D\u0430\u0447\u0438\u0442 \u0432\u0435\u0442\u043A\u0430 \u0441\u043B\u0430\u0433\u0430 \u0447\u0435\u043A\u043D\u0443\u0442\u0430 \u0432 \u043D\u0451\u043C (git checkout <slug>) \u2014 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0435\u0433\u043E \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u0432\u0435\u0442\u043A\u0443.`
@@ -10246,7 +10533,7 @@ async function ensureWorktreeDirGone(worktree) {
10246
10533
  );
10247
10534
  return true;
10248
10535
  }
10249
- if (existsSync8(worktree)) {
10536
+ if (existsSync9(worktree)) {
10250
10537
  console.error(`[git] \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree ${worktree} \u043F\u0435\u0440\u0435\u0436\u0438\u043B rm \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u043A\u0430\u043C\u0438, \u0438\u043D\u0430\u0447\u0435 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u0441\u043B\u0430\u0433\u0430 \u0432\u0441\u0442\u0430\u043D\u0435\u0442.`);
10251
10538
  }
10252
10539
  return true;
@@ -10274,7 +10561,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
10274
10561
 
10275
10562
  // src/attachments.ts
10276
10563
  import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
10277
- import { join as join13 } from "path";
10564
+ import { join as join14 } from "path";
10278
10565
  init_swarmFiles();
10279
10566
  var ATTACHMENTS_DIR = `${SWARM_TASK_DIR}/attachments`;
10280
10567
  var ATTACH_MAX_TOTAL_BYTES = 24 * 1024 * 1024;
@@ -10286,7 +10573,7 @@ var KNOWN_EXT = /* @__PURE__ */ new Set([...IMAGE_EXT, ...DOC_EXT, ...TEXT_EXT])
10286
10573
 
10287
10574
  // src/shotUpload.ts
10288
10575
  import { readFile as fsReadFile } from "fs/promises";
10289
- import { basename as basename3, isAbsolute as isAbsolute2, join as join14 } from "path";
10576
+ import { basename as basename3, isAbsolute as isAbsolute2, join as join15 } from "path";
10290
10577
  var SHOT_MAX_FILE_BYTES = 10 * 1024 * 1024;
10291
10578
 
10292
10579
  // src/taskless.ts
@@ -10297,8 +10584,8 @@ init_linkFailure();
10297
10584
  // src/workspace.ts
10298
10585
  init_config();
10299
10586
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4 } from "fs";
10300
- import { join as join15 } from "path";
10301
- var WORKSPACE_PATH = join15(SWARM_HOME, "workspace.json");
10587
+ import { join as join16 } from "path";
10588
+ var WORKSPACE_PATH = join16(SWARM_HOME, "workspace.json");
10302
10589
  function loadWorkspace() {
10303
10590
  try {
10304
10591
  const w = JSON.parse(readFileSync9(WORKSPACE_PATH, "utf8"));
@@ -11189,7 +11476,7 @@ async function runDaemonLogs(argv) {
11189
11476
  console.error(`\u043B\u043E\u0433 \u041F\u0420\u041E\u0413\u041E\u041D\u0410 \u2014 \u044D\u0442\u043E \xABswarm log ${stray}\xBB (\u0431\u0435\u0437 s). \xABswarm logs\xBB \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442 \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430; \u0447\u0438\u0441\u043B\u043E \u0441\u0442\u0440\u043E\u043A \u2014 \xABswarm logs -n 200\xBB.`);
11190
11477
  return 1;
11191
11478
  }
11192
- if (!existsSync15(DAEMON_LOG_PATH)) {
11479
+ if (!existsSync16(DAEMON_LOG_PATH)) {
11193
11480
  console.error(`\u043B\u043E\u0433\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442: ${DAEMON_LOG_PATH}`);
11194
11481
  console.error("\u043E\u043D \u043F\u0438\u0448\u0435\u0442\u0441\u044F \u043F\u0440\u0438 \u0444\u043E\u043D\u043E\u0432\u043E\u043C \u0437\u0430\u043F\u0443\u0441\u043A\u0435 (swarm start). \u0414\u0435\u043C\u043E\u043D \u043F\u043E\u0434\u043D\u044F\u0442 \u0432 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435 (--foreground) \u2014 \u0432\u044B\u0432\u043E\u0434 \u0438\u0434\u0451\u0442 \u0442\u0443\u0434\u0430.");
11195
11482
  return 1;
@@ -11197,12 +11484,12 @@ async function runDaemonLogs(argv) {
11197
11484
  const head = tailFile(DAEMON_LOG_PATH, lines);
11198
11485
  if (head) console.log(head);
11199
11486
  if (!follow) return 0;
11200
- let pos = statSync9(DAEMON_LOG_PATH).size;
11487
+ let pos = statSync10(DAEMON_LOG_PATH).size;
11201
11488
  for (; ; ) {
11202
11489
  await sleep2(500);
11203
11490
  let size;
11204
11491
  try {
11205
- size = statSync9(DAEMON_LOG_PATH).size;
11492
+ size = statSync10(DAEMON_LOG_PATH).size;
11206
11493
  } catch {
11207
11494
  continue;
11208
11495
  }
@@ -11461,10 +11748,10 @@ function effectiveWorktreesDir() {
11461
11748
  legacyDir: WORKTREES_LEGACY_DIR,
11462
11749
  targetDir: WORKTREES_DIR,
11463
11750
  envOverride: process.env.SWARM_WORKTREES || "",
11464
- exists: (d) => existsSync15(d),
11751
+ exists: (d) => existsSync16(d),
11465
11752
  isNonEmptyDir: (d) => {
11466
11753
  try {
11467
- return readdirSync9(d).length > 0;
11754
+ return readdirSync10(d).length > 0;
11468
11755
  } catch {
11469
11756
  return false;
11470
11757
  }
@@ -11501,7 +11788,7 @@ async function runScoutCommand(repo) {
11501
11788
  });
11502
11789
  const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo, autoFlush: false });
11503
11790
  const name = pickName([]);
11504
- const outFile = join23(SWARM_HOME, `scout-${repoFullName.replace(/[^\w.-]+/g, "-")}.json`);
11791
+ const outFile = join24(SWARM_HOME, `scout-${repoFullName.replace(/[^\w.-]+/g, "-")}.json`);
11505
11792
  const bin = resolveExecutor().resolveBin();
11506
11793
  const code = await runRepoScout({
11507
11794
  repoFullName,
@@ -11521,7 +11808,7 @@ async function runScoutCommand(repo) {
11521
11808
  });
11522
11809
  child.on("exit", (c) => resolve4(c ?? 1));
11523
11810
  }),
11524
- readOut: () => existsSync15(outFile) ? readFileSync17(outFile, "utf8") : null,
11811
+ readOut: () => existsSync16(outFile) ? readFileSync17(outFile, "utf8") : null,
11525
11812
  clearOut: () => rmSync2(outFile, { force: true }),
11526
11813
  report: (report) => client.reportRepoScout(repoFullName, report, name),
11527
11814
  log: (line) => console.log(line)
@@ -11597,7 +11884,7 @@ async function runZones(db2, repo, args) {
11597
11884
  }
11598
11885
  if (model.spine.length) {
11599
11886
  console.log(`
11600
- \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${model.spine.length} \u0444\u0430\u0439\u043B\u043E\u0432) \u2014 \u0437\u0430\u0434\u0430\u0447\u0438 \u043F\u0440\u043E \u043D\u0435\u0433\u043E \u0438\u0434\u0443\u0442 \u043F\u043E\u0441\u043B\u0435\u0434\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u0437\u043E\u043D\u044B:`);
11887
+ \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${model.spine.length} \u0444\u0430\u0439\u043B\u043E\u0432) \u2014 \u0434\u0432\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u041E\u0414\u041D\u041E\u041C \u0442\u0430\u043A\u043E\u043C \u0444\u0430\u0439\u043B\u0435 \u0438\u0434\u0443\u0442 \u043F\u043E\u0441\u043B\u0435\u0434\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u0437\u043E\u043D\u044B:`);
11601
11888
  for (const f of model.spine.slice(0, 15)) console.log(` ${f}`);
11602
11889
  }
11603
11890
  console.log("\n\u0441\u0432\u0435\u0440\u0438\u0442\u044C \u0441 \u0440\u0443\u0447\u043D\u043E\u0439 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u043E\u0439: swarm zones check");
@@ -11918,10 +12205,19 @@ switch (cmd) {
11918
12205
  overmindEntry: OVERMIND_ENTRY,
11919
12206
  dbPath: DB_PATH,
11920
12207
  runLogsDir: RUN_LOGS_DIR,
12208
+ // TASWM-419: каталог лога — по стабильному ключу прогона (тот же адрес, что у демона).
12209
+ runLogKeys: (r) => db.runLogKeys(r.id),
11921
12210
  // Тот же исполнитель, что у демона (TASWM-349): `swarm wake` при мёртвом демоне не
11922
12211
  // должен внезапно поднимать дрона другим харнессом.
11923
12212
  executor: EXECUTOR,
11924
12213
  droneIsolation: DRONE_ISOLATION,
12214
+ // TASWM-450: ПОТОЛОК ТРАТ обязателен и здесь. Демон собирает лаунчер с
12215
+ // `maxCostUsd: () => lastMaxCostUsd`, а этот путь его не передавал вовсе — значит
12216
+ // разбуженный руками прогон уходил БЕЗ `--max-budget-usd` и мог жечь сколько угодно.
12217
+ // Живой настройки роя у CLI нет (демон не отвечает — иначе мы сюда не попали бы),
12218
+ // поэтому источник тот же, что и у остальных ~40 ручек: env машины, а за ним дефолт
12219
+ // ядра. Это ровно то, что делает `launchBudgetUsd(null, …)`.
12220
+ maxCostUsd: () => null,
11925
12221
  ...DRONE_CHANNEL_ENABLED ? {
11926
12222
  channel: {
11927
12223
  url: DRONE_CHANNEL_URL,