@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/overmind.js CHANGED
@@ -317,21 +317,28 @@ function computeReviewPhase(input) {
317
317
  return { kind: "reviewer_dead", label: REVIEW_PHASE_TEXT.reviewer_dead, ready: true };
318
318
  }
319
319
  const p = input.ciProgress;
320
+ if (p && p.total > 0 && p.pass >= p.total) {
321
+ return { kind: "awaiting_reviewer", label: REVIEW_PHASE_TEXT.awaiting_reviewer, ready: false };
322
+ }
320
323
  const label = p && p.total > 0 ? `${REVIEW_PHASE_TEXT.checks} \u2014 CI ${p.pass}/${p.total}` : REVIEW_PHASE_TEXT.checks;
321
324
  return { kind: "checks", label, ready: false };
322
325
  }
323
- var REVIEW_PHASE_TEXT;
326
+ var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
324
327
  var init_reviewPhase = __esm({
325
328
  "../swarm-core/src/reviewPhase.ts"() {
326
329
  "use strict";
327
330
  REVIEW_PHASE_TEXT = {
328
331
  checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
332
+ awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
329
333
  ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
330
334
  in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
331
335
  changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
332
336
  ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
333
337
  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"
334
338
  };
339
+ PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
340
+ (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
341
+ );
335
342
  }
336
343
  });
337
344
 
@@ -425,6 +432,40 @@ var init_mergeGate = __esm({
425
432
  }
426
433
  });
427
434
 
435
+ // ../swarm-core/src/modelPolicy.ts
436
+ function findProvider(id) {
437
+ const key = typeof id === "string" ? id.trim().toLowerCase() : "";
438
+ return MODEL_PROVIDERS.find((p) => p.id === key) ?? null;
439
+ }
440
+ function parseModelRef(ref) {
441
+ const s = typeof ref === "string" ? ref.trim() : "";
442
+ if (!s) return null;
443
+ const slash = s.indexOf("/");
444
+ if (slash < 0) return { providerId: DEFAULT_PROVIDER_ID, model: s };
445
+ const providerId = s.slice(0, slash).trim().toLowerCase();
446
+ const model = s.slice(slash + 1).trim();
447
+ if (!model) return null;
448
+ return { providerId: providerId || DEFAULT_PROVIDER_ID, model };
449
+ }
450
+ function defaultModelFor(role, providerId = null) {
451
+ const provider = findProvider(providerId) ?? findProvider(DEFAULT_PROVIDER_ID);
452
+ return provider.roles[role];
453
+ }
454
+ var MODEL_PROVIDERS, DEFAULT_PROVIDER_ID;
455
+ var init_modelPolicy = __esm({
456
+ "../swarm-core/src/modelPolicy.ts"() {
457
+ "use strict";
458
+ MODEL_PROVIDERS = [
459
+ {
460
+ id: "anthropic",
461
+ label: "Anthropic",
462
+ roles: { executor: "claude-opus-5", review: "claude-sonnet-5" }
463
+ }
464
+ ];
465
+ DEFAULT_PROVIDER_ID = MODEL_PROVIDERS[0].id;
466
+ }
467
+ });
468
+
428
469
  // ../swarm-core/src/status.ts
429
470
  function isAwaitingCi(status, stopReason) {
430
471
  return status === "paused" && stopReason === AWAITING_CI_REASON;
@@ -477,7 +518,7 @@ var init_status = __esm({
477
518
  // ../swarm-core/src/deps.ts
478
519
  function judgeDep(runs) {
479
520
  const real = runs.filter((r) => !r.service);
480
- if (real.length === 0) return { ...PENDING };
521
+ if (real.length === 0) return { ...UNKNOWN };
481
522
  if (real.some((r) => !isTerminalStatus(r.status))) return { ...PENDING };
482
523
  if (real.some((r) => r.status === "done")) {
483
524
  return { outcome: "done", met: true, status: "done", name: null, stopReason: null };
@@ -497,12 +538,13 @@ function depClearedText(slug, v) {
497
538
  const why = v.stopReason ? ` \u2014 ${v.stopReason}` : "";
498
539
  return `\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C \u0441\u043D\u044F\u0442\u0430: \u0436\u0434\u0430\u043B ${who}, \u0442\u043E\u0442 ${how}${why}. \u0423\u0441\u043F\u0435\u0448\u043D\u043E\u0433\u043E \u0444\u0438\u043D\u0430\u043B\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u2014 \u0435\u0434\u0443.`;
499
540
  }
500
- var PENDING, DEP_END_LABEL;
541
+ var PENDING, UNKNOWN, DEP_END_LABEL;
501
542
  var init_deps = __esm({
502
543
  "../swarm-core/src/deps.ts"() {
503
544
  "use strict";
504
545
  init_status();
505
546
  PENDING = { outcome: "pending", met: false, status: null, name: null, stopReason: null };
547
+ UNKNOWN = { outcome: "unknown", met: false, status: null, name: null, stopReason: null };
506
548
  DEP_END_LABEL = {
507
549
  stopped: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C",
508
550
  failed: "\u0443\u043F\u0430\u043B",
@@ -521,6 +563,9 @@ function stampFromEpochSec(sec) {
521
563
  function sqliteMs(ts) {
522
564
  return Date.parse(ts.replace(" ", "T") + "Z");
523
565
  }
566
+ function stampMs(at) {
567
+ return at.includes("T") ? Date.parse(at) : sqliteMs(at);
568
+ }
524
569
  function toStamp(v) {
525
570
  if (typeof v !== "string" || !v) return null;
526
571
  const d = new Date(v);
@@ -989,6 +1034,42 @@ var init_cost = __esm({
989
1034
  }
990
1035
  });
991
1036
 
1037
+ // ../swarm-core/src/modelPricing.ts
1038
+ function findModelPricing(model, table = MODEL_PRICING) {
1039
+ if (!model) return void 0;
1040
+ const exact = table[model];
1041
+ if (exact) return exact;
1042
+ let bestKey = "";
1043
+ for (const key of Object.keys(table)) {
1044
+ if (model.startsWith(key) && key.length > bestKey.length) bestKey = key;
1045
+ }
1046
+ return bestKey ? table[bestKey] : void 0;
1047
+ }
1048
+ var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
1049
+ var init_modelPricing = __esm({
1050
+ "../swarm-core/src/modelPricing.ts"() {
1051
+ "use strict";
1052
+ MODEL_PRICING = {
1053
+ // Sonnet 5 — 2/10. Цена ПОСТОЯННАЯ: 18.08.2026 владелец подтвердил, что вводной
1054
+ // она быть перестала. Даты годности у тарифа тут больше нет и заводить её заново не
1055
+ // нужно: тариф со сроком означает, что в календарный день расчёт молча меняет цифру,
1056
+ // и «почему прогон вчера стоил иначе» приходится выяснять по коду, а не по таблице.
1057
+ // Меняется цена — меняется строка, одним видимым коммитом.
1058
+ "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
1059
+ "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
1060
+ "claude-fable-5": { inputPerMTok: 10, outputPerMTok: 50 },
1061
+ // Прогоны и вызовы на 4.x остаются в истории — строки держим ради их стоимости.
1062
+ "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
1063
+ "claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25 },
1064
+ "claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25 },
1065
+ "claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
1066
+ "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
1067
+ };
1068
+ CACHE_READ_FACTOR = 0.1;
1069
+ CACHE_WRITE_5M_FACTOR = 1.25;
1070
+ }
1071
+ });
1072
+
992
1073
  // ../swarm-core/src/runGuards.ts
993
1074
  function remainingRunBudgetUsd(maxCostUsd, spentUsd) {
994
1075
  if (!(maxCostUsd > 0)) return null;
@@ -1193,31 +1274,6 @@ function buildZoneModel(entries, opts = {}) {
1193
1274
  const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
1194
1275
  return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
1195
1276
  }
1196
- function zoneForFiles(model, files) {
1197
- if (!hasZoneModel(model) || !files.length) return null;
1198
- const spineSet = new Set(model.spine);
1199
- const touchedSpine = files.filter((f) => spineSet.has(f));
1200
- const votes = /* @__PURE__ */ new Map();
1201
- for (const f of new Set(files)) {
1202
- const z2 = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
1203
- if (z2) bump(votes, z2);
1204
- }
1205
- const top = topKey(votes);
1206
- if (!top) {
1207
- return {
1208
- zone: SPINE_ZONE,
1209
- confidence: 1,
1210
- why: `\u0432\u0441\u0435 \u0444\u0430\u0439\u043B\u044B \u2014 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${touchedSpine.slice(0, 3).join(", ")})`,
1211
- spine: true
1212
- };
1213
- }
1214
- return {
1215
- zone: top.key,
1216
- confidence: top.share,
1217
- why: `${top.count} \u0438\u0437 ${[...votes.values()].reduce((a, b) => a + b, 0)} \u0444\u0430\u0439\u043B\u043E\u0432 \u2014 \u0437\u043E\u043D\u0430 ${top.key}`,
1218
- spine: touchedSpine.length > 0
1219
- };
1220
- }
1221
1277
  function suggestZonesFromText(model, text, limit = 3) {
1222
1278
  if (!hasZoneModel(model)) return [];
1223
1279
  const terms = tokenize(text);
@@ -1448,6 +1504,7 @@ var init_src = __esm({
1448
1504
  init_reviewPhase();
1449
1505
  init_mergeGate();
1450
1506
  init_verifyVerdict();
1507
+ init_modelPolicy();
1451
1508
  init_status();
1452
1509
  init_deps();
1453
1510
  init_time();
@@ -1463,6 +1520,7 @@ var init_src = __esm({
1463
1520
  init_hostCommands();
1464
1521
  init_relief();
1465
1522
  init_cost();
1523
+ init_modelPricing();
1466
1524
  init_runGuards();
1467
1525
  init_codeZones();
1468
1526
  init_effort();
@@ -1597,18 +1655,12 @@ var init_swarmFiles = __esm({
1597
1655
  });
1598
1656
 
1599
1657
  // src/pricing.ts
1600
- function pricingAt(pricing, at) {
1601
- const intro = pricing.intro;
1602
- if (!intro) return pricing;
1603
- const day = at.toISOString().slice(0, 10);
1604
- return day <= intro.until ? { inputPerMTok: intro.inputPerMTok, outputPerMTok: intro.outputPerMTok } : pricing;
1605
- }
1606
- function costFromUsage(usage, model, table = MODEL_PRICING, at = /* @__PURE__ */ new Date()) {
1607
- const entry = model ? table[model] : void 0;
1658
+ function costFromUsage(usage, model, table = MODEL_PRICING) {
1659
+ const entry = findModelPricing(model, table);
1608
1660
  if (!entry) {
1609
1661
  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` };
1610
1662
  }
1611
- const pricing = pricingAt(entry, at);
1663
+ const pricing = entry;
1612
1664
  const input = usage.input_tokens ?? 0;
1613
1665
  const output = usage.output_tokens ?? 0;
1614
1666
  const cacheWrite = usage.cache_creation_input_tokens ?? 0;
@@ -1616,24 +1668,10 @@ function costFromUsage(usage, model, table = MODEL_PRICING, at = /* @__PURE__ */
1616
1668
  const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
1617
1669
  return { costUsd };
1618
1670
  }
1619
- var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
1620
1671
  var init_pricing = __esm({
1621
1672
  "src/pricing.ts"() {
1622
1673
  "use strict";
1623
- MODEL_PRICING = {
1624
- // Sonnet 5: вводные 2/10 по 31.08.2026 включительно, дальше базовые 3/15.
1625
- "claude-sonnet-5": {
1626
- inputPerMTok: 3,
1627
- outputPerMTok: 15,
1628
- intro: { inputPerMTok: 2, outputPerMTok: 10, until: "2026-08-31" }
1629
- },
1630
- "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
1631
- // Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
1632
- "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
1633
- "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
1634
- };
1635
- CACHE_READ_FACTOR = 0.1;
1636
- CACHE_WRITE_5M_FACTOR = 1.25;
1674
+ init_src();
1637
1675
  }
1638
1676
  });
1639
1677
 
@@ -2053,6 +2091,7 @@ __export(config_exports, {
2053
2091
  REPO_MAP_PATH: () => REPO_MAP_PATH,
2054
2092
  REVIEW_CUTOFF_PATH: () => REVIEW_CUTOFF_PATH,
2055
2093
  RUN_LOGS_DIR: () => RUN_LOGS_DIR,
2094
+ SWARM_ANOMALY_DEP_MISSING_MS: () => SWARM_ANOMALY_DEP_MISSING_MS,
2056
2095
  SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
2057
2096
  SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS: () => SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
2058
2097
  SWARM_ANOMALY_MAX_REPEATS: () => SWARM_ANOMALY_MAX_REPEATS,
@@ -2095,6 +2134,7 @@ __export(config_exports, {
2095
2134
  SWARM_REPO: () => SWARM_REPO,
2096
2135
  SWARM_REPO_MAP_MS: () => SWARM_REPO_MAP_MS,
2097
2136
  SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
2137
+ SWARM_REPO_SCAN_TTL_MS: () => SWARM_REPO_SCAN_TTL_MS,
2098
2138
  SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
2099
2139
  SWARM_SCAN_DEPTH: () => SWARM_SCAN_DEPTH,
2100
2140
  SWARM_SCAN_ROOTS_RAW: () => SWARM_SCAN_ROOTS_RAW,
@@ -2116,6 +2156,7 @@ __export(config_exports, {
2116
2156
  WORKTREES_LEGACY_DIR: () => WORKTREES_LEGACY_DIR,
2117
2157
  WORKTREES_NOINDEX_DIR: () => WORKTREES_NOINDEX_DIR,
2118
2158
  droneWorktreePath: () => droneWorktreePath,
2159
+ droneWorktreeRoot: () => droneWorktreeRoot,
2119
2160
  mcpEndpoint: () => mcpEndpoint,
2120
2161
  migrateWorktreesToNoindex: () => migrateWorktreesToNoindex,
2121
2162
  normalizeRepo: () => normalizeRepo,
@@ -2134,7 +2175,10 @@ function normalizeRepo(repo) {
2134
2175
  }
2135
2176
  }
2136
2177
  function droneWorktreePath(worktreesDir, repo, slug) {
2137
- return join5(worktreesDir, basename(normalizeRepo(repo)), slug);
2178
+ return join5(droneWorktreeRoot(worktreesDir, repo), slug);
2179
+ }
2180
+ function droneWorktreeRoot(worktreesDir, repo) {
2181
+ return join5(worktreesDir, basename(normalizeRepo(repo)));
2138
2182
  }
2139
2183
  function resolveEffectiveWorktreesDir(deps) {
2140
2184
  if (deps.envOverride) return deps.envOverride;
@@ -2196,7 +2240,7 @@ function reviewCutoffAt() {
2196
2240
  }
2197
2241
  return at;
2198
2242
  }
2199
- 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;
2243
+ 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;
2200
2244
  var init_config = __esm({
2201
2245
  "src/config.ts"() {
2202
2246
  "use strict";
@@ -2258,6 +2302,7 @@ var init_config = __esm({
2258
2302
  SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
2259
2303
  REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join5(SWARM_HOME, "repo-map.json");
2260
2304
  SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
2305
+ SWARM_REPO_SCAN_TTL_MS = envNum("SWARM_REPO_SCAN_TTL_MS", 24 * 60 * 6e4);
2261
2306
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
2262
2307
  SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
2263
2308
  TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
@@ -2284,6 +2329,7 @@ var init_config = __esm({
2284
2329
  SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
2285
2330
  SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
2286
2331
  SWARM_ANOMALY_RESOURCE_GATE_STALL_MS = envNum("SWARM_ANOMALY_RESOURCE_GATE_STALL_MS", 5 * 6e4);
2332
+ SWARM_ANOMALY_DEP_MISSING_MS = envNum("SWARM_ANOMALY_DEP_MISSING_MS", 2 * 6e4);
2287
2333
  SWARM_WATCHDOG_STALL_MS = envNum("SWARM_WATCHDOG_STALL_MS", 15 * 6e4);
2288
2334
  }
2289
2335
  });
@@ -2682,11 +2728,11 @@ var init_storm = __esm({
2682
2728
  });
2683
2729
 
2684
2730
  // src/heartbeat.ts
2685
- import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
2731
+ import { readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
2686
2732
  function writeHeartbeat(path, now) {
2687
2733
  const tmp = `${path}.tmp`;
2688
2734
  writeFileSync5(tmp, String(now));
2689
- renameSync3(tmp, path);
2735
+ renameSync4(tmp, path);
2690
2736
  }
2691
2737
  function readHeartbeat(path) {
2692
2738
  try {
@@ -2709,7 +2755,7 @@ var init_heartbeat = __esm({
2709
2755
 
2710
2756
  // src/agentToken.ts
2711
2757
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
2712
- import { join as join8 } from "path";
2758
+ import { join as join9 } from "path";
2713
2759
  function loadAgentToken() {
2714
2760
  try {
2715
2761
  const s = JSON.parse(readFileSync7(STORE_PATH, "utf8"));
@@ -2765,7 +2811,7 @@ var init_agentToken = __esm({
2765
2811
  "src/agentToken.ts"() {
2766
2812
  "use strict";
2767
2813
  init_config();
2768
- STORE_PATH = join8(SWARM_HOME, "agent.json");
2814
+ STORE_PATH = join9(SWARM_HOME, "agent.json");
2769
2815
  base = (api) => api.replace(/\/$/, "");
2770
2816
  TOKEN_PROBE_TIMEOUT_MS = 8e3;
2771
2817
  }
@@ -3125,9 +3171,9 @@ var init_procCleanup = __esm({
3125
3171
  });
3126
3172
 
3127
3173
  // src/git.ts
3128
- import { existsSync as existsSync9, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
3174
+ import { existsSync as existsSync10, readdirSync as readdirSync11, statSync as statSync8 } from "fs";
3129
3175
  import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
3130
- import { isAbsolute as isAbsolute2, join as join16, sep as sep2 } from "path";
3176
+ import { isAbsolute as isAbsolute2, join as join17, sep as sep2 } from "path";
3131
3177
  import { $ as $2 } from "execa";
3132
3178
  function isUnrecoverableRefError(e) {
3133
3179
  if (e instanceof MissingRefError) return true;
@@ -3139,7 +3185,7 @@ async function reclaimStaleWorktree(repo, worktree) {
3139
3185
  }
3140
3186
  function isLinkedWorktree(dir) {
3141
3187
  try {
3142
- return statSync7(join16(dir, ".git")).isFile();
3188
+ return statSync8(join17(dir, ".git")).isFile();
3143
3189
  } catch {
3144
3190
  return false;
3145
3191
  }
@@ -3151,7 +3197,7 @@ async function detachWorktree(repo, worktree) {
3151
3197
  }
3152
3198
  }
3153
3199
  async function ensureWorktreeDirGone(worktree) {
3154
- if (!existsSync9(worktree)) return false;
3200
+ if (!existsSync10(worktree)) return false;
3155
3201
  if (!isLinkedWorktree(worktree)) {
3156
3202
  console.error(
3157
3203
  `[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.`
@@ -3166,7 +3212,7 @@ async function ensureWorktreeDirGone(worktree) {
3166
3212
  );
3167
3213
  return true;
3168
3214
  }
3169
- if (existsSync9(worktree)) {
3215
+ if (existsSync10(worktree)) {
3170
3216
  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.`);
3171
3217
  }
3172
3218
  return true;
@@ -3214,7 +3260,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
3214
3260
  }
3215
3261
  const busyAt = await worktreeForBranch(repo, branch);
3216
3262
  if (busyAt) {
3217
- const exists = existsSync9(busyAt);
3263
+ const exists = existsSync10(busyAt);
3218
3264
  const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
3219
3265
  if (live) throw new WorktreeBusyError(branch, busyAt);
3220
3266
  if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
@@ -3249,8 +3295,8 @@ async function ensureGitExcluded(worktree, pattern) {
3249
3295
  try {
3250
3296
  const commonDir = await gitPath(worktree, "--git-common-dir");
3251
3297
  if (!commonDir) return false;
3252
- const infoDir = join16(commonDir, "info");
3253
- const excludeFile = join16(infoDir, "exclude");
3298
+ const infoDir = join17(commonDir, "info");
3299
+ const excludeFile = join17(infoDir, "exclude");
3254
3300
  let current = "";
3255
3301
  try {
3256
3302
  current = await readFile(excludeFile, "utf8");
@@ -3270,7 +3316,7 @@ async function gitPath(worktree, flag) {
3270
3316
  const out = await $2({ reject: false })`git -C ${worktree} rev-parse ${flag}`;
3271
3317
  const raw = out.stdout.trim();
3272
3318
  if (out.exitCode !== 0 || !raw) return null;
3273
- return isAbsolute2(raw) ? raw : join16(worktree, raw);
3319
+ return isAbsolute2(raw) ? raw : join17(worktree, raw);
3274
3320
  }
3275
3321
  async function ensureWorktreeHygiene(worktree) {
3276
3322
  for (const pattern of SWARM_EXCLUDE_PATTERNS) await ensureGitExcluded(worktree, pattern);
@@ -3280,12 +3326,12 @@ async function installGuardHooks(worktree) {
3280
3326
  try {
3281
3327
  const gitDir = await gitPath(worktree, "--git-dir");
3282
3328
  if (!gitDir) return false;
3283
- const hooksDir = join16(gitDir, "swarm-hooks");
3329
+ const hooksDir = join17(gitDir, "swarm-hooks");
3284
3330
  const prev = await previousHooksPath(worktree);
3285
3331
  await mkdir(hooksDir, { recursive: true });
3286
3332
  for (const hook of GUARD_HOOKS) {
3287
- await writeFile(join16(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
3288
- await chmod(join16(hooksDir, hook), 493);
3333
+ await writeFile(join17(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
3334
+ await chmod(join17(hooksDir, hook), 493);
3289
3335
  }
3290
3336
  await $2`git -C ${worktree} config extensions.worktreeConfig true`;
3291
3337
  await $2`git -C ${worktree} config --worktree core.hooksPath ${posix(hooksDir)}`;
@@ -3304,7 +3350,7 @@ async function previousHooksPath(worktree) {
3304
3350
  if (out.exitCode === 0 && value) return value;
3305
3351
  }
3306
3352
  const commonDir = await gitPath(worktree, "--git-common-dir");
3307
- return commonDir ? join16(commonDir, "hooks") : null;
3353
+ return commonDir ? join17(commonDir, "hooks") : null;
3308
3354
  }
3309
3355
  async function removeWorktree(repo, path) {
3310
3356
  const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
@@ -3323,7 +3369,7 @@ async function worktreeDirty(path) {
3323
3369
  async function repairMovedWorktrees(targetDir) {
3324
3370
  const subdirs = (dir) => {
3325
3371
  try {
3326
- return readdirSync10(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join16(dir, d.name));
3372
+ return readdirSync11(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join17(dir, d.name));
3327
3373
  } catch {
3328
3374
  return [];
3329
3375
  }
@@ -3450,7 +3496,7 @@ __export(droneChannel_exports, {
3450
3496
  verifyLocalCliToken: () => verifyLocalCliToken
3451
3497
  });
3452
3498
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
3453
- import { chmodSync, mkdirSync as mkdirSync7, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
3499
+ import { chmodSync, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
3454
3500
  import { dirname as dirname3 } from "path";
3455
3501
  import { serve } from "@hono/node-server";
3456
3502
  function loadChannelSecret(path) {
@@ -3460,7 +3506,7 @@ function loadChannelSecret(path) {
3460
3506
  } catch {
3461
3507
  }
3462
3508
  const secret = randomBytes(32).toString("hex");
3463
- mkdirSync7(dirname3(path), { recursive: true });
3509
+ mkdirSync8(dirname3(path), { recursive: true });
3464
3510
  writeFileSync10(path, secret, { mode: 384 });
3465
3511
  try {
3466
3512
  chmodSync(path, 384);
@@ -3805,7 +3851,7 @@ var init_droneChannel = __esm({
3805
3851
 
3806
3852
  // src/attachments.ts
3807
3853
  import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3808
- import { join as join19 } from "path";
3854
+ import { join as join20 } from "path";
3809
3855
  function attachmentsFromDoc(doc, origin) {
3810
3856
  const out = [];
3811
3857
  const walk2 = (node) => {
@@ -3896,7 +3942,7 @@ async function materializeAttachments(opts) {
3896
3942
  };
3897
3943
  const result = { files: [], skipped: [] };
3898
3944
  if (opts.refs.length === 0) return result;
3899
- const dir = join19(opts.worktree, ATTACHMENTS_DIR);
3945
+ const dir = join20(opts.worktree, ATTACHMENTS_DIR);
3900
3946
  await io.removeDir(dir);
3901
3947
  let dirReady = false;
3902
3948
  let total = 0;
@@ -3942,7 +3988,7 @@ async function materializeAttachments(opts) {
3942
3988
  await io.mkdir(dir);
3943
3989
  dirReady = true;
3944
3990
  }
3945
- await io.writeFile(join19(dir, name), file.bytes);
3991
+ await io.writeFile(join20(dir, name), file.bytes);
3946
3992
  } catch (e) {
3947
3993
  result.skipped.push(`${label} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText(e)})`);
3948
3994
  continue;
@@ -4095,7 +4141,7 @@ var init_attachments = __esm({
4095
4141
 
4096
4142
  // src/shotUpload.ts
4097
4143
  import { readFile as fsReadFile } from "fs/promises";
4098
- import { basename as basename2, isAbsolute as isAbsolute3, join as join20 } from "path";
4144
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join21 } from "path";
4099
4145
  function shotMimetype(path) {
4100
4146
  const dot = path.lastIndexOf(".");
4101
4147
  if (dot < 0) return null;
@@ -4121,7 +4167,7 @@ async function readShots(paths, opts) {
4121
4167
  }
4122
4168
  let bytes;
4123
4169
  try {
4124
- bytes = await read(isAbsolute3(path) ? path : join20(opts.worktree, path));
4170
+ bytes = await read(isAbsolute3(path) ? path : join21(opts.worktree, path));
4125
4171
  } catch (e) {
4126
4172
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
4127
4173
  continue;
@@ -4200,7 +4246,7 @@ var init_shotUpload = __esm({
4200
4246
  // src/oauth.ts
4201
4247
  import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
4202
4248
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
4203
- import { join as join21 } from "path";
4249
+ import { join as join22 } from "path";
4204
4250
  function makePkce() {
4205
4251
  const verifier = randomBytes2(48).toString("base64url");
4206
4252
  const challenge = createHash2("sha256").update(verifier).digest("base64url");
@@ -4335,8 +4381,8 @@ var init_oauth = __esm({
4335
4381
  AUTHORIZE = "/api/oauth/authorize";
4336
4382
  TOKEN = "/api/oauth/token";
4337
4383
  SCOPE = "openid profile email taskless:full";
4338
- STORE_PATH2 = join21(SWARM_HOME, "oauth.json");
4339
- CLIENT_PATH = join21(SWARM_HOME, "oauth-client.json");
4384
+ STORE_PATH2 = join22(SWARM_HOME, "oauth.json");
4385
+ CLIENT_PATH = join22(SWARM_HOME, "oauth-client.json");
4340
4386
  base2 = (api) => api.replace(/\/$/, "");
4341
4387
  }
4342
4388
  });
@@ -4361,6 +4407,10 @@ function authStateLine(nowMs = Date.now()) {
4361
4407
  if (!denial) return null;
4362
4408
  return `\u0414\u041E\u0421\u0422\u0423\u041F \u041A TASKLESS \u041E\u0422\u041E\u0417\u0412\u0410\u041D \u2014 ${deniedSinceLine(nowMs)}. \u041F\u043E\u0447\u0438\u043D\u0438 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: swarm login`;
4363
4409
  }
4410
+ function signedInToTaskless() {
4411
+ if (denial) return false;
4412
+ return Boolean(loadAgentToken()?.token);
4413
+ }
4364
4414
  function deniedSinceLine(nowMs) {
4365
4415
  const d = denial;
4366
4416
  if (!d) return "";
@@ -4379,7 +4429,7 @@ var init_authState = __esm({
4379
4429
 
4380
4430
  // src/workspace.ts
4381
4431
  import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
4382
- import { join as join22 } from "path";
4432
+ import { join as join23 } from "path";
4383
4433
  function loadWorkspace() {
4384
4434
  try {
4385
4435
  const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
@@ -4396,7 +4446,7 @@ var init_workspace = __esm({
4396
4446
  "src/workspace.ts"() {
4397
4447
  "use strict";
4398
4448
  init_config();
4399
- WORKSPACE_PATH = join22(SWARM_HOME, "workspace.json");
4449
+ WORKSPACE_PATH = join23(SWARM_HOME, "workspace.json");
4400
4450
  }
4401
4451
  });
4402
4452
 
@@ -4891,7 +4941,8 @@ __export(db_exports, {
4891
4941
  SwarmDb: () => SwarmDb
4892
4942
  });
4893
4943
  import { createRequire as createRequire2 } from "module";
4894
- import { existsSync as existsSync11 } from "fs";
4944
+ import { randomUUID as randomUUID3 } from "crypto";
4945
+ import { existsSync as existsSync12 } from "fs";
4895
4946
  import { resolve as resolve3 } from "path";
4896
4947
  function rowToQuestion(row) {
4897
4948
  return {
@@ -4924,6 +4975,7 @@ function rowToRun(row) {
4924
4975
  area: row.area ?? null,
4925
4976
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
4926
4977
  model: row.model ?? null,
4978
+ review_model: row.review_model ?? null,
4927
4979
  // TASWM-395: уровень усилия — через toEffortLevel, а не сырой строкой: в зеркале может
4928
4980
  // лежать значение, записанное демоном другой версии (словарь уровней задаёт CLI), и
4929
4981
  // непонятное здесь означает «уровень не задан», а не сломанный тип.
@@ -5001,6 +5053,7 @@ CREATE TABLE IF NOT EXISTS runs (
5001
5053
  area TEXT,
5002
5054
  depends_on TEXT,
5003
5055
  model TEXT,
5056
+ review_model TEXT,
5004
5057
  effort TEXT,
5005
5058
  cost_usd REAL NOT NULL DEFAULT 0,
5006
5059
  tokens_in INTEGER NOT NULL DEFAULT 0,
@@ -5051,6 +5104,12 @@ CREATE INDEX IF NOT EXISTS idx_runs_area ON runs(area);
5051
5104
  CREATE INDEX IF NOT EXISTS idx_questions_run ON questions(run_id);
5052
5105
  CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
5053
5106
  CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id);
5107
+ -- \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
5108
+ -- (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.
5109
+ CREATE TABLE IF NOT EXISTS meta (
5110
+ key TEXT PRIMARY KEY,
5111
+ value TEXT NOT NULL
5112
+ );
5054
5113
  -- \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)
5055
5114
  CREATE TABLE IF NOT EXISTS imports (
5056
5115
  source TEXT PRIMARY KEY,
@@ -5063,6 +5122,8 @@ CREATE TABLE IF NOT EXISTS imports (
5063
5122
  defaultRepo;
5064
5123
  databaseCtor;
5065
5124
  // для importDb (второй Database, source)
5125
+ /** Идентификатор этой жизни файла базы (TASWM-419) — им неймспейсятся каталоги run-log'ов. */
5126
+ instanceId;
5066
5127
  constructor(path = "swarm.db", defaultRepo = "") {
5067
5128
  this.defaultRepo = normalizeRepo(defaultRepo);
5068
5129
  this.databaseCtor = loadBetterSqlite3();
@@ -5114,7 +5175,11 @@ CREATE TABLE IF NOT EXISTS imports (
5114
5175
  "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0",
5115
5176
  // TASWM-395: усилие рассуждения прогона (`claude --effort`). Nullable без DEFAULT —
5116
5177
  // NULL это и все прогоны до фичи, и прогоны на дефолте харнесса.
5117
- "ALTER TABLE runs ADD COLUMN effort TEXT"
5178
+ "ALTER TABLE runs ADD COLUMN effort TEXT",
5179
+ // TASWM-451: чем проверять работу этого прогона — модель ревью, приехавшая с задачи.
5180
+ // Живёт на прогоне ИСПОЛНИТЕЛЯ: ревьюер и верификатор заводятся локально и читают её
5181
+ // оттуда, а не ходят за задачей в прод в момент спавна.
5182
+ "ALTER TABLE runs ADD COLUMN review_model TEXT"
5118
5183
  ]) {
5119
5184
  try {
5120
5185
  this.db.exec(alter);
@@ -5126,6 +5191,22 @@ CREATE TABLE IF NOT EXISTS imports (
5126
5191
  if (this.defaultRepo) {
5127
5192
  this.db.prepare("UPDATE runs SET repo = ? WHERE repo IS NULL").run(this.defaultRepo);
5128
5193
  }
5194
+ this.instanceId = this.ensureInstanceId();
5195
+ }
5196
+ /**
5197
+ * Идентификатор ЭТОЙ жизни файла базы (TASWM-419). Заводится при первом открытии и живёт
5198
+ * в самой базе, поэтому переживает рестарт демона, но НЕ переживает пересоздание файла:
5199
+ * снесли базу — новый id, и каталоги run-log'ов нового прогона #1 не подселяются к файлам
5200
+ * прогона #1 из прошлой жизни. Именно это и было корнем «лог не того прогона»: номер там
5201
+ * был вся информация об адресе.
5202
+ */
5203
+ ensureInstanceId() {
5204
+ const read = () => this.db.prepare("SELECT value FROM meta WHERE key = 'instance_id'").get()?.value;
5205
+ const existing = read();
5206
+ if (existing) return existing;
5207
+ const fresh = randomUUID3().replace(/-/g, "").slice(0, 12);
5208
+ this.db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES ('instance_id', ?)").run(fresh);
5209
+ return read() ?? fresh;
5129
5210
  }
5130
5211
  close() {
5131
5212
  this.db.close();
@@ -5134,7 +5215,7 @@ CREATE TABLE IF NOT EXISTS imports (
5134
5215
  createRun(input) {
5135
5216
  const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
5136
5217
  const res = this.db.prepare(
5137
- "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
5218
+ "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
5138
5219
  ).run(
5139
5220
  input.task_slug,
5140
5221
  name,
@@ -5147,6 +5228,7 @@ CREATE TABLE IF NOT EXISTS imports (
5147
5228
  input.area ?? null,
5148
5229
  input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
5149
5230
  input.model ?? null,
5231
+ input.review_model ?? null,
5150
5232
  input.effort ?? null,
5151
5233
  // усилие рассуждения (TASWM-395); null → разрешит спавн
5152
5234
  input.decision ? JSON.stringify(input.decision) : null,
@@ -5219,6 +5301,15 @@ CREATE TABLE IF NOT EXISTS imports (
5219
5301
  const m = /^local:(\d+)$/.exec(key);
5220
5302
  return m ? this.getRun(Number(m[1])) : null;
5221
5303
  }
5304
+ /**
5305
+ * TASWM-419: адрес каталога run-log'ов — номер, НЕЙМСПЕЙСНУТЫЙ идентификатором файла базы
5306
+ * (`local:<instance>:<id>`). Тот же номер в пересозданной базе даёт другой instance, то
5307
+ * есть другой каталог: подселиться к файлам чужого прогона с тем же номером больше нечем.
5308
+ * Псевдонимов у sqlite-прогона нет — ключ не меняется, пока жив файл базы.
5309
+ */
5310
+ runLogKeys(id) {
5311
+ return this.getRun(id) ? [`local:${this.instanceId}:${id}`] : [];
5312
+ }
5222
5313
  listRuns(status) {
5223
5314
  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();
5224
5315
  return rows.map(rowToRun);
@@ -5459,7 +5550,7 @@ CREATE TABLE IF NOT EXISTS imports (
5459
5550
  const repoN = normalizeRepo(repo);
5460
5551
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
5461
5552
  if (already) return { imported: false, runs: already.run_count };
5462
- if (!existsSync11(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
5553
+ if (!existsSync12(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
5463
5554
  const src = new this.databaseCtor(abs);
5464
5555
  try {
5465
5556
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -5828,8 +5919,8 @@ var init_droneStore = __esm({
5828
5919
 
5829
5920
  // src/overmind.ts
5830
5921
  import { homedir as homedir5 } from "os";
5831
- import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync12, renameSync as renameSync10 } from "fs";
5832
- import { basename as basename4, dirname as dirname4, join as join29 } from "path";
5922
+ import { existsSync as existsSync17, readFileSync as readFileSync20, readdirSync as readdirSync13, renameSync as renameSync11 } from "fs";
5923
+ import { basename as basename4, dirname as dirname4, join as join30 } from "path";
5833
5924
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
5834
5925
  import { spawn as spawn3 } from "child_process";
5835
5926
 
@@ -5909,13 +6000,66 @@ init_config();
5909
6000
  init_resources();
5910
6001
  init_names2();
5911
6002
  init_storm();
5912
- import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
6003
+ import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
5913
6004
  import { dirname } from "path";
5914
6005
  import { randomUUID } from "crypto";
5915
6006
 
5916
6007
  // src/logTail.ts
5917
- import { closeSync, existsSync as existsSync4, openSync, readSync, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
6008
+ import { closeSync, existsSync as existsSync5, openSync, readSync, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
6009
+ import { join as join7 } from "path";
6010
+
6011
+ // src/runLogDir.ts
6012
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync4, renameSync as renameSync3, statSync as statSync3 } from "fs";
5918
6013
  import { join as join6 } from "path";
6014
+ var LEGACY_RUN_LOGS_SUBDIR = "legacy";
6015
+ function runLogDirName(runLogKey) {
6016
+ const safe2 = runLogKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
6017
+ if (!safe2) 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)}`);
6018
+ return /^\d+$/.test(safe2) ? `k-${safe2}` : safe2;
6019
+ }
6020
+ function runLogDirPath(runLogsDir, runLogKey) {
6021
+ return join6(runLogsDir, runLogDirName(runLogKey));
6022
+ }
6023
+ function findRunLogDir(runLogsDir, keys) {
6024
+ for (const key of keys) {
6025
+ const dir = runLogDirPath(runLogsDir, key);
6026
+ if (existsSync4(dir)) return dir;
6027
+ }
6028
+ return null;
6029
+ }
6030
+ function runLogDirForWrite(runLogsDir, keys) {
6031
+ const canonical = keys[0];
6032
+ if (!canonical) return null;
6033
+ return findRunLogDir(runLogsDir, keys) ?? runLogDirPath(runLogsDir, canonical);
6034
+ }
6035
+ function sweepLegacyRunLogs(runLogsDir) {
6036
+ const legacyDir = join6(runLogsDir, LEGACY_RUN_LOGS_SUBDIR);
6037
+ let entries;
6038
+ try {
6039
+ entries = readdirSync4(runLogsDir);
6040
+ } catch {
6041
+ return { moved: 0, failed: 0, legacyDir };
6042
+ }
6043
+ let moved = 0;
6044
+ let failed = 0;
6045
+ for (const name of entries) {
6046
+ if (!/^\d+$/.test(name)) continue;
6047
+ const from = join6(runLogsDir, name);
6048
+ try {
6049
+ if (!statSync3(from).isDirectory()) continue;
6050
+ mkdirSync4(legacyDir, { recursive: true });
6051
+ let to = join6(legacyDir, name);
6052
+ for (let n = 2; existsSync4(to) && n < 100; n++) to = join6(legacyDir, `${name}-${n}`);
6053
+ renameSync3(from, to);
6054
+ moved++;
6055
+ } catch {
6056
+ failed++;
6057
+ }
6058
+ }
6059
+ return { moved, failed, legacyDir };
6060
+ }
6061
+
6062
+ // src/logTail.ts
5919
6063
  var TAIL_LINES_FALLBACK = 200;
5920
6064
  var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
5921
6065
  function fileHeader(name, size, shown) {
@@ -5925,7 +6069,7 @@ function fileHeader(name, size, shown) {
5925
6069
  function readTail(path, opts) {
5926
6070
  let size;
5927
6071
  try {
5928
- size = statSync3(path).size;
6072
+ size = statSync4(path).size;
5929
6073
  } catch {
5930
6074
  return null;
5931
6075
  }
@@ -5955,11 +6099,12 @@ function readTail(path, opts) {
5955
6099
  text = rows.join("\n");
5956
6100
  return { text, size, shown: Buffer.byteLength(text, "utf8") };
5957
6101
  }
5958
- function latestRunLaunch(runLogsDir, localRunId) {
5959
- const dir = join6(runLogsDir, String(localRunId));
6102
+ function latestRunLaunch(runLogsDir, runLogKeys) {
6103
+ const dir = findRunLogDir(runLogsDir, runLogKeys);
6104
+ if (!dir) return null;
5960
6105
  let files;
5961
6106
  try {
5962
- files = readdirSync4(dir);
6107
+ files = readdirSync5(dir);
5963
6108
  } catch {
5964
6109
  return null;
5965
6110
  }
@@ -5970,13 +6115,17 @@ function latestRunLaunch(runLogsDir, localRunId) {
5970
6115
  }
5971
6116
  if (indexes.size === 0) return null;
5972
6117
  const index = Math.max(...indexes);
5973
- const out = [join6(dir, `${index}.json`), join6(dir, `${index}.json.done`)].find((p) => existsSync4(p)) ?? null;
5974
- const err = existsSync4(join6(dir, `${index}.err.log`)) ? join6(dir, `${index}.err.log`) : null;
6118
+ const out = [join7(dir, `${index}.json`), join7(dir, `${index}.json.done`)].find((p) => existsSync5(p)) ?? null;
6119
+ const err = existsSync5(join7(dir, `${index}.err.log`)) ? join7(dir, `${index}.err.log`) : null;
5975
6120
  return { index, total: indexes.size, out, err };
5976
6121
  }
5977
- function runLogTail(runLogsDir, localRunId, opts) {
5978
- const launch = latestRunLaunch(runLogsDir, localRunId);
5979
- if (!launch) return { error: `\u043B\u043E\u0433\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 (${join6(runLogsDir, String(localRunId))} \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442)` };
6122
+ function runLogTail(runLogsDir, runLogKeys, opts) {
6123
+ const launch = latestRunLaunch(runLogsDir, runLogKeys);
6124
+ if (!launch) {
6125
+ const canonical = runLogKeys[0];
6126
+ const where = canonical ? runLogDirPath(runLogsDir, canonical) : runLogsDir;
6127
+ return { error: `\u043B\u043E\u0433\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 (${where} \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442)` };
6128
+ }
5980
6129
  const parts = [];
5981
6130
  let budget = opts.maxBytes;
5982
6131
  if (launch.err) {
@@ -6031,8 +6180,8 @@ async function serveLogRequests(deps, commands) {
6031
6180
  } else if (!cmd.runId) {
6032
6181
  result = { error: "\u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0431\u0435\u0437 runId \u2014 \u043D\u0435\u0447\u0435\u0433\u043E \u0447\u0438\u0442\u0430\u0442\u044C" };
6033
6182
  } else {
6034
- const local = deps.localRunId(cmd.runId);
6035
- result = local == null ? { error: `\u043F\u0440\u043E\u0433\u043E\u043D ${cmd.runId} \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D (\u0435\u0433\u043E \u0432\u0435\u043B\u043E \u043D\u0435 \u043E\u043D\u0430?)` } : runLogTail(deps.runLogsDir, local, opts);
6183
+ const keys = deps.runLogKeys(cmd.runId);
6184
+ result = keys.length === 0 ? { error: `\u043F\u0440\u043E\u0433\u043E\u043D ${cmd.runId} \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D (\u0435\u0433\u043E \u0432\u0435\u043B\u043E \u043D\u0435 \u043E\u043D\u0430?)` } : runLogTail(deps.runLogsDir, keys, opts);
6036
6185
  }
6037
6186
  } catch (e) {
6038
6187
  result = { error: `\u043D\u0435 \u0441\u043C\u043E\u0433 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u043B\u043E\u0433: ${e instanceof Error ? e.message : String(e)}` };
@@ -6128,6 +6277,11 @@ var SwarmClient = class _SwarmClient {
6128
6277
  // Живёт в памяти: рестарт демона просто переспросит прод.
6129
6278
  depsProd = /* @__PURE__ */ new Map();
6130
6279
  depsAskedAt = /* @__PURE__ */ new Map();
6280
+ // TASWM-399: ссылка dependsOn → человеко-слаг блокера, как его назвал прод. В колонке
6281
+ // лежит cuid прогона или задачи, а в лог демона, в событие прогона и в аномалию должен
6282
+ // уходить «TASWM-392», а не «cmsqqpphd011y2xtbckkut6bj» — иначе оператору всё равно
6283
+ // нечего искать. Резолв делает прод (см. resolveDepRefs), здесь только запоминаем.
6284
+ depSlug = /* @__PURE__ */ new Map();
6131
6285
  static DEPS_TTL_MS = 6e4;
6132
6286
  // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
6133
6287
  // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
@@ -6187,25 +6341,25 @@ var SwarmClient = class _SwarmClient {
6187
6341
  this.repoFullNames = registry.map((e) => e.repoFullName);
6188
6342
  this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
6189
6343
  this.host = opts.host ?? null;
6190
- this.repoExists = opts.repoExists ?? existsSync5;
6344
+ this.repoExists = opts.repoExists ?? existsSync6;
6191
6345
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
6192
6346
  this.executorId = opts.executorId ?? EXECUTOR.id;
6193
6347
  this.outboxPath = opts.outboxPath;
6194
6348
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
6195
6349
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
6196
- mkdirSync4(dirname(this.outboxPath), { recursive: true });
6350
+ mkdirSync5(dirname(this.outboxPath), { recursive: true });
6197
6351
  this.recover();
6198
6352
  }
6199
6353
  // ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
6200
6354
  recover() {
6201
- if (existsSync5(this.mapPath)) {
6355
+ if (existsSync6(this.mapPath)) {
6202
6356
  try {
6203
6357
  const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
6204
6358
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
6205
6359
  } catch {
6206
6360
  }
6207
6361
  }
6208
- if (existsSync5(this.outboxPath)) {
6362
+ if (existsSync6(this.outboxPath)) {
6209
6363
  const txt = readFileSync4(this.outboxPath, "utf8");
6210
6364
  for (const line of txt.split("\n")) {
6211
6365
  const s = line.trim();
@@ -6226,7 +6380,7 @@ var SwarmClient = class _SwarmClient {
6226
6380
  // eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
6227
6381
  // переприсваиваем локально (id событий — только порядок, не корреляция с продом).
6228
6382
  replayGlobalEvents() {
6229
- if (!existsSync5(this.stormPath)) return;
6383
+ if (!existsSync6(this.stormPath)) return;
6230
6384
  let txt;
6231
6385
  try {
6232
6386
  txt = readFileSync4(this.stormPath, "utf8");
@@ -6306,6 +6460,7 @@ var SwarmClient = class _SwarmClient {
6306
6460
  if (patch.status === "running") out.executor = this.executorId;
6307
6461
  if ("area" in patch) out.area = patch.area ?? null;
6308
6462
  if ("model" in patch) out.model = patch.model ?? null;
6463
+ if ("review_model" in patch) out.reviewModel = patch.review_model ?? null;
6309
6464
  if ("effort" in patch) out.effort = patch.effort ?? null;
6310
6465
  if ("name" in patch) out.droneName = patch.name ?? null;
6311
6466
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
@@ -6379,6 +6534,10 @@ var SwarmClient = class _SwarmClient {
6379
6534
  area: input.area ?? null,
6380
6535
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
6381
6536
  model: input.model ?? null,
6537
+ // TASWM-451: чем проверять эту работу. У прогона исполнителя приезжает из задачи
6538
+ // (claim в проде), у локально заведённого ревьюера/верификатора — наследством от
6539
+ // проверяемого прогона, которое ставит spawnReviewer/spawnVerifier.
6540
+ review_model: input.review_model ?? null,
6382
6541
  // Усилие рассуждения (TASWM-395): пин, если его поставили на спавне; иначе null —
6383
6542
  // разрешённый уровень впечатает launcher, когда действительно поднимет дрона.
6384
6543
  effort: input.effort ?? null,
@@ -6437,6 +6596,8 @@ var SwarmClient = class _SwarmClient {
6437
6596
  status: "queued",
6438
6597
  area: d.area,
6439
6598
  model: d.model,
6599
+ review_model: d.review_model,
6600
+ // TASWM-451: чем проверять — едет рядом с моделью
6440
6601
  effort: d.effort,
6441
6602
  // усилие рассуждения (TASWM-395), если пинили на спавне
6442
6603
  name: d.name,
@@ -6484,6 +6645,24 @@ var SwarmClient = class _SwarmClient {
6484
6645
  const viaProd = [...this.runs.values()].find((r) => r.runKey === `p:${prodId}` || this.prodIdByKey.get(r.runKey) === prodId);
6485
6646
  return viaProd ? this.mat(viaProd) : null;
6486
6647
  }
6648
+ /**
6649
+ * TASWM-419: адреса каталога run-log'ов прогона. КАНОНИЧЕСКИЙ — прод-вида `p:<cuid>`, как
6650
+ * только cuid известен: он один и тот же во всех жизнях демона, а локальный uuid прогона
6651
+ * после рестарта уже не воскресает (гидрированный прогон приезжает под прод-ключом).
6652
+ * Дальше — псевдонимы: uuid, под которым каталог мог завестись в окно между спавном и
6653
+ * первым флашем в прод. По ним каталог ищется, чтобы запуски одного прогона не расползлись
6654
+ * по двум каталогам (иначе cost недосчитал бы часть, а хвост показал бы не тот запуск).
6655
+ * Номер прогона в адресе не участвует вовсе — он и есть источник «лога чужого прогона».
6656
+ */
6657
+ runLogKeys(id) {
6658
+ const key = this.runs.get(id)?.runKey;
6659
+ if (!key) return [];
6660
+ const prodId = key.startsWith("p:") ? key.slice(2) : this.prodIdByKey.get(key);
6661
+ if (!prodId) return [key];
6662
+ const canonical = `p:${prodId}`;
6663
+ const aliases = [key, ...[...this.prodIdByKey.entries()].filter(([, v]) => v === prodId).map(([k]) => k)];
6664
+ return [canonical, ...new Set(aliases.filter((k) => k !== canonical))];
6665
+ }
6487
6666
  listRuns(status) {
6488
6667
  const all = [...this.runs.values()].filter((r) => status ? r.status === status : true);
6489
6668
  all.sort((a, b) => b.id - a.id);
@@ -6600,16 +6779,25 @@ var SwarmClient = class _SwarmClient {
6600
6779
  /**
6601
6780
  * TASWM-361: вердикт по каждой зависимости — см. докблок SwarmStore.depVerdicts.
6602
6781
  *
6603
- * АСИММЕТРИЯ ЗЕРКАЛА И ПРОДА, и почему она здесь намеренная. Зеркало — ЧАСТИЧНАЯ выборка
6604
- * (только прогоны этой машины, только окно гидрации), и по частичной выборке два вывода
6605
- * стоят разного:
6606
- * • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он не
6607
- * портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
6608
- * • «живых прогонов слага не осталось» — вывод от ОТСУТСТВИЯ: невидимый зеркалу прогон
6609
- * на соседней машине как раз и может быть живым. Поэтому «исчерпана неуспешно»
6610
- * принимаем ТОЛЬКО из ответа прода (refreshDeps) — он видит всех.
6611
- * Прод недоступен → вердикта нет → прогон честно ждёт следующего окна: лишняя минута
6612
- * очереди дешевле дрона, поднятого поверх работающего блокера.
6782
+ * ИСТОЧНИК ПРАВДЫ — ПРОД (TASWM-399), зеркало здесь только быстрый путь. Зеркало —
6783
+ * ЧАСТИЧНАЯ выборка (прогоны этой машины, окно гидрации), и по частичной выборке два
6784
+ * вывода стоят разного:
6785
+ * • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он
6786
+ * не портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
6787
+ * • всё остальное — вывод от ОТСУТСТВИЯ, и его принимаем ТОЛЬКО из ответа прода
6788
+ * (refreshDeps): невидимый зеркалу прогон на соседней машине как раз и может быть
6789
+ * живым, а закрытый с прода — давно смерженным.
6790
+ *
6791
+ * ССЫЛКА — НЕ ОБЯЗАТЕЛЬНО СЛАГ. В `dependsOn` лежит чаще всего cuid ПРОГОНА (так пишет
6792
+ * overmind), реже cuid задачи, и только дев-спавн `--after` пишет человеко-слаг. Быстрый
6793
+ * путь по зеркалу сравнивает `task_slug`, то есть срабатывает лишь на третьей форме —
6794
+ * это нормально: остальные две резолвит прод. А вот считать «раз в зеркале не нашёл —
6795
+ * значит не готово» было корнем TASWM-399.
6796
+ *
6797
+ * Прода ещё не спрашивали (первый тик) или он моргнул → `pending`: прогон честно ждёт
6798
+ * следующего окна. Лишняя минута очереди дешевле дрона, поднятого поверх работающего
6799
+ * блокера. `unknown` (блокера НЕ ЗНАЕТ И ПРОД) ставит только ответ прода — местным
6800
+ * незнанием такой вывод не делается.
6613
6801
  */
6614
6802
  depVerdicts(depends_on) {
6615
6803
  if (!depends_on || depends_on.length === 0) return [];
@@ -6619,57 +6807,89 @@ var SwarmClient = class _SwarmClient {
6619
6807
  doneInMirror.add(r.task_slug);
6620
6808
  }
6621
6809
  }
6622
- return depends_on.map((slug) => {
6623
- if (doneInMirror.has(slug)) {
6624
- return { slug, outcome: "done", met: true, status: "done", name: null, stopReason: null };
6810
+ return depends_on.map((ref) => {
6811
+ if (doneInMirror.has(ref)) {
6812
+ return { slug: ref, outcome: "done", met: true, status: "done", name: null, stopReason: null };
6813
+ }
6814
+ const fromProd = this.depsProd.get(ref);
6815
+ if (fromProd) {
6816
+ if (fromProd.outcome === "unknown" && this.knowsDepRef(ref)) {
6817
+ return { slug: this.depSlug.get(ref) ?? ref, outcome: "pending", met: false, status: null, name: null, stopReason: null };
6818
+ }
6819
+ return { slug: this.depSlug.get(ref) ?? ref, ...fromProd };
6625
6820
  }
6626
- const fromProd = this.depsProd.get(slug);
6627
- if (fromProd) return { slug, ...fromProd };
6628
- return { slug, outcome: "pending", met: false, status: null, name: null, stopReason: null };
6821
+ return { slug: ref, outcome: "pending", met: false, status: null, name: null, stopReason: null };
6629
6822
  });
6630
6823
  }
6631
6824
  /**
6632
- * TASWM-300: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
6825
+ * Знает ли ЗЕРКАЛО хоть один прогон по ссылке `dependsOn` (TASWM-399). Ссылка бывает
6826
+ * слагом, cuid'ом задачи и cuid'ом прогона — последний сверяем с прод-id, как это
6827
+ * делают runByProdId/refreshRun. Служебные сабраны (ревьюер/резолвер/верификатор) не
6828
+ * в счёт: их существование не означает, что жив сам блокер.
6829
+ */
6830
+ knowsDepRef(ref) {
6831
+ for (const r of this.runs.values()) {
6832
+ if (r.resolves_run_id != null || r.reviews_run_id != null || r.verifies_run_id != null) continue;
6833
+ if (r.task_slug === ref) return true;
6834
+ if (r.runKey === `p:${ref}` || this.prodIdByKey.get(r.runKey) === ref) return true;
6835
+ }
6836
+ return false;
6837
+ }
6838
+ /**
6839
+ * TASWM-300 → TASWM-399: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
6840
+ *
6841
+ * Почему зеркала мало. В нём лежат только прогоны ЭТОЙ машины (гейт isMine, hydrateRun) и
6842
+ * только те, что попали в окно гидрации (`swarm_list_runs limit`, сортировка по updatedAt
6843
+ * desc — сутки работы роя). Зависимость, закрытая на соседней машине, обнулившаяся при
6844
+ * рестарте демона или просто позавчерашняя, в зеркало не попадала — и `dependsOn` держал
6845
+ * прогон в очереди вечно.
6633
6846
  *
6634
- * Почему зеркала было мало. В нём лежат только прогоны ЭТОЙ машины (гейт isMine,
6635
- * hydrateRun) и только те, что попали в окно гидрации (`swarm_list_runs limit`, сортировка
6636
- * по updatedAt desc — сутки работы роя). Зависимость, закрытая на соседней машине или
6637
- * просто позавчера, в зеркало не попадала — и `--after` держал прогон в очереди вечно
6638
- * (04.08: снимали руками).
6847
+ * ЧЕМ СПРАШИВАЕМ И ПОЧЕМУ НЕ `swarm_list_runs` (корень TASWM-399). До этой задачи здесь
6848
+ * шёл `swarm_list_runs { taskId: <ref> }` — то есть ссылка трактовалась как ЗАДАЧА. Но в
6849
+ * `dependsOn` чаще всего лежит cuid ПРОГОНА: `resolveTaskRef` его задачей не признаёт,
6850
+ * ответ приходит `isError`, вердикт не появляется НИКОГДА, и прогон стоит молча при
6851
+ * свободной машине (12–13.08 — пять остановок роя за сутки). `swarm_deps_status` понимает
6852
+ * все три формы ссылки и отвечает по единому правилу роя (`judgeDep` на прод-стороне).
6639
6853
  *
6640
6854
  * Кеш. «Смержено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
6641
6855
  * положительный ответ помним навсегда и больше не спрашиваем. Остальные вердикты (включая
6642
- * «исчерпана неуспешно», TASWM-361 — блокер могут переиграть новым прогоном) переспрашиваем
6643
- * не чаще DEPS_TTL_MS: тик идёт раз в ~4с, а ждущих зависимость прогонов может быть
6644
- * много — без TTL это был бы поход в прод на каждый слаг каждый тик.
6856
+ * «исчерпана неуспешно» и «блокера не знает никто») переспрашиваем не чаще DEPS_TTL_MS:
6857
+ * оператор вправе переиграть блокер новым прогоном, а тик идёт раз в ~4 с — без TTL это
6858
+ * был бы поход в прод на каждую ссылку каждый тик.
6859
+ *
6860
+ * Одним вызовом на все просроченные ссылки, а не по вызову на ссылку: очередь ждущих
6861
+ * бывает в десятки прогонов, и N запросов на тик — это тот же самый поход в прод, только
6862
+ * дороже.
6645
6863
  */
6646
- async refreshDeps(slugs, nowMs = Date.now()) {
6647
- for (const slug of new Set(slugs)) {
6648
- if (!slug || this.depsProd.get(slug)?.outcome === "done") continue;
6649
- const asked = this.depsAskedAt.get(slug);
6650
- if (asked !== void 0 && nowMs - asked < _SwarmClient.DEPS_TTL_MS) continue;
6651
- this.depsAskedAt.set(slug, nowMs);
6652
- let res;
6653
- try {
6654
- res = await this.call("swarm_list_runs", { taskId: slug });
6655
- } catch {
6656
- this.depsAskedAt.delete(slug);
6657
- continue;
6658
- }
6659
- if (res?.isError) continue;
6660
- const runs = res.structuredContent?.runs ?? [];
6661
- const facts = runs.slice().sort((a, b) => String(a.createdAt ?? "").localeCompare(String(b.createdAt ?? ""))).map((r) => ({
6662
- status: String(r.status ?? ""),
6663
- // Служебный сабран под тем же слагом (резолвер конфликта / ревьюер): его финал —
6664
- // не финал задачи. Резолвер вычёркивался и до TASWM-361, ревьюер добавлен вместе
6665
- // с расширением критерия — иначе остановленный ревьюер закрывал бы зависимость.
6666
- service: Boolean(r.resolvesRunId) || Boolean(r.reviewsRunId) || Boolean(r.verifiesRunId),
6667
- // В проде имя дрона — колонка droneName (см. hydrateRun); `name` терпим как
6668
- // запасной вариант ради фикстур и старых ответов.
6669
- name: r.droneName ?? r.name ?? null,
6670
- stopReason: r.stopReason ?? null
6671
- }));
6672
- this.depsProd.set(slug, judgeDep(facts));
6864
+ async refreshDeps(refs, nowMs = Date.now()) {
6865
+ const ask = [...new Set(refs)].filter((ref) => {
6866
+ if (!ref || this.depsProd.get(ref)?.outcome === "done") return false;
6867
+ const asked = this.depsAskedAt.get(ref);
6868
+ return asked === void 0 || nowMs - asked >= _SwarmClient.DEPS_TTL_MS;
6869
+ });
6870
+ if (!ask.length) return;
6871
+ for (const ref of ask) this.depsAskedAt.set(ref, nowMs);
6872
+ let res;
6873
+ try {
6874
+ res = await this.call("swarm_deps_status", { refs: ask });
6875
+ } catch {
6876
+ for (const ref of ask) this.depsAskedAt.delete(ref);
6877
+ return;
6878
+ }
6879
+ if (res?.isError) return;
6880
+ const deps = res.structuredContent?.deps ?? [];
6881
+ for (const d of deps) {
6882
+ const ref = typeof d.ref === "string" ? d.ref : null;
6883
+ if (!ref) continue;
6884
+ const outcome = String(d.outcome ?? "pending");
6885
+ this.depsProd.set(ref, {
6886
+ outcome,
6887
+ met: Boolean(d.met),
6888
+ status: d.status ?? null,
6889
+ name: d.name ?? null,
6890
+ stopReason: d.stopReason ?? null
6891
+ });
6892
+ if (typeof d.slug === "string" && d.slug) this.depSlug.set(ref, d.slug);
6673
6893
  }
6674
6894
  }
6675
6895
  /**
@@ -6988,7 +7208,7 @@ var SwarmClient = class _SwarmClient {
6988
7208
  * транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
6989
7209
  */
6990
7210
  reconcileRuntime(opts) {
6991
- const wtExists = opts.worktreeExists ?? existsSync5;
7211
+ const wtExists = opts.worktreeExists ?? existsSync6;
6992
7212
  const pidByWorktree = /* @__PURE__ */ new Map();
6993
7213
  try {
6994
7214
  for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
@@ -7477,6 +7697,12 @@ var SwarmClient = class _SwarmClient {
7477
7697
  ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
7478
7698
  ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
7479
7699
  ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {},
7700
+ // TASWM-424: «доступ отозван» — состояние ровно того же рода, что пауза, и
7701
+ // формулировка у него ОДНА на все поверхности (HOST_ACCESS_REVOKED_TEXT):
7702
+ // `hostLines` зовёт тот же `hostStateText`, что и парк машин в /swarm. Без
7703
+ // этого поля `swarm hosts` показывал отвергнутую машину просто молчащей —
7704
+ // то самое различие, ради которого флаг и заводили (TASWM-324).
7705
+ ...typeof h.accessRevoked === "boolean" ? { accessRevoked: h.accessRevoked } : {},
7480
7706
  // Показаний нет (старый прод, машина со старым демоном) — ключа не заводим вовсе,
7481
7707
  // ровно как с паузой выше: «не знаем» и так значит «не знаем».
7482
7708
  ...resources ? { resources } : {}
@@ -7562,9 +7788,18 @@ var SwarmClient = class _SwarmClient {
7562
7788
  * ВСЕГДА (в т.ч. `storm:false`), как паузу: это состояние, а не команда, и молчание тут
7563
7789
  * означало бы «прод помнит вчерашний шторм», когда рой давно работает.
7564
7790
  *
7791
+ * TASWM-402: тем же пульсом уезжают КОРНИ WORKTREE'ЕВ этой машины (`worktreeRoots` —
7792
+ * `owner/repo` → `<worktreesDir>/<repo-basename>`). Это НЕ рантайм и правило «pid и путь
7793
+ * дрона в прод не уходят» не трогает: корень — статичная конфигурация машины, того же
7794
+ * рода, что реестр репо и ёмкость, он не зависит ни от прогона, ни от живости процесса и
7795
+ * не протухает между пульсами. Прод склеивает `<корень>/<слаг>` сам — и отвечает человеку
7796
+ * на вопрос «куда идти руками», когда команда машине не доиграла (а в этом случае, по
7797
+ * определению, спросить рантайм у молчащей машины уже нельзя). Не передали корень (не
7798
+ * знаем своего каталога) → ключа нет, прод прошлое не трогает — как с capacity/resources.
7799
+ *
7565
7800
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
7566
7801
  */
7567
- async heartbeat(daemonVersion, pause, capacity, resources, storm) {
7802
+ async heartbeat(daemonVersion, pause, capacity, resources, storm, worktreesDir, signIn) {
7568
7803
  try {
7569
7804
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
7570
7805
  if (daemonVersion) args.daemonVersion = daemonVersion;
@@ -7574,10 +7809,19 @@ var SwarmClient = class _SwarmClient {
7574
7809
  args.paused = !!pause?.paused;
7575
7810
  if (pause?.reason) args.pauseReason = pause.reason;
7576
7811
  }
7812
+ if (signIn) {
7813
+ args.signedIn = signIn.signedIn;
7814
+ if (signIn.webUrl) args.webUrl = signIn.webUrl;
7815
+ }
7577
7816
  if (storm !== void 0 && storm !== null) {
7578
7817
  args.storm = storm.active;
7579
7818
  if (storm.reason) args.stormReason = storm.reason;
7580
7819
  }
7820
+ if (worktreesDir) {
7821
+ const roots = {};
7822
+ for (const [full, path] of this.repoPathByFullName) roots[full] = droneWorktreeRoot(worktreesDir, path);
7823
+ if (Object.keys(roots).length) args.worktreeRoots = roots;
7824
+ }
7581
7825
  if (this.host) {
7582
7826
  args.hostId = this.host.hostId;
7583
7827
  args.hostName = this.host.hostName;
@@ -7726,10 +7970,20 @@ var SwarmClient = class _SwarmClient {
7726
7970
  * именно путь. Не нашли в реестре (репо не наш / ручной `--repo <path>` спавн, где
7727
7971
  * repo — ВСЕГДА уже путь, старый формат) → возвращаем как есть без изменений
7728
7972
  * (обратная совместимость: раньше pr.repo ВСЕГДА был локальным путём).
7973
+ *
7974
+ * РЕГИСТР НЕ РАЗЛИЧАЕМ (TASWM-422). Ключи карты приведены к нижнему регистру
7975
+ * (parseRepoFullName — «GitHub сам не различает case в owner/repo»), а прод присылает
7976
+ * `run.repo` тем регистром, каким репозиторий записан в проекте. 17.08 это разошлось:
7977
+ * машина объявляла `ilyasmurov/ansamblist`, задача пришла с `ilyasmurov/Ansamblist`,
7978
+ * `Map.get` промахнулся — и вместо пути в прогон уехало полное имя репозитория. Дальше
7979
+ * git-операции шли в несуществующий каталог, прогон умирал за 5 секунд, а задача
7980
+ * возвращалась в очередь и бралась снова: пять павших прогонов за пять минут.
7981
+ * Возвращаем ИСХОДНУЮ строку, а не приведённую: промах бывает и на локальном пути
7982
+ * (ручной спавн), а путь регистр различает — вернув lowercase, мы сломали бы его.
7729
7983
  */
7730
7984
  resolveRepo(raw) {
7731
7985
  if (!raw) return "";
7732
- return this.repoPathByFullName.get(raw) ?? raw;
7986
+ return this.repoPathByFullName.get(raw) ?? this.repoPathByFullName.get(raw.toLowerCase()) ?? raw;
7733
7987
  }
7734
7988
  /**
7735
7989
  * TASWM-281: МОЯ ли это работа. Три исхода:
@@ -7805,6 +8059,8 @@ var SwarmClient = class _SwarmClient {
7805
8059
  // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
7806
8060
  // в отличие от respawns, — авто-ревью v2, TASWM-162.
7807
8061
  review_cycle: 0,
8062
+ // TASWM-451: модель ПРОВЕРКИ — прод-поле, заполняется ниже из ответа (как base_branch).
8063
+ review_model: null,
7808
8064
  task_title: null,
7809
8065
  task_desc: null,
7810
8066
  task_url: null,
@@ -7857,6 +8113,7 @@ var SwarmClient = class _SwarmClient {
7857
8113
  local.area = pr.area ?? null;
7858
8114
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
7859
8115
  local.model = pr.model ?? null;
8116
+ local.review_model = pr.reviewModel ?? null;
7860
8117
  local.effort = toEffortLevel(pr.effort);
7861
8118
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
7862
8119
  local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
@@ -8167,20 +8424,20 @@ init_config();
8167
8424
  init_heartbeat();
8168
8425
 
8169
8426
  // src/logRotate.ts
8170
- import { copyFileSync, renameSync as renameSync4, statSync as statSync4, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
8427
+ import { copyFileSync, renameSync as renameSync5, statSync as statSync5, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync7 } from "fs";
8171
8428
  var realIo = {
8172
8429
  size: (path) => {
8173
8430
  try {
8174
- return statSync4(path).size;
8431
+ return statSync5(path).size;
8175
8432
  } catch {
8176
8433
  return null;
8177
8434
  }
8178
8435
  },
8179
8436
  copy: copyFileSync,
8180
- rename: renameSync4,
8437
+ rename: renameSync5,
8181
8438
  truncate: (path) => truncateSync(path, 0),
8182
8439
  unlink: unlinkSync2,
8183
- exists: existsSync6
8440
+ exists: existsSync7
8184
8441
  };
8185
8442
  var realOwnerIo = {
8186
8443
  statFd: (fd) => {
@@ -8193,7 +8450,7 @@ var realOwnerIo = {
8193
8450
  },
8194
8451
  statPath: (path) => {
8195
8452
  try {
8196
- const s = statSync4(path);
8453
+ const s = statSync5(path);
8197
8454
  return { dev: s.dev, ino: s.ino };
8198
8455
  } catch {
8199
8456
  return null;
@@ -8265,12 +8522,12 @@ async function buildRepoRegistry(paths) {
8265
8522
 
8266
8523
  // src/repoMemory.ts
8267
8524
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
8268
- import { join as join7 } from "path";
8525
+ import { join as join8 } from "path";
8269
8526
  var FILE = "repos.json";
8270
8527
  function loadRememberedRepos(home) {
8271
8528
  let raw;
8272
8529
  try {
8273
- raw = readFileSync6(join7(home, FILE), "utf8");
8530
+ raw = readFileSync6(join8(home, FILE), "utf8");
8274
8531
  } catch {
8275
8532
  return [];
8276
8533
  }
@@ -8285,7 +8542,7 @@ function loadRememberedRepos(home) {
8285
8542
  function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
8286
8543
  const clean = paths.filter((p) => p.trim().length > 0);
8287
8544
  if (clean.length === 0) return;
8288
- writeFileSync6(join7(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
8545
+ writeFileSync6(join8(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
8289
8546
  `);
8290
8547
  }
8291
8548
  function pickRepoPaths(args) {
@@ -8300,12 +8557,12 @@ init_src();
8300
8557
  init_config();
8301
8558
  init_linkFailure();
8302
8559
  import { homedir as homedir3 } from "os";
8303
- import { readFileSync as readFileSync9, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
8304
- import { isAbsolute, join as join10, resolve as resolve2 } from "path";
8560
+ import { readFileSync as readFileSync9, renameSync as renameSync6, writeFileSync as writeFileSync8 } from "fs";
8561
+ import { isAbsolute, join as join11, resolve as resolve2 } from "path";
8305
8562
 
8306
8563
  // src/repoScan.ts
8307
- import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
8308
- import { join as join9 } from "path";
8564
+ import { readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
8565
+ import { join as join10 } from "path";
8309
8566
  var SKIP = /* @__PURE__ */ new Set([
8310
8567
  "node_modules",
8311
8568
  "vendor",
@@ -8335,7 +8592,7 @@ function parseOriginFromGitConfig(text) {
8335
8592
  }
8336
8593
  function originOf(dir) {
8337
8594
  try {
8338
- return parseOriginFromGitConfig(readFileSync8(join9(dir, ".git", "config"), "utf8"));
8595
+ return parseOriginFromGitConfig(readFileSync8(join10(dir, ".git", "config"), "utf8"));
8339
8596
  } catch {
8340
8597
  return null;
8341
8598
  }
@@ -8364,15 +8621,15 @@ function walk(dir, depth, maxDepth, found) {
8364
8621
  }
8365
8622
  let entries;
8366
8623
  try {
8367
- entries = readdirSync5(dir);
8624
+ entries = readdirSync6(dir);
8368
8625
  } catch {
8369
8626
  return;
8370
8627
  }
8371
8628
  for (const name of entries) {
8372
8629
  if (name.startsWith(".") || SKIP.has(name)) continue;
8373
- const child = join9(dir, name);
8630
+ const child = join10(dir, name);
8374
8631
  try {
8375
- if (!statSync5(child).isDirectory()) continue;
8632
+ if (!statSync6(child).isDirectory()) continue;
8376
8633
  } catch {
8377
8634
  continue;
8378
8635
  }
@@ -8392,7 +8649,7 @@ function defaultScanRoots(home, knownPaths = []) {
8392
8649
  if (parent && parent !== p) add(parent);
8393
8650
  }
8394
8651
  for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
8395
- add(join9(home, name));
8652
+ add(join10(home, name));
8396
8653
  }
8397
8654
  return out;
8398
8655
  }
@@ -8406,7 +8663,7 @@ function resolveScanRoots(raw, home = homedir3(), knownPaths = []) {
8406
8663
  for (const part of v.split(",")) {
8407
8664
  const p = part.trim();
8408
8665
  if (!p) continue;
8409
- const abs = p === "~" ? home : p.startsWith("~/") ? join10(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
8666
+ const abs = p === "~" ? home : p.startsWith("~/") ? join11(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
8410
8667
  if (!out.includes(abs)) out.push(abs);
8411
8668
  }
8412
8669
  return out;
@@ -8471,6 +8728,28 @@ function describeReposFailure(kind, detail) {
8471
8728
  function emptyRepoMapState(roots) {
8472
8729
  return { clones: [], missing: [], roots: [...roots], at: null, unknown: true };
8473
8730
  }
8731
+ var SCAN_REASON_TEXT = {
8732
+ "list-unknown": "\u043F\u0440\u043E\u0434 \u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043B, \u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443 \u0440\u043E\u044F \u2014 \u0441\u043C\u043E\u0442\u0440\u044E \u0441\u0430\u043C, \u0447\u0435\u0433\u043E \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u043F\u0440\u0438\u0431\u0430\u0432\u0438\u043B\u043E\u0441\u044C",
8733
+ "wanted-missing": "\u0438\u0437 \u0441\u043F\u0438\u0441\u043A\u0430 \u0440\u043E\u044F \u0447\u0435\u0433\u043E-\u0442\u043E \u043D\u0435\u0442 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435",
8734
+ "cache-expired": "\u043A\u044D\u0448 \u043A\u0430\u0440\u0442\u044B \u043F\u0440\u043E\u0442\u0443\u0445"
8735
+ };
8736
+ var REPO_SCAN_TTL_MS = 24 * 60 * 6e4;
8737
+ var UNKNOWN_LIST_SCAN_TTL_MS = 60 * 6e4;
8738
+ function isScanStale(scannedAt, ttlMs, now = Date.now()) {
8739
+ if (!scannedAt) return true;
8740
+ const t = stampMs(scannedAt);
8741
+ if (!Number.isFinite(t)) return true;
8742
+ return now - t >= ttlMs;
8743
+ }
8744
+ function decideScan(opts) {
8745
+ if (!opts.known) {
8746
+ const ttl = Math.min(opts.ttlMs, UNKNOWN_LIST_SCAN_TTL_MS);
8747
+ return isScanStale(opts.scannedAt, ttl, opts.now) ? { scan: true, reason: "list-unknown" } : { scan: false, reason: null };
8748
+ }
8749
+ if (opts.missingWanted > 0) return { scan: true, reason: "wanted-missing" };
8750
+ if (isScanStale(opts.scannedAt, opts.ttlMs, opts.now)) return { scan: true, reason: "cache-expired" };
8751
+ return { scan: false, reason: null };
8752
+ }
8474
8753
  async function refreshRepoMap(opts) {
8475
8754
  const log = opts.log ?? (() => {
8476
8755
  });
@@ -8484,14 +8763,21 @@ async function refreshRepoMap(opts) {
8484
8763
  clones.set(repoFullName, { repoFullName, path, source, alternatives });
8485
8764
  };
8486
8765
  for (const e of opts.configured) take(e.repoFullName.toLowerCase(), e.path, "configured");
8487
- for (const e of readRepoMapCache(opts.cachePath)) {
8766
+ const cache = loadRepoMapCache(opts.cachePath);
8767
+ for (const e of cache.entries) {
8488
8768
  const name = e.repoFullName.toLowerCase();
8489
8769
  if (clones.has(name)) continue;
8490
8770
  if (repoAt(e.path) === name) take(name, e.path, "cache");
8491
8771
  }
8492
8772
  const wantedNames = wanted ? normalizeFullNames(wanted.map((r) => r.repoFullName)) : [];
8493
- const needScan = wanted ? wantedNames.some((n) => !clones.has(n)) : clones.size === 0;
8494
- if (needScan) {
8773
+ const decision = decideScan({
8774
+ known: !!wanted,
8775
+ missingWanted: wantedNames.filter((n) => !clones.has(n)).length,
8776
+ scannedAt: cache.scannedAt,
8777
+ ttlMs: opts.scanTtlMs ?? REPO_SCAN_TTL_MS
8778
+ });
8779
+ let scannedAt = cache.scannedAt;
8780
+ if (decision.scan) {
8495
8781
  const startedAt = Date.now();
8496
8782
  const scanned = scan(opts.roots, opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {});
8497
8783
  let added = 0;
@@ -8500,9 +8786,10 @@ async function refreshRepoMap(opts) {
8500
8786
  take(full, paths[0], "scan", paths.slice(1));
8501
8787
  added++;
8502
8788
  }
8503
- if (added > 0) {
8504
- log(`[overmind] \u043D\u0430\u0448\u0451\u043B \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 ${added} \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439(\u0435\u0432) \u0441\u043A\u0430\u043D\u043E\u043C \u0437\u0430 ${Date.now() - startedAt}\u043C\u0441`);
8505
- }
8789
+ scannedAt = stamp();
8790
+ log(
8791
+ `[overmind] \u043F\u0440\u043E\u0448\u0451\u043B \u043F\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C (${SCAN_REASON_TEXT[decision.reason]}): ${added > 0 ? `\u043D\u0430\u0448\u0451\u043B \u0435\u0449\u0451 ${added}` : "\u043D\u043E\u0432\u043E\u0433\u043E \u043D\u0435 \u043D\u0430\u0448\u0451\u043B"}, \u0432\u0441\u0435\u0433\u043E \u043A\u043B\u043E\u043D\u043E\u0432 ${clones.size}, \u0437\u0430 ${Date.now() - startedAt}\u043C\u0441. \u0418\u0441\u043A\u0430\u043B \u0432: ${opts.roots.length ? opts.roots.slice(0, 6).join(", ") : "\u043D\u0438\u0433\u0434\u0435 \u2014 \u043F\u043E\u0438\u0441\u043A \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D (SWARM_SCAN_ROOTS)"}`
8792
+ );
8506
8793
  }
8507
8794
  const registry = [...clones.values()].map((c) => ({
8508
8795
  repoFullName: c.repoFullName,
@@ -8510,10 +8797,12 @@ async function refreshRepoMap(opts) {
8510
8797
  }));
8511
8798
  writeRepoMapCache(
8512
8799
  opts.cachePath,
8513
- [...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path }))
8800
+ [...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path })),
8801
+ scannedAt
8514
8802
  );
8515
8803
  if (!answer.ok) {
8516
8804
  log(describeReposFailure(answer.kind, answer.detail));
8805
+ log(`[overmind] \u0438\u0442\u043E\u0433: \u043A\u043B\u043E\u043D\u043E\u0432 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E ${clones.size}, \u0441\u043F\u0438\u0441\u043E\u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0440\u043E\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D (\u0441\u043C. \u0441\u0442\u0440\u043E\u043A\u0443 \u0432\u044B\u0448\u0435)`);
8517
8806
  return registry;
8518
8807
  }
8519
8808
  opts.state.unknown = false;
@@ -8525,7 +8814,7 @@ async function refreshRepoMap(opts) {
8525
8814
  log("[overmind] \u0443 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043D\u0435\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 \u0441 \u0432\u043A\u043B\u044E\u0447\u0451\u043D\u043D\u044B\u043C \u0440\u043E\u0435\u043C \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u043F\u043E \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u043C\u0443 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435");
8526
8815
  return registry;
8527
8816
  }
8528
- log(`[overmind] \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0440\u043E\u044F (${wantedNames.length}), \u043A\u0430\u0440\u0442\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435:`);
8817
+ log(`[overmind] ${describeMapSummary(opts.state, opts.roots)}`);
8529
8818
  for (const c of opts.state.clones) log(`[overmind] ${describeClone(c)}`);
8530
8819
  const prefixOf = new Map(answer.repos.map((r) => [r.repoFullName, r.taskPrefix]));
8531
8820
  for (const name of opts.state.missing) {
@@ -8534,6 +8823,11 @@ async function refreshRepoMap(opts) {
8534
8823
  }
8535
8824
  return registry;
8536
8825
  }
8826
+ function describeMapSummary(state, roots) {
8827
+ const found = state.clones.length;
8828
+ const where = roots.length ? roots.slice(0, 6).join(", ") : "\u043F\u043E\u0438\u0441\u043A \u043F\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D (SWARM_SCAN_ROOTS)";
8829
+ return `\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0440\u043E\u044F (${found + state.missing.length}): \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 ${found}, \u0431\u0435\u0437 \u043A\u043B\u043E\u043D\u0430 ${state.missing.length}. \u0418\u0441\u043A\u0430\u043B \u0432: ${where}`;
8830
+ }
8537
8831
  function describeClone(c) {
8538
8832
  const how = c.source === "configured" ? "\u043F\u0443\u0442\u044C \u0437\u0430\u0434\u0430\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C" : c.source === "cache" ? "\u0438\u0437 \u043A\u044D\u0448\u0430 \u043C\u0430\u0448\u0438\u043D\u044B" : "\u043D\u0430\u0439\u0434\u0435\u043D \u0441\u043A\u0430\u043D\u043E\u043C";
8539
8833
  const alt = c.alternatives.length ? ` \u26A0 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u0435\u0449\u0451 ${c.alternatives.length}: ${c.alternatives.join(", ")} \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u0432 \u043F\u0435\u0440\u0432\u043E\u043C \u043F\u043E \u043F\u043E\u0440\u044F\u0434\u043A\u0443 \u043A\u043E\u0440\u043D\u0435\u0439` : "";
@@ -8554,40 +8848,43 @@ function repoMapRows(state) {
8554
8848
  ...state.missing.map((repoFullName) => ({ repoFullName, path: null, source: null, alternatives: [] }))
8555
8849
  ];
8556
8850
  }
8557
- function readRepoMapCache(path) {
8851
+ function loadRepoMapCache(path) {
8558
8852
  try {
8559
8853
  const raw = JSON.parse(readFileSync9(path, "utf8"));
8560
- if (!Array.isArray(raw?.entries)) return [];
8561
- return raw.entries.filter(
8562
- (e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
8563
- );
8854
+ if (!Array.isArray(raw?.entries)) return { scannedAt: null, entries: [] };
8855
+ return {
8856
+ scannedAt: typeof raw.scannedAt === "string" && raw.scannedAt ? raw.scannedAt : null,
8857
+ entries: raw.entries.filter(
8858
+ (e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
8859
+ )
8860
+ };
8564
8861
  } catch {
8565
- return [];
8862
+ return { scannedAt: null, entries: [] };
8566
8863
  }
8567
8864
  }
8568
- function writeRepoMapCache(path, entries) {
8865
+ function writeRepoMapCache(path, entries, scannedAt = stamp()) {
8569
8866
  try {
8570
8867
  const tmp = `${path}.tmp`;
8571
- writeFileSync8(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
8572
- renameSync5(tmp, path);
8868
+ writeFileSync8(tmp, JSON.stringify({ at: stamp(), scannedAt, entries }, null, 2));
8869
+ renameSync6(tmp, path);
8573
8870
  } catch {
8574
8871
  }
8575
8872
  }
8576
8873
 
8577
8874
  // src/hostIdentity.ts
8578
8875
  import { hostname } from "os";
8579
- import { existsSync as existsSync7, readFileSync as readFileSync10, renameSync as renameSync6, writeFileSync as writeFileSync9 } from "fs";
8580
- import { join as join11 } from "path";
8876
+ import { existsSync as existsSync8, readFileSync as readFileSync10, renameSync as renameSync7, writeFileSync as writeFileSync9 } from "fs";
8877
+ import { join as join12 } from "path";
8581
8878
  import { randomUUID as randomUUID2 } from "crypto";
8582
8879
  function hostIdentityPath(home) {
8583
- return join11(home, "host.json");
8880
+ return join12(home, "host.json");
8584
8881
  }
8585
8882
  function loadHostIdentity(deps) {
8586
- const exists = deps.exists ?? existsSync7;
8883
+ const exists = deps.exists ?? existsSync8;
8587
8884
  const readFile2 = deps.readFile ?? ((p) => readFileSync10(p, "utf8"));
8588
8885
  const writeFile3 = deps.writeFile ?? ((p, data) => {
8589
8886
  writeFileSync9(`${p}.tmp`, data);
8590
- renameSync6(`${p}.tmp`, p);
8887
+ renameSync7(`${p}.tmp`, p);
8591
8888
  });
8592
8889
  const host = deps.hostname ?? hostname;
8593
8890
  const newId = deps.newId ?? randomUUID2;
@@ -8769,15 +9066,15 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
8769
9066
 
8770
9067
  // src/launcher.ts
8771
9068
  import { spawn } from "child_process";
8772
- import { closeSync as closeSync2, existsSync as existsSync8, mkdirSync as mkdirSync6, openSync as openSync2, readdirSync as readdirSync7 } from "fs";
9069
+ import { closeSync as closeSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, openSync as openSync2, readdirSync as readdirSync8 } from "fs";
8773
9070
  import { createRequire } from "module";
8774
9071
  import { homedir as homedir4 } from "os";
8775
- import { dirname as dirname2, join as join13 } from "path";
9072
+ import { dirname as dirname2, join as join14 } from "path";
8776
9073
  import { fileURLToPath } from "url";
8777
9074
 
8778
9075
  // src/droneHome.ts
8779
- import { lstatSync, mkdirSync as mkdirSync5, readdirSync as readdirSync6, rmSync, statSync as statSync6, symlinkSync as symlinkSync2 } from "fs";
8780
- import { join as join12, relative, sep } from "path";
9076
+ import { lstatSync, mkdirSync as mkdirSync6, readdirSync as readdirSync7, rmSync, statSync as statSync7, symlinkSync as symlinkSync2 } from "fs";
9077
+ import { join as join13, relative, sep } from "path";
8781
9078
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
8782
9079
  function droneHomeDeny(env = process.env) {
8783
9080
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -8795,29 +9092,29 @@ function buildDroneHome(opts) {
8795
9092
  const { realHome, droneHome } = opts;
8796
9093
  const deny = opts.deny ?? droneHomeDeny();
8797
9094
  const platform = opts.platform ?? process.platform;
8798
- mkdirSync5(droneHome, { recursive: true, mode: 448 });
8799
- const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
9095
+ mkdirSync6(droneHome, { recursive: true, mode: 448 });
9096
+ const wanted = new Set(mirroredEntries(readdirSync7(realHome), deny, platform));
8800
9097
  const rel = relative(realHome, droneHome);
8801
9098
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
8802
- for (const existing of readdirSync6(droneHome)) {
9099
+ for (const existing of readdirSync7(droneHome)) {
8803
9100
  if (wanted.has(existing)) continue;
8804
- const p = join12(droneHome, existing);
9101
+ const p = join13(droneHome, existing);
8805
9102
  try {
8806
9103
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
8807
9104
  } catch {
8808
9105
  }
8809
9106
  }
8810
9107
  for (const name of wanted) {
8811
- const link = join12(droneHome, name);
9108
+ const link = join13(droneHome, name);
8812
9109
  try {
8813
9110
  lstatSync(link);
8814
9111
  continue;
8815
9112
  } catch {
8816
9113
  }
8817
- const target = join12(realHome, name);
9114
+ const target = join13(realHome, name);
8818
9115
  let isDir;
8819
9116
  try {
8820
- isDir = statSync6(target).isDirectory();
9117
+ isDir = statSync7(target).isDirectory();
8821
9118
  } catch {
8822
9119
  continue;
8823
9120
  }
@@ -8835,7 +9132,7 @@ var REAL_PATH_ENV_PINS = [
8835
9132
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
8836
9133
  ];
8837
9134
  function realPathEnvPins(realHome) {
8838
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join12(realHome, ...segments)]));
9135
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join13(realHome, ...segments)]));
8839
9136
  }
8840
9137
  function dronePrivatePaths(realHome, swarmHome) {
8841
9138
  return [
@@ -8895,12 +9192,26 @@ function pick(v) {
8895
9192
  const s = typeof v === "string" ? v.trim() : "";
8896
9193
  return s.length > 0 ? s : null;
8897
9194
  }
9195
+ function isReviewKind(kind) {
9196
+ return kind === "reviewer" || kind === "verifier";
9197
+ }
9198
+ function modelRoleOf(kind) {
9199
+ return isReviewKind(kind) ? "review" : "executor";
9200
+ }
8898
9201
  function forKind(kind, swarm, norm) {
8899
9202
  if (!swarm) return null;
8900
- return kind === "reviewer" ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
9203
+ return isReviewKind(kind) ? norm(swarm.reviewer) ?? norm(swarm.drone) : norm(swarm.drone);
9204
+ }
9205
+ function resolveRunModelRef(run, swarm, env = process.env) {
9206
+ const configured = parseModelRef(
9207
+ pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL)
9208
+ );
9209
+ if (configured) return configured;
9210
+ const providerId = pick(env.SWARM_MODEL_PROVIDER) ?? DEFAULT_PROVIDER_ID;
9211
+ return { providerId, model: defaultModelFor(modelRoleOf(run.kind), providerId) };
8901
9212
  }
8902
9213
  function resolveRunModel(run, swarm, env = process.env) {
8903
- return pick(run.model) ?? forKind(run.kind, swarm, pick) ?? pick(env.SWARM_DRONE_MODEL) ?? null;
9214
+ return resolveRunModelRef(run, swarm, env).model;
8904
9215
  }
8905
9216
  function resolveRunEffort(run, swarm, env = process.env) {
8906
9217
  return toEffortLevel(run.effort) ?? forKind(run.kind, swarm, toEffortLevel) ?? toEffortLevel(env.SWARM_DRONE_EFFORT) ?? null;
@@ -8933,7 +9244,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
8933
9244
  const env = { ...base3 };
8934
9245
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
8935
9246
  if (isolate && !base3.TURBO_CACHE_DIR) {
8936
- env.TURBO_CACHE_DIR = join13(worktree, ".turbo", "cache");
9247
+ env.TURBO_CACHE_DIR = join14(worktree, ".turbo", "cache");
8937
9248
  }
8938
9249
  Object.assign(env, droneTestEnv(droneCap, base3));
8939
9250
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -8961,20 +9272,21 @@ var DAEMON_STATE_ENV = [
8961
9272
  "TASKLESS_TOKEN"
8962
9273
  ];
8963
9274
  function bundledRoleDir(kind) {
8964
- return join13(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
9275
+ return join14(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
8965
9276
  }
8966
9277
  function roleDir(worktree, kind) {
8967
- return join13(worktree, ".claude", "roles", kind);
9278
+ return join14(worktree, ".claude", "roles", kind);
8968
9279
  }
8969
9280
  function roleDirs(worktree, kind) {
8970
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync8(dir));
9281
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync9(dir));
8971
9282
  }
8972
- function nextRunLogPath(runLogsDir, runId) {
8973
- const dir = join13(runLogsDir, String(runId));
8974
- mkdirSync6(dir, { recursive: true });
9283
+ function nextRunLogPath(runLogsDir, runLogKeys) {
9284
+ const dir = runLogDirForWrite(runLogsDir, runLogKeys);
9285
+ 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)");
9286
+ mkdirSync7(dir, { recursive: true });
8975
9287
  let files = [];
8976
9288
  try {
8977
- files = readdirSync7(dir);
9289
+ files = readdirSync8(dir);
8978
9290
  } catch {
8979
9291
  }
8980
9292
  let next = 0;
@@ -8982,11 +9294,11 @@ function nextRunLogPath(runLogsDir, runId) {
8982
9294
  const m = /^(\d+)\.json/.exec(f);
8983
9295
  if (m) next = Math.max(next, Number(m[1]) + 1);
8984
9296
  }
8985
- return join13(dir, `${next}.json`);
9297
+ return join14(dir, `${next}.json`);
8986
9298
  }
8987
- function openNextRunLog(runLogsDir, runId) {
9299
+ function openNextRunLog(runLogsDir, runLogKeys) {
8988
9300
  for (let attempt = 0; attempt < 50; attempt++) {
8989
- const path = nextRunLogPath(runLogsDir, runId);
9301
+ const path = nextRunLogPath(runLogsDir, runLogKeys);
8990
9302
  let out;
8991
9303
  try {
8992
9304
  out = openSync2(path, "wx");
@@ -9002,7 +9314,7 @@ function openNextRunLog(runLogsDir, runId) {
9002
9314
  }
9003
9315
  return { out, err };
9004
9316
  }
9005
- 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?)`);
9317
+ 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?`);
9006
9318
  }
9007
9319
  function makeLauncher(cfg) {
9008
9320
  return ({ worktree, run, prompt, continueSession }) => {
@@ -9065,9 +9377,9 @@ function makeLauncher(cfg) {
9065
9377
  }
9066
9378
  const realHome = cfg.droneIsolation?.realHome ?? homedir4();
9067
9379
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
9068
- if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join13(cfg.droneIsolation.swarmHome, "token"))) {
9380
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync9(join14(cfg.droneIsolation.swarmHome, "token"))) {
9069
9381
  console.error(
9070
- `[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 ${join13(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.`
9382
+ `[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 ${join14(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.`
9071
9383
  );
9072
9384
  }
9073
9385
  const executor = cfg.executor ?? resolveExecutor();
@@ -9094,7 +9406,13 @@ function makeLauncher(cfg) {
9094
9406
  let outFd;
9095
9407
  let errFd;
9096
9408
  if (cfg.runLogsDir) {
9097
- const fds = openNextRunLog(cfg.runLogsDir, run.id);
9409
+ const logKeys = cfg.runLogKeys?.(run) ?? [];
9410
+ if (logKeys.length === 0) {
9411
+ throw new Error(
9412
+ `\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).`
9413
+ );
9414
+ }
9415
+ const fds = openNextRunLog(cfg.runLogsDir, logKeys);
9098
9416
  outFd = fds.out;
9099
9417
  errFd = fds.err ?? void 0;
9100
9418
  stdio = ["ignore", outFd, errFd ?? "ignore"];
@@ -9122,21 +9440,21 @@ function makeLauncher(cfg) {
9122
9440
  init_procCleanup();
9123
9441
 
9124
9442
  // src/cost.ts
9443
+ import { readFileSync as readFileSync11, readdirSync as readdirSync9, renameSync as renameSync8 } from "fs";
9444
+ import { join as join15 } from "path";
9125
9445
  init_executors();
9126
- import { readFileSync as readFileSync11, readdirSync as readdirSync8, renameSync as renameSync7 } from "fs";
9127
- import { join as join14 } from "path";
9128
9446
  var UNPROCESSED_LOG_RE = /^\d+\.json$/;
9129
9447
  function unprocessedLogFiles(dir) {
9130
9448
  try {
9131
- return readdirSync8(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
9449
+ return readdirSync9(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
9132
9450
  } catch {
9133
9451
  return [];
9134
9452
  }
9135
9453
  }
9136
9454
  function accountRunCost(deps, run) {
9137
- const dir = join14(deps.runLogsDir, String(run.id));
9138
- const files = unprocessedLogFiles(dir);
9139
- if (files.length === 0) return;
9455
+ const dir = findRunLogDir(deps.runLogsDir, deps.db.runLogKeys(run.id));
9456
+ const files = dir ? unprocessedLogFiles(dir) : [];
9457
+ if (!dir || files.length === 0) return;
9140
9458
  const model = resolveRunModel(run, deps.swarmModels?.() ?? null);
9141
9459
  const executor = deps.executor ?? resolveExecutor();
9142
9460
  let added = 0;
@@ -9145,7 +9463,7 @@ function accountRunCost(deps, run) {
9145
9463
  let durationMs = 0;
9146
9464
  const processed = [];
9147
9465
  for (const f of files) {
9148
- const full = join14(dir, f);
9466
+ const full = join15(dir, f);
9149
9467
  let raw = null;
9150
9468
  try {
9151
9469
  raw = readFileSync11(full, "utf8");
@@ -9174,7 +9492,7 @@ function accountRunCost(deps, run) {
9174
9492
  }
9175
9493
  for (const full of processed) {
9176
9494
  try {
9177
- renameSync7(full, `${full}.done`);
9495
+ renameSync8(full, `${full}.done`);
9178
9496
  } catch {
9179
9497
  }
9180
9498
  }
@@ -9182,20 +9500,34 @@ function accountRunCost(deps, run) {
9182
9500
  function makeCostAccountant(deps) {
9183
9501
  return (run) => accountRunCost(deps, run);
9184
9502
  }
9503
+ function sweepPendingRunCosts(deps) {
9504
+ for (const run of deps.db.listRuns()) {
9505
+ if (run.pid != null && deps.isAlive(run.pid)) continue;
9506
+ try {
9507
+ accountRunCost(deps, run);
9508
+ } catch (e) {
9509
+ console.error(`[cost] \u043F\u0440\u043E\u0433\u043E\u043D #${run.id}: \u0443\u0447\u0451\u0442 \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F \u2014 ${String(e)}`);
9510
+ }
9511
+ }
9512
+ }
9513
+ function makeCostSweeper(deps) {
9514
+ return () => sweepPendingRunCosts(deps);
9515
+ }
9185
9516
 
9186
9517
  // src/runOutcome.ts
9518
+ import { readdirSync as readdirSync10, readFileSync as readFileSync12 } from "fs";
9519
+ import { join as join16 } from "path";
9187
9520
  init_executors();
9188
- import { readdirSync as readdirSync9, readFileSync as readFileSync12 } from "fs";
9189
- import { join as join15 } from "path";
9190
9521
  function endedItself(outcome) {
9191
9522
  return outcome === "graceful" || outcome === "budget";
9192
9523
  }
9193
9524
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
9194
- function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
9195
- const dir = join15(runLogsDir, String(runId));
9525
+ function classifyExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
9526
+ const dir = findRunLogDir(runLogsDir, runLogKeys);
9527
+ if (!dir) return "unknown";
9196
9528
  let files;
9197
9529
  try {
9198
- files = readdirSync9(dir);
9530
+ files = readdirSync10(dir);
9199
9531
  } catch {
9200
9532
  return "unknown";
9201
9533
  }
@@ -9209,7 +9541,7 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
9209
9541
  if (!latest) return "unknown";
9210
9542
  let raw;
9211
9543
  try {
9212
- raw = readFileSync12(join15(dir, latest.name), "utf8");
9544
+ raw = readFileSync12(join16(dir, latest.name), "utf8");
9213
9545
  } catch {
9214
9546
  return "unknown";
9215
9547
  }
@@ -9217,9 +9549,9 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
9217
9549
  if (!result) return "crash";
9218
9550
  return result.stoppedByBudget ? "budget" : "graceful";
9219
9551
  }
9220
- function makeExitClassifier(runLogsDir, executor) {
9552
+ function makeExitClassifier(runLogsDir, runLogKeys, executor) {
9221
9553
  const exec = executor ?? resolveExecutor();
9222
- return (run) => classifyExit(runLogsDir, run.id, exec);
9554
+ return (run) => classifyExit(runLogsDir, runLogKeys(run.id), exec);
9223
9555
  }
9224
9556
 
9225
9557
  // src/orchestrator.ts
@@ -9227,20 +9559,20 @@ init_src();
9227
9559
  init_git();
9228
9560
 
9229
9561
  // src/deps.ts
9230
- import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync13 } from "fs";
9231
- import { join as join18 } from "path";
9562
+ import { existsSync as existsSync11, readdirSync as readdirSync12, readFileSync as readFileSync13 } from "fs";
9563
+ import { join as join19 } from "path";
9232
9564
  import { execa } from "execa";
9233
9565
 
9234
9566
  // src/prepareLock.ts
9235
9567
  import { createHash } from "crypto";
9236
- import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as statSync8, writeSync } from "fs";
9237
- import { join as join17 } from "path";
9568
+ import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as statSync9, writeSync } from "fs";
9569
+ import { join as join18 } from "path";
9238
9570
  import { tmpdir } from "os";
9239
9571
  var WAIT_TIMEOUT_MS = 5 * 6e4;
9240
9572
  var POLL_MS = 300;
9241
9573
  var STALE_MS = 20 * 6e4;
9242
9574
  function machineLockPath(key, dir = tmpdir()) {
9243
- return join17(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
9575
+ return join18(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
9244
9576
  }
9245
9577
  function defaultTryCreate(path, content) {
9246
9578
  try {
@@ -9257,7 +9589,7 @@ function defaultTryCreate(path, content) {
9257
9589
  }
9258
9590
  function defaultAgeMs(path) {
9259
9591
  try {
9260
- return Date.now() - statSync8(path).mtimeMs;
9592
+ return Date.now() - statSync9(path).mtimeMs;
9261
9593
  } catch {
9262
9594
  return null;
9263
9595
  }
@@ -9321,14 +9653,14 @@ function safeReadText(p) {
9321
9653
  }
9322
9654
  function safeListDirs(p) {
9323
9655
  try {
9324
- return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
9656
+ return readdirSync12(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
9325
9657
  } catch {
9326
9658
  return [];
9327
9659
  }
9328
9660
  }
9329
9661
  function safeListFiles(p) {
9330
9662
  try {
9331
- return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
9663
+ return readdirSync12(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
9332
9664
  } catch {
9333
9665
  return [];
9334
9666
  }
@@ -9354,21 +9686,21 @@ function overridePlan(override) {
9354
9686
  };
9355
9687
  }
9356
9688
  function detectPackageManager(worktree, io = {}) {
9357
- const exists = io.exists ?? existsSync10;
9689
+ const exists = io.exists ?? existsSync11;
9358
9690
  const readText = io.readText ?? safeReadText;
9359
- const hasPkgJson = exists(join18(worktree, "package.json"));
9691
+ const hasPkgJson = exists(join19(worktree, "package.json"));
9360
9692
  if (!hasPkgJson) {
9361
- if (exists(join18(worktree, "deno.json")) || exists(join18(worktree, "deno.jsonc")) || exists(join18(worktree, "deno.lock"))) return "deno";
9693
+ if (exists(join19(worktree, "deno.json")) || exists(join19(worktree, "deno.jsonc")) || exists(join19(worktree, "deno.lock"))) return "deno";
9362
9694
  return null;
9363
9695
  }
9364
- const pkg = readJson(readText, join18(worktree, "package.json"));
9696
+ const pkg = readJson(readText, join19(worktree, "package.json"));
9365
9697
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
9366
9698
  if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
9367
- if (exists(join18(worktree, "bun.lock")) || exists(join18(worktree, "bun.lockb"))) return "bun";
9368
- if (exists(join18(worktree, "pnpm-lock.yaml"))) return "pnpm";
9369
- if (exists(join18(worktree, "yarn.lock"))) return "yarn";
9370
- if (exists(join18(worktree, "package-lock.json"))) return "npm";
9371
- if (exists(join18(worktree, "deno.lock"))) return "deno";
9699
+ if (exists(join19(worktree, "bun.lock")) || exists(join19(worktree, "bun.lockb"))) return "bun";
9700
+ if (exists(join19(worktree, "pnpm-lock.yaml"))) return "pnpm";
9701
+ if (exists(join19(worktree, "yarn.lock"))) return "yarn";
9702
+ if (exists(join19(worktree, "package-lock.json"))) return "npm";
9703
+ if (exists(join19(worktree, "deno.lock"))) return "deno";
9372
9704
  return "npm";
9373
9705
  }
9374
9706
  function jsInstallCommand(pm) {
@@ -9421,7 +9753,7 @@ var ECOSYSTEM_MARKERS = [
9421
9753
  // конфигурация — дело сборки, не подготовки
9422
9754
  ];
9423
9755
  function detectPlan(worktree, io) {
9424
- const exists = io.exists ?? existsSync10;
9756
+ const exists = io.exists ?? existsSync11;
9425
9757
  const listFiles = io.listFiles ?? safeListFiles;
9426
9758
  const pm = detectPackageManager(worktree, io);
9427
9759
  if (pm) {
@@ -9429,7 +9761,7 @@ function detectPlan(worktree, io) {
9429
9761
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
9430
9762
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
9431
9763
  // заново, но он дешёвый: тянет из того же общего кэша.
9432
- { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join18(worktree, "node_modules"), source: "detect" }
9764
+ { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join19(worktree, "node_modules"), source: "detect" }
9433
9765
  ];
9434
9766
  for (const target of workspaceBuildTargets(worktree, io)) {
9435
9767
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
@@ -9438,12 +9770,12 @@ function detectPlan(worktree, io) {
9438
9770
  return { source: "detect", ecosystem: `node/${pm}`, commands };
9439
9771
  }
9440
9772
  for (const marker of ECOSYSTEM_MARKERS) {
9441
- const hit = marker.files.some((f) => exists(join18(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
9773
+ const hit = marker.files.some((f) => exists(join19(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
9442
9774
  if (!hit) continue;
9443
9775
  let ecosystem = marker.ecosystem;
9444
9776
  let command = marker.command;
9445
9777
  for (const refine of marker.refine ?? []) {
9446
- if (!exists(join18(worktree, refine.file))) continue;
9778
+ if (!exists(join19(worktree, refine.file))) continue;
9447
9779
  ecosystem = refine.ecosystem;
9448
9780
  command = refine.command;
9449
9781
  break;
@@ -9500,12 +9832,12 @@ var CI_INSTALL_PATTERNS = [
9500
9832
  function ciWorkflowPlan(worktree, io) {
9501
9833
  const readText = io.readText ?? safeReadText;
9502
9834
  const listFiles = io.listFiles ?? safeListFiles;
9503
- const dir = join18(worktree, ".github", "workflows");
9835
+ const dir = join19(worktree, ".github", "workflows");
9504
9836
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
9505
9837
  const seen = /* @__PURE__ */ new Set();
9506
9838
  const commands = [];
9507
9839
  for (const name of files.sort()) {
9508
- const yaml = readText(join18(dir, name));
9840
+ const yaml = readText(join19(dir, name));
9509
9841
  if (yaml === null) continue;
9510
9842
  for (const raw of workflowRunLines(yaml)) {
9511
9843
  const line = raw.replace(/\s+/g, " ").trim();
@@ -9520,7 +9852,7 @@ function ciWorkflowPlan(worktree, io) {
9520
9852
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
9521
9853
  }
9522
9854
  function workspaceGlobs(worktree, readText) {
9523
- const yaml = readText(join18(worktree, "pnpm-workspace.yaml"));
9855
+ const yaml = readText(join19(worktree, "pnpm-workspace.yaml"));
9524
9856
  if (yaml !== null) {
9525
9857
  const globs = [];
9526
9858
  let inPackages = false;
@@ -9540,7 +9872,7 @@ function workspaceGlobs(worktree, readText) {
9540
9872
  }
9541
9873
  if (globs.length) return globs;
9542
9874
  }
9543
- const pkg = readJson(readText, join18(worktree, "package.json"));
9875
+ const pkg = readJson(readText, join19(worktree, "package.json"));
9544
9876
  const ws = pkg?.workspaces;
9545
9877
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
9546
9878
  return list.filter((g) => typeof g === "string");
@@ -9551,14 +9883,14 @@ function workspaceDirs(worktree, globs, io) {
9551
9883
  if (glob.startsWith("!")) continue;
9552
9884
  const star = glob.indexOf("*");
9553
9885
  if (star === -1) {
9554
- const dir = join18(worktree, glob);
9555
- if (io.exists(join18(dir, "package.json"))) dirs.add(dir);
9886
+ const dir = join19(worktree, glob);
9887
+ if (io.exists(join19(dir, "package.json"))) dirs.add(dir);
9556
9888
  continue;
9557
9889
  }
9558
- const base3 = join18(worktree, glob.slice(0, star).replace(/\/$/, ""));
9890
+ const base3 = join19(worktree, glob.slice(0, star).replace(/\/$/, ""));
9559
9891
  for (const name of io.listDirs(base3)) {
9560
- const dir = join18(base3, name);
9561
- if (io.exists(join18(dir, "package.json"))) dirs.add(dir);
9892
+ const dir = join19(base3, name);
9893
+ if (io.exists(join19(dir, "package.json"))) dirs.add(dir);
9562
9894
  }
9563
9895
  }
9564
9896
  return [...dirs];
@@ -9579,18 +9911,18 @@ function entryPaths(pkg) {
9579
9911
  return out;
9580
9912
  }
9581
9913
  function workspaceBuildTargets(worktree, io = {}) {
9582
- const exists = io.exists ?? existsSync10;
9914
+ const exists = io.exists ?? existsSync11;
9583
9915
  const readText = io.readText ?? safeReadText;
9584
9916
  const listDirs = io.listDirs ?? safeListDirs;
9585
9917
  const globs = workspaceGlobs(worktree, readText);
9586
9918
  if (!globs.length) return [];
9587
9919
  const pkgs = [];
9588
9920
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
9589
- const pkg = readJson(readText, join18(dir, "package.json"));
9921
+ const pkg = readJson(readText, join19(dir, "package.json"));
9590
9922
  if (!pkg || typeof pkg.name !== "string") continue;
9591
9923
  const scripts = pkg.scripts ?? {};
9592
9924
  const entries = entryPaths(pkg);
9593
- const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join18(dir, e)));
9925
+ const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join19(dir, e)));
9594
9926
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
9595
9927
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
9596
9928
  }
@@ -9618,7 +9950,7 @@ function devDatabaseUrl(worktree, io = {}) {
9618
9950
  const fromEnv = (io.env ?? process.env).DATABASE_URL;
9619
9951
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
9620
9952
  for (const file of [".env", ".env.example"]) {
9621
- const raw = readText(join18(worktree, file));
9953
+ const raw = readText(join19(worktree, file));
9622
9954
  if (raw === null) continue;
9623
9955
  const m = /^\s*(?:export\s+)?DATABASE_URL\s*=\s*(.*)$/m.exec(raw);
9624
9956
  const value = (m?.[1] ?? "").trim().replace(/^['"]|['"]$/g, "");
@@ -9634,11 +9966,11 @@ function isLocalDatabase(url) {
9634
9966
  }
9635
9967
  }
9636
9968
  function prismaPackages(worktree, io) {
9637
- const exists = io.exists ?? existsSync10;
9969
+ const exists = io.exists ?? existsSync11;
9638
9970
  const readText = io.readText ?? safeReadText;
9639
9971
  const listDirs = io.listDirs ?? safeListDirs;
9640
9972
  const dirs = [worktree, ...workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs })];
9641
- return dirs.filter((dir) => exists(join18(dir, "prisma", "schema.prisma")) && listDirs(join18(dir, "prisma", "migrations")).length > 0);
9973
+ return dirs.filter((dir) => exists(join19(dir, "prisma", "schema.prisma")) && listDirs(join19(dir, "prisma", "migrations")).length > 0);
9642
9974
  }
9643
9975
  function jsExecBin(pm, command) {
9644
9976
  switch (pm) {
@@ -9671,7 +10003,7 @@ function planPrepare(worktree, io = {}) {
9671
10003
  return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
9672
10004
  }
9673
10005
  async function ensureWorktreeDeps(worktree, io = {}) {
9674
- const exists = io.exists ?? existsSync10;
10006
+ const exists = io.exists ?? existsSync11;
9675
10007
  const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
9676
10008
  const approve = io.approve ?? (() => true);
9677
10009
  const plan = planPrepare(worktree, io);
@@ -9856,6 +10188,43 @@ function pruneDefers(episodes, live) {
9856
10188
  for (const key of episodes.keys()) if (!live.has(key)) episodes.delete(key);
9857
10189
  }
9858
10190
 
10191
+ // src/spawnAttempts.ts
10192
+ init_src();
10193
+ var PRE_START_ATTEMPT_LIMIT = 3;
10194
+ var PRE_START_MARK = "\u0434\u0440\u043E\u043D \u043D\u0435 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u0430\u043B";
10195
+ var SPAWN_PARK_MARK = "\u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0442\u043B\u043E\u0436\u0435\u043D\u0430 \u0440\u043E\u0435\u043C";
10196
+ function isPreStartFailure(run) {
10197
+ return run.status === "failed" && (run.stop_reason ?? "").includes(PRE_START_MARK);
10198
+ }
10199
+ function isSpawnParked(run) {
10200
+ return (run.stop_reason ?? "").includes(SPAWN_PARK_MARK);
10201
+ }
10202
+ var SPAWN_ATTEMPT_WINDOW_MS = 60 * 6e4;
10203
+ function preStartAttempts(runs, taskSlug, opts = {}) {
10204
+ const windowMs = opts.windowMs ?? SPAWN_ATTEMPT_WINDOW_MS;
10205
+ const now = opts.now ?? Date.now();
10206
+ const own = runs.filter((r) => r.task_slug === taskSlug && r.reviews_run_id == null && r.resolves_run_id == null && r.verifies_run_id == null).sort((a, b) => b.id - a.id);
10207
+ let n = 0;
10208
+ for (const run of own) {
10209
+ if (run.status === "queued" || run.status === "running") continue;
10210
+ if (isSpawnParked(run) && run.status !== "failed") continue;
10211
+ if (!isPreStartFailure(run)) break;
10212
+ const at = run.created_at ? stampMs(run.created_at) : Number.NaN;
10213
+ if (Number.isFinite(at) && now - at >= windowMs) break;
10214
+ n += 1;
10215
+ }
10216
+ return n;
10217
+ }
10218
+ function unresolvedRepoReason(repo, served) {
10219
+ const raw = repo.trim();
10220
+ if (!raw || raw.startsWith("/") || raw.startsWith("~") || /^[a-z]:[\\/]/i.test(raw)) return null;
10221
+ const list = served.length ? served.join(", ") : "\u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E (\u0440\u0435\u0435\u0441\u0442\u0440 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u043F\u0443\u0441\u0442)";
10222
+ return `\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 ${raw} \u043D\u0435 \u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u043B\u0441\u044F \u0432 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0443\u0442\u044C: \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u0435\u0433\u043E \u043A\u043B\u043E\u043D\u0430 \u043D\u0435\u0442. \u041C\u0430\u0448\u0438\u043D\u0430 \u043E\u0431\u0441\u043B\u0443\u0436\u0438\u0432\u0430\u0435\u0442: ${list}. \u041A\u043B\u043E\u043D\u0438\u0440\u0443\u0439 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u0438 \u0434\u043E\u0431\u0430\u0432\u044C \u0435\u0433\u043E \u0432 SWARM_REPO_PATHS (\u0438\u043B\u0438 swarm start --repo <\u043F\u0443\u0442\u044C>), \u043B\u0438\u0431\u043E \u043E\u0442\u0434\u0430\u0439 \u0437\u0430\u0434\u0430\u0447\u0443 \u043C\u0430\u0448\u0438\u043D\u0435, \u0433\u0434\u0435 \u043E\u043D \u0435\u0441\u0442\u044C.`;
10223
+ }
10224
+ function parkReason(attempts, reason) {
10225
+ return `${PRE_START_MARK} ${attempts} \u0440\u0430\u0437\u0430 \u043F\u043E\u0434\u0440\u044F\u0434 \u2014 ${SPAWN_PARK_MARK}, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u043F\u043E \u043A\u0440\u0443\u0433\u0443. \u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ${reason}`;
10226
+ }
10227
+
9859
10228
  // src/overmind.ts
9860
10229
  init_git();
9861
10230
 
@@ -10122,11 +10491,16 @@ import { z } from "zod";
10122
10491
  init_src();
10123
10492
 
10124
10493
  // src/reviewCycle.ts
10494
+ init_src();
10125
10495
  function reviewTransitionPatch(run, prUrl) {
10126
- const isFixIteration = run.review_verdict === "changes";
10496
+ const isFixIteration = run.review_verdict === "changes" || isNegativeVerifyVerdict(toVerifyVerdict(run.verify_verdict));
10127
10497
  if (isFixIteration) {
10128
10498
  const cycle = (run.review_cycle ?? 0) + 1;
10129
- return { isFixIteration: true, cycle, patch: { status: "review", pr_url: prUrl, review_cycle: cycle, review_verdict: null } };
10499
+ return {
10500
+ isFixIteration: true,
10501
+ cycle,
10502
+ patch: { status: "review", pr_url: prUrl, review_cycle: cycle, review_verdict: null, verify_verdict: null }
10503
+ };
10130
10504
  }
10131
10505
  return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
10132
10506
  }
@@ -11108,7 +11482,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11108
11482
  };
11109
11483
  const closedBeforeStart = (run) => opts.daemonStartedAt != null && sqliteMs(run.updated_at) < opts.daemonStartedAt;
11110
11484
  const recentFailed = allRuns.filter(
11111
- (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r)
11485
+ (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r) && !isPreStartFailure(r)
11112
11486
  );
11113
11487
  if (recentFailed.length >= thresholds.failedSpikeCount) {
11114
11488
  candidates.push({
@@ -11167,6 +11541,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11167
11541
  if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
11168
11542
  if (killedByOperator(run)) continue;
11169
11543
  if (closedBeforeStart(run)) continue;
11544
+ if (isPreStartFailure(run)) continue;
11170
11545
  const repeat = isRepeatDown(run);
11171
11546
  candidates.push({
11172
11547
  key: `run_failed:${run.id}`,
@@ -11181,6 +11556,23 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11181
11556
  })
11182
11557
  });
11183
11558
  }
11559
+ for (const run of allRuns) {
11560
+ if (run.status !== "failed" || !isSpawnParked(run)) continue;
11561
+ if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
11562
+ if (closedBeforeStart(run)) continue;
11563
+ candidates.push({
11564
+ key: `spawn_parked:${run.id}`,
11565
+ minDurationMs: 0,
11566
+ // серия попыток УЖЕ прошла свой PRE_START_ATTEMPT_LIMIT — отлёживать нечего
11567
+ build: () => ({
11568
+ runId: run.id,
11569
+ level: "decision",
11570
+ title: runTitle(run, "\u0440\u043E\u0439 \u043D\u0435 \u0441\u043C\u043E\u0433 \u0432\u0437\u044F\u0442\u044C\u0441\u044F \u0437\u0430 \u0437\u0430\u0434\u0430\u0447\u0443"),
11571
+ body: `${run.task_slug}: ${run.stop_reason ?? SPAWN_PARK_MARK}`,
11572
+ ...hrefOf(run.task_url)
11573
+ })
11574
+ });
11575
+ }
11184
11576
  const queuedCount = allRuns.filter((r) => r.status === "queued").length;
11185
11577
  const activeDroneCount2 = allRuns.filter((r) => r.kind !== "hero" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
11186
11578
  if (queuedCount > 0 && activeDroneCount2 === 0) {
@@ -11198,6 +11590,31 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11198
11590
  })
11199
11591
  });
11200
11592
  }
11593
+ const GROUP_DEP_MISSING = {
11594
+ id: "dep_missing",
11595
+ title: (n) => `${n} ${plural2(n, ["\u043F\u0440\u043E\u0433\u043E\u043D \u0436\u0434\u0451\u0442", "\u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0436\u0434\u0443\u0442", "\u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u0436\u0434\u0443\u0442"])} \u0431\u043B\u043E\u043A\u0435\u0440\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u043D\u0435\u0442`
11596
+ };
11597
+ for (const run of allRuns) {
11598
+ if (run.status !== "queued") continue;
11599
+ if (!run.depends_on || run.depends_on.length === 0) continue;
11600
+ const lost = db.depVerdicts(run.depends_on).filter((d) => d.outcome === "unknown");
11601
+ if (!lost.length) continue;
11602
+ candidates.push({
11603
+ key: `dep_missing:${run.task_slug}`,
11604
+ // по СЛАГУ, а не по id зеркала: тот меняется на рестарте (TAS-485)
11605
+ minDurationMs: thresholds.depMissingMs,
11606
+ group: GROUP_DEP_MISSING,
11607
+ build: () => ({
11608
+ runId: run.id,
11609
+ // Пуш: рой сам из этого не выйдет НИКОГДА, а лечение — одно действие человека.
11610
+ // Ровно тот случай, ради которого уровень decision и заведён.
11611
+ level: "decision",
11612
+ title: runTitle(run, "\u0436\u0434\u0451\u0442 \u0431\u043B\u043E\u043A\u0435\u0440\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u043D\u0435\u0442"),
11613
+ body: `${droneOf(run)}: \u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0442\u043E\u0438\u0442 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0441 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C\u044E ${joinCapped(lost.map((d) => d.slug))}, \u043D\u043E \u043F\u043E \u044D\u0442\u043E\u0439 \u0441\u0441\u044B\u043B\u043A\u0435 \u043D\u0435\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u2014 \u043D\u0438 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435, \u043D\u0438 \u0432 \u043F\u0440\u043E\u0434\u0435. \u0421\u0430\u043C \u043E\u043D \u043D\u0435 \u043F\u043E\u0435\u0434\u0435\u0442: \u0441\u043D\u0438\u043C\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C (swarm_run_upsert{runId, dependsOn:[]}) \u0438\u043B\u0438 \u0437\u0430\u0432\u0435\u0434\u0438 \u0431\u043B\u043E\u043A\u0435\u0440 \u0437\u0430\u043D\u043E\u0432\u043E.`,
11614
+ ...hrefOf(run.task_url)
11615
+ })
11616
+ });
11617
+ }
11201
11618
  if (opts.gateReason && queuedCount > 0) {
11202
11619
  const reason = opts.gateReason;
11203
11620
  candidates.push({
@@ -11282,6 +11699,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11282
11699
  for (const q of db.openQuestions()) {
11283
11700
  const run = runsById.get(q.run_id);
11284
11701
  if (!run) continue;
11702
+ if (isTerminalStatus(run.status)) continue;
11285
11703
  candidates.push({
11286
11704
  key: `question_unanswered:${q.id}`,
11287
11705
  minDurationMs: thresholds.questionUnansweredMs,
@@ -11764,22 +12182,22 @@ function makeMergeGateResolver(deps) {
11764
12182
  }
11765
12183
 
11766
12184
  // src/envGate.ts
11767
- import { existsSync as existsSync13 } from "fs";
11768
- import { join as join24 } from "path";
12185
+ import { existsSync as existsSync14 } from "fs";
12186
+ import { join as join25 } from "path";
11769
12187
  import { createConnection } from "net";
11770
12188
  import { execa as execa2 } from "execa";
11771
12189
 
11772
12190
  // src/platform.ts
11773
- import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
11774
- import { delimiter, join as join23 } from "path";
12191
+ import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
12192
+ import { delimiter, join as join24 } from "path";
11775
12193
  function resolveCommandPath(name, env = process.env, platform = process.platform) {
11776
- if (name.includes("/") || name.includes("\\")) return existsSync12(name) ? name : null;
12194
+ if (name.includes("/") || name.includes("\\")) return existsSync13(name) ? name : null;
11777
12195
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
11778
12196
  const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
11779
12197
  for (const dir of dirs) {
11780
12198
  for (const ext of exts) {
11781
- const candidate = join23(dir, name + ext);
11782
- if (existsSync12(candidate)) return candidate;
12199
+ const candidate = join24(dir, name + ext);
12200
+ if (existsSync13(candidate)) return candidate;
11783
12201
  }
11784
12202
  }
11785
12203
  return null;
@@ -11862,8 +12280,8 @@ var COMPOSE_MARKERS = [
11862
12280
  "compose.yaml"
11863
12281
  ];
11864
12282
  function detectRequirements(repo, io = {}) {
11865
- const exists = io.exists ?? existsSync13;
11866
- const marker = COMPOSE_MARKERS.find((f) => exists(join24(repo, f)));
12283
+ const exists = io.exists ?? existsSync14;
12284
+ const marker = COMPOSE_MARKERS.find((f) => exists(join25(repo, f)));
11867
12285
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
11868
12286
  }
11869
12287
  function resolveRequirements(repo, declared, io = {}) {
@@ -11999,8 +12417,8 @@ import { basename as basename3 } from "path";
11999
12417
  import { serve as serve2 } from "@hono/node-server";
12000
12418
 
12001
12419
  // src/diff.ts
12002
- import { statSync as statSync9 } from "fs";
12003
- import { join as join25 } from "path";
12420
+ import { statSync as statSync10 } from "fs";
12421
+ import { join as join26 } from "path";
12004
12422
  import { $ as $4 } from "execa";
12005
12423
  var MAX_FILES = 200;
12006
12424
  var MAX_LINES_PER_FILE = 1500;
@@ -12143,7 +12561,7 @@ async function resolveBase(wt, baseBranch) {
12143
12561
  }
12144
12562
  function mtime(path) {
12145
12563
  try {
12146
- return statSync9(path).mtimeMs;
12564
+ return statSync10(path).mtimeMs;
12147
12565
  } catch {
12148
12566
  return 0;
12149
12567
  }
@@ -12168,7 +12586,7 @@ async function collectRunDiff(worktree, opts = {}) {
12168
12586
  files.push(pf);
12169
12587
  }
12170
12588
  }
12171
- files.sort((a, b) => mtime(join25(worktree, b.path)) - mtime(join25(worktree, a.path)));
12589
+ files.sort((a, b) => mtime(join26(worktree, b.path)) - mtime(join26(worktree, a.path)));
12172
12590
  const truncatedFiles = files.length > MAX_FILES;
12173
12591
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
12174
12592
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -12201,7 +12619,7 @@ async function changedPaths(worktree, opts = {}) {
12201
12619
  // package.json
12202
12620
  var package_default = {
12203
12621
  name: "@taskless-app/swarm",
12204
- version: "0.6.13",
12622
+ version: "0.6.15",
12205
12623
  type: "module",
12206
12624
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
12207
12625
  keywords: [
@@ -12251,7 +12669,7 @@ var package_default = {
12251
12669
  execa: "^10.0.0",
12252
12670
  zod: "3.25.76"
12253
12671
  },
12254
- gitHead: "ea3913bf66c52d730c25ad62b7090621a5006ee7"
12672
+ gitHead: "5bc8e0c067de58fd72b0eb434e80ad29c7a00223"
12255
12673
  };
12256
12674
 
12257
12675
  // src/version.ts
@@ -13884,10 +14302,10 @@ async function checkDaemonVersion(start, opts = {}) {
13884
14302
 
13885
14303
  // src/hostCommands.ts
13886
14304
  init_src();
13887
- import { readFileSync as readFileSync18, renameSync as renameSync8, writeFileSync as writeFileSync13 } from "fs";
13888
- import { join as join26 } from "path";
14305
+ import { readFileSync as readFileSync18, renameSync as renameSync9, writeFileSync as writeFileSync13 } from "fs";
14306
+ import { join as join27 } from "path";
13889
14307
  function hostCommandStatePath(home) {
13890
- return join26(home, "host-command.json");
14308
+ return join27(home, "host-command.json");
13891
14309
  }
13892
14310
  function readState(path) {
13893
14311
  try {
@@ -13899,7 +14317,7 @@ function readState(path) {
13899
14317
  function writeState(path, state) {
13900
14318
  try {
13901
14319
  writeFileSync13(`${path}.tmp`, JSON.stringify(state));
13902
- renameSync8(`${path}.tmp`, path);
14320
+ renameSync9(`${path}.tmp`, path);
13903
14321
  } catch {
13904
14322
  }
13905
14323
  }
@@ -14061,8 +14479,8 @@ async function doRestart(cmd, opts, note) {
14061
14479
 
14062
14480
  // src/selfUpdate.ts
14063
14481
  init_git();
14064
- import { existsSync as existsSync14 } from "fs";
14065
- import { join as join27 } from "path";
14482
+ import { existsSync as existsSync15 } from "fs";
14483
+ import { join as join28 } from "path";
14066
14484
  import { $ as $5 } from "execa";
14067
14485
  var DAEMON_PACKAGE = "@taskless-app/swarm";
14068
14486
  var defaultRun = async (cmd, args, cwd) => {
@@ -14075,7 +14493,7 @@ function tail(text, max = 400) {
14075
14493
  }
14076
14494
  async function runSelfUpdate(io) {
14077
14495
  const run = io.run ?? defaultRun;
14078
- const exists = io.exists ?? existsSync14;
14496
+ const exists = io.exists ?? existsSync15;
14079
14497
  const repo = io.repo;
14080
14498
  await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
14081
14499
  const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
@@ -14104,7 +14522,7 @@ async function runSelfUpdate(io) {
14104
14522
  };
14105
14523
  }
14106
14524
  await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
14107
- if (!exists(join27(repo, "pnpm-lock.yaml"))) {
14525
+ if (!exists(join28(repo, "pnpm-lock.yaml"))) {
14108
14526
  return { ok: false, step: "install", message: "\u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442 pnpm-lock.yaml \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u0435\u0433\u043E \u044D\u0442\u0438\u043C \u043F\u0443\u0442\u0451\u043C \u043D\u0435 \u0431\u0435\u0440\u0443\u0441\u044C" };
14109
14527
  }
14110
14528
  const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
@@ -14162,6 +14580,47 @@ function makeOvermindNarrator() {
14162
14580
  };
14163
14581
  }
14164
14582
 
14583
+ // src/orphanQuestions.ts
14584
+ init_src();
14585
+ var ORPHAN_NOTE_WINDOW_MS = DAY_MS;
14586
+ var ORPHAN_NOTE_PREFIX = "\u0412\u043E\u043F\u0440\u043E\u0441 \u043E\u0441\u0442\u0430\u043B\u0441\u044F \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u0441\u044F \u0440\u0430\u043D\u044C\u0448\u0435";
14587
+ function orphanWithdrawReason(status) {
14588
+ return `\u0441\u043D\u044F\u0442 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438: \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D (${status}), \u043E\u0442\u0432\u0435\u0447\u0430\u0442\u044C \u043D\u0435\u043A\u043E\u043C\u0443`;
14589
+ }
14590
+ function reapOrphanedQuestions(db, now, windowMs = ORPHAN_NOTE_WINDOW_MS) {
14591
+ const result = { withdrawn: 0, noted: 0 };
14592
+ let open;
14593
+ let runsById;
14594
+ try {
14595
+ open = db.openQuestions();
14596
+ if (open.length === 0) return result;
14597
+ runsById = new Map(db.listRuns().map((r) => [r.id, r]));
14598
+ } catch {
14599
+ return result;
14600
+ }
14601
+ for (const q of open) {
14602
+ const run = runsById.get(q.run_id);
14603
+ if (!run) continue;
14604
+ if (!isTerminalStatus(run.status) && run.superseded_by == null) continue;
14605
+ try {
14606
+ if (now - sqliteMs(run.updated_at) <= windowMs) {
14607
+ db.addEvent(run.id, NOTE_KIND, orphanNoteText(run, q), NOTE_ACTOR);
14608
+ result.noted++;
14609
+ }
14610
+ db.withdrawQuestion(q.id, orphanWithdrawReason(run.status));
14611
+ result.withdrawn++;
14612
+ } catch (e) {
14613
+ console.error(`[orphan-questions] \u0432\u043E\u043F\u0440\u043E\u0441 #${q.id} \u043D\u0435 \u0441\u043D\u044F\u0442, \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:`, e);
14614
+ }
14615
+ }
14616
+ return result;
14617
+ }
14618
+ var NOTE_KIND = "note";
14619
+ var NOTE_ACTOR = "orchestrator";
14620
+ function orphanNoteText(run, q) {
14621
+ return `${ORPHAN_NOTE_PREFIX} (${run.status}). \u041E\u0442\u0432\u0435\u0447\u0430\u0442\u044C \u043D\u0435\u043A\u043E\u043C\u0443, \u0437\u0432\u0430\u0442\u044C \u043D\u0438\u043A\u043E\u0433\u043E \u043D\u0435 \u0431\u0443\u0434\u0435\u043C \u2014 \u0442\u0435\u043A\u0441\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u044E: \xAB${q.text}\xBB`;
14622
+ }
14623
+
14165
14624
  // src/probe.ts
14166
14625
  init_executors();
14167
14626
  import { spawn as spawn2 } from "child_process";
@@ -14222,18 +14681,18 @@ async function fetchExternalStorm(url, opts = {}) {
14222
14681
  }
14223
14682
 
14224
14683
  // src/anomalyState.ts
14225
- import { existsSync as existsSync15, readFileSync as readFileSync19, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
14226
- import { join as join28 } from "path";
14684
+ import { existsSync as existsSync16, readFileSync as readFileSync19, renameSync as renameSync10, writeFileSync as writeFileSync14 } from "fs";
14685
+ import { join as join29 } from "path";
14227
14686
  var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
14228
14687
  var MAX_RESOLVED = 200;
14229
14688
  function emptyState() {
14230
14689
  return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
14231
14690
  }
14232
14691
  function anomalyStatePath(home) {
14233
- return join28(home, "anomaly-state.json");
14692
+ return join29(home, "anomaly-state.json");
14234
14693
  }
14235
14694
  function loadAnomalyState(home, now = Date.now(), io = {}) {
14236
- const exists = io.exists ?? existsSync15;
14695
+ const exists = io.exists ?? existsSync16;
14237
14696
  const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
14238
14697
  const path = anomalyStatePath(home);
14239
14698
  if (!exists(path)) return emptyState();
@@ -14265,7 +14724,7 @@ function saveAnomalyState(home, state, io = {}) {
14265
14724
  const path = anomalyStatePath(home);
14266
14725
  const writeFile3 = io.writeFile ?? ((p, data) => {
14267
14726
  writeFileSync14(`${p}.tmp`, data);
14268
- renameSync9(`${p}.tmp`, p);
14727
+ renameSync10(`${p}.tmp`, p);
14269
14728
  });
14270
14729
  writeFile3(path, JSON.stringify(state, null, 2));
14271
14730
  }
@@ -14591,6 +15050,9 @@ function short(files, n = 3) {
14591
15050
  return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
14592
15051
  }
14593
15052
  var SPINE_PREDICTION_WEIGHT = 0.7;
15053
+ function spineOfPrediction(spine, predicted) {
15054
+ return predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
15055
+ }
14594
15056
  function decideConflict({ model, candidate, active }) {
14595
15057
  if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
14596
15058
  if (!hasZoneModel(model)) return { kind: "unknown" };
@@ -14598,23 +15060,16 @@ function decideConflict({ model, candidate, active }) {
14598
15060
  if (!predicted.length) return { kind: "unknown" };
14599
15061
  if (!active.length) return { kind: "clear" };
14600
15062
  const spine = new Set(model.spine);
14601
- const candidateSpine = predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
14602
- if (candidateSpine.length) {
14603
- const blocker = active[0];
14604
- return {
14605
- kind: "wait",
14606
- blockedBy: blocker.run,
14607
- message: `\u0437\u0430\u0434\u0430\u0447\u0430 \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${short(candidateSpine)}) \u2014 \u0442\u0430\u043A\u0438\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; \u0436\u0434\u0443 ${blocker.run.task_slug} (#${blocker.run.id})`
14608
- };
14609
- }
14610
- for (const w of active) {
14611
- if (!w.actual) continue;
14612
- const theirSpine = w.files.filter((f) => spine.has(f));
14613
- if (theirSpine.length && zoneForFiles(model, w.files)?.zone === SPINE_ZONE) {
15063
+ const candidateSpine = new Set(spineOfPrediction(spine, predicted));
15064
+ if (candidateSpine.size) {
15065
+ for (const w of active) {
15066
+ const theirSpine = w.actual ? w.files.filter((f) => spine.has(f)) : spineOfPrediction(spine, predictForRun(model, w.run));
15067
+ const shared = theirSpine.filter((f) => candidateSpine.has(f));
15068
+ if (!shared.length) continue;
14614
15069
  return {
14615
15070
  kind: "wait",
14616
15071
  blockedBy: w.run,
14617
- message: `${w.run.task_slug} (#${w.run.id}) \u043F\u0440\u044F\u043C\u043E \u0441\u0435\u0439\u0447\u0430\u0441 \u043F\u0440\u0430\u0432\u0438\u0442 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${short(theirSpine)}) \u2014 \u043E\u0442\u0432\u0435\u0442\u0432\u043B\u044F\u0442\u044C\u0441\u044F \u043E\u0442 \u043D\u0435\u0433\u043E \u0440\u0430\u043D\u043E, \u0436\u0434\u0443`
15072
+ message: `\u044D\u0442\u0438 \u0434\u0432\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0442\u0440\u043E\u0433\u0430\u044E\u0442 \u043E\u0434\u043D\u0438 \u0444\u0430\u0439\u043B\u044B \u043E\u0431\u0449\u0435\u0433\u043E \u0445\u0440\u0435\u0431\u0442\u0430 (${short(shared)}) \u2014 \u0442\u0430\u043A\u0438\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: \u0436\u0434\u0443 ${w.run.task_slug} (#${w.run.id})` + (w.actual ? "" : " (\u043F\u043E \u043F\u0440\u0435\u0434\u0441\u043A\u0430\u0437\u0430\u043D\u0438\u044E \u2014 \u0434\u0440\u043E\u043D \u0435\u0449\u0451 \u043D\u0435 \u043D\u0430\u0447\u0430\u043B \u043F\u0438\u0441\u0430\u0442\u044C \u043A\u043E\u0434)")
14618
15073
  };
14619
15074
  }
14620
15075
  }
@@ -15212,6 +15667,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
15212
15667
  "3. `/verify` \u2014 \u0422\u041E\u041B\u042C\u041A\u041E \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044F\u043B \u0432\u0438\u0434\u0438\u043C\u044B\u0439 UI/\u0432\u0438\u0437\u0443\u0430\u043B: \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0444\u0438\u0447\u0443 \u043D\u0430 \u0436\u0438\u0432\u043E\u043C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 (\u043A\u0430\u043A \u0435\u0433\u043E \u043F\u043E\u0434\u043D\u044F\u0442\u044C \u2014 \u0432 \u0441\u043A\u0438\u043B\u043B\u0430\u0445 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F) \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435, \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u043E \u043A\u043E\u043C\u043F\u0438\u043B\u0438\u0440\u0443\u0435\u0442\u0441\u044F.",
15213
15668
  `4. \u0421\u043A\u0440\u0438\u043D\u044B mobile+desktop \u2014 \u0422\u041E\u041B\u042C\u041A\u041E \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044F\u043B \u0432\u0438\u0434\u0438\u043C\u044B\u0439 UI/\u0432\u0438\u0437\u0443\u0430\u043B, \u041E\u0414\u041D\u041E\u0419 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: \`swarm shot <url> <out-prefix>\` (headless Chromium \u0441\u0430\u043C \u043E\u0442\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u043E\u0431\u0430 \u0432\u044C\u044E\u043F\u043E\u0440\u0442\u0430, \u0441\u043D\u0438\u043C\u0430\u0435\u0442 \u0438 \u0432\u044B\u0445\u043E\u0434\u0438\u0442 \u2014 \u043D\u0435 \u0434\u0435\u0440\u0436\u0438\u0442 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C). \u041F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C \u2014 \u043E\u0431\u0430 \u0444\u0430\u0439\u043B\u0430 \u0418\u0414\u0423\u0422 \u0412 \u0417\u0410\u0414\u0410\u0427\u0423: \u043F\u0435\u0440\u0435\u0434\u0430\u0439 \u0438\u0445 \u043F\u0443\u0442\u0438 \u0432 \`done(pr_url, summary, screenshots: ["\u2026-desktop.png", "\u2026-mobile.png"])\`, \u0434\u0435\u043C\u043E\u043D \u043F\u0440\u0438\u043B\u043E\u0436\u0438\u0442 \u0438\u0445 \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u044F\u043C\u0438 \u043A \u0442\u0432\u043E\u0435\u043C\u0443 \u043E\u0442\u0447\u0451\u0442\u0443 (TASWM-294). \u0427\u0435\u043B\u043E\u0432\u0435\u043A \u0441\u043C\u043E\u0442\u0440\u0438\u0442 \u0442\u0440\u0435\u043A\u0435\u0440, \u0430 \u043D\u0435 \u0433\u0438\u0442\u0445\u0430\u0431: \u0437\u0430\u0434\u0430\u0447\u0430 \u0431\u0435\u0437 \u0441\u043A\u0440\u0438\u043D\u043E\u0432 = \u043D\u0435\u0434\u043E\u043A\u0430\u0437\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430. \u0412 PR \u2014 \u0414\u041E\u041F\u041E\u041B\u041D\u0418\u0422\u0415\u041B\u042C\u041D\u041E, \u043F\u043E \u0436\u0435\u043B\u0430\u043D\u0438\u044E; \u0432\u043C\u0435\u0441\u0442\u043E \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u043D\u0435\u0442. \u23F1 \u0425\u0430\u0440\u0434-\u043B\u0438\u043C\u0438\u0442 \u043D\u0430 \u0432\u0435\u0441\u044C \u0448\u0430\u0433 \u0441\u043A\u0440\u0438\u043D\u043E\u0432 \u2014 5 \u043C\u0438\u043D\u0443\u0442. \u041D\u0415 \u0433\u043E\u043D\u044F\u0439 Playwright/\u0441\u0438\u0441\u0442\u0435\u043C\u043D\u044B\u0439 Chrome \u0432\u0440\u0443\u0447\u043D\u0443\u044E \u043F\u043E \u0448\u0430\u0433\u0430\u043C (\u043E\u0442\u043A\u0440\u044B\u043B\u2192\u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u043B\u2192\u043A\u043B\u0438\u043A\u043D\u0443\u043B\u2192\u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u043B\u2026) \u2014 \u0442\u0430\u043A \u0434\u0440\u043E\u043D \u0414\u0412\u0410\u0416\u0414\u042B \u0437\u0430\u043B\u0438\u043F\u0430\u043B \u043D\u0430 \u044D\u0442\u043E\u043C (TASWM-30, >30 \u043C\u0438\u043D stall \u0431\u0435\u0437 \u0435\u0434\u0438\u043D\u043E\u0433\u043E \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u0434\u0430\u043B\u044C\u0448\u0435) \u0432\u043C\u0435\u0441\u0442\u043E \u0442\u043E\u0433\u043E \u0447\u0442\u043E\u0431\u044B \u0441\u0434\u0430\u0442\u044C \u0433\u043E\u0442\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443. \u041D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u0441\u044F \u0432 \u043B\u0438\u043C\u0438\u0442 \u0438\u043B\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u043D\u0435 \u0438\u0434\u0451\u0442 \u2014 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u043D\u044F\u043B, \u0438\u043B\u0438 \u043F\u0440\u044F\u043C\u043E \u043D\u0430\u043F\u0438\u0448\u0438 \u0432 PR/\u0437\u0430\u0434\u0430\u0447\u0435 \xAB\u0441\u043A\u0440\u0438\u043D\u044B \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\xBB \u0438 \u0421\u0420\u0410\u0417\u0423 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url) \u2014 \u043D\u0435 \u043F\u0435\u0440\u0435\u0438\u0433\u0440\u044B\u0432\u0430\u0439 \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u043F\u043E \u043A\u0440\u0443\u0433\u0443.`,
15214
15669
  "4a. \u0421\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u0417\u0410 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u0431\u0435\u0437 \u0433\u043E\u0442\u043E\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0434\u0430\u0451\u0442 \u044D\u043A\u0440\u0430\u043D \u043B\u043E\u0433\u0438\u043D\u0430, \u0430 \u043D\u0435 \u0446\u0435\u043B\u0435\u0432\u043E\u0439 \u044D\u043A\u0440\u0430\u043D \u2014 \u0441\u043D\u0438\u043C\u0430\u0442\u044C \u0435\u0451 \u043D\u0443\u0436\u043D\u043E \u0441 `--storage-state <\u0444\u0430\u0439\u043B>` (\u043A\u0430\u043A \u0437\u0430\u0432\u0435\u0441\u0442\u0438 \u0442\u0430\u043A\u043E\u0439 \u0444\u0430\u0439\u043B \u0432 \u044D\u0442\u043E\u043C \u043F\u0440\u043E\u0435\u043A\u0442\u0435 \u2014 \u0432 \u0435\u0433\u043E \u0441\u043A\u0438\u043B\u043B\u0430\u0445). \u041F\u043E\u0440\u044F\u0434\u043E\u043A \u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u043E\u0432 \u0432\u0430\u0436\u0435\u043D: `--storage-state` \u0411\u0415\u0417 \u043F\u0440\u0435\u0434\u0448\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0435\u0433\u043E `<out-prefix>` \u043C\u043E\u043B\u0447\u0430 \u0441\u044A\u0435\u0434\u0430\u0435\u0442 \u0435\u0433\u043E \u043C\u0435\u0441\u0442\u043E, \u0438 \u0444\u043B\u0430\u0433 \u043D\u0438\u043A\u043E\u0433\u0434\u0430 \u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u0441\u044F (\u0441\u043C. shotCli.ts parseShotArgs) \u2014 \u0435\u0441\u043B\u0438 \u043D\u0430 \u0441\u043D\u0438\u043C\u043A\u0435 \u044D\u043A\u0440\u0430\u043D \u043B\u043E\u0433\u0438\u043D\u0430 \u0432\u043C\u0435\u0441\u0442\u043E \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F, \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u044D\u0442\u043E.",
15670
+ "4b. \u0421\u041D\u0418\u041C\u0410\u0419 \u041F\u0420\u041E\u0414\u0423\u041A\u0422, \u0410 \u041D\u0415 \u041F\u0420\u0415\u0412\u042C\u042E-\u0421\u0422\u0420\u0410\u041D\u0418\u0426\u0423 (TASWM-404). \u0414\u0435\u043C\u043E-\u0440\u043E\u0443\u0442 \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442\u0430 (`/dev/*`, \u043F\u0435\u0441\u043E\u0447\u043D\u0438\u0446\u0430, \u0441\u0442\u043E\u0440\u0438 \u0432 \u043C\u043E\u0431\u0438\u043B\u044C\u043D\u043E\u043C \u0444\u0440\u0435\u0439\u043C\u0435) \u2014 \u041D\u0415 \u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E \u043C\u043E\u0431\u0438\u043B\u044C\u043D\u043E\u0439 \u0432\u0451\u0440\u0441\u0442\u043A\u0438: \u0443 \u0442\u0430\u043A\u043E\u0439 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B \u0441\u0432\u043E\u044F \u043E\u0431\u043E\u043B\u043E\u0447\u043A\u0430, \u0435\u0451 \u043C\u0435\u043D\u044E \u043E\u0431\u044B\u0447\u043D\u043E \u043D\u0435 \u0441\u0432\u043E\u0440\u0430\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u043D\u0430 \u0443\u0437\u043A\u043E\u043C \u044D\u043A\u0440\u0430\u043D\u0435, \u0438 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u043C\u0443 \u043E\u0441\u0442\u0430\u0451\u0442\u0441\u044F \u043F\u043E\u043B\u043E\u0441\u0430 \u0432 \u043F\u0430\u0440\u0443 \u0441\u043E\u0442\u0435\u043D \u043F\u0438\u043A\u0441\u0435\u043B\u0435\u0439 \u0432\u043C\u0435\u0441\u0442\u043E \u043D\u0430\u0441\u0442\u043E\u044F\u0449\u0435\u0439 \u0448\u0438\u0440\u0438\u043D\u044B. \u041F\u043E \u0442\u0430\u043A\u043E\u043C\u0443 \u0441\u043D\u0438\u043C\u043A\u0443 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E \u043D\u0438 \u0448\u0438\u0440\u0438\u043D\u044B \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432, \u043D\u0438 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u044F \u0448\u0430\u043F\u043A\u0438, \u043D\u0438 \u043D\u0430\u043F\u043E\u043B\u0437\u0430\u043D\u0438\u044F \u043B\u0438\u043F\u043A\u0438\u0445 \u043F\u043E\u0434\u043F\u0438\u0441\u0435\u0439 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043F\u0440\u043E\u0445\u043E\u0434\u0438\u0442 \u0412\u0421\u0415\u0413\u0414\u0410 \u0438 \u043D\u0435 \u043B\u043E\u0432\u0438\u0442 \u043D\u0438\u0447\u0435\u0433\u043E (\u0442\u0430\u043A \u0441\u0434\u0430\u043B\u0438 \u0441\u043B\u043E\u043C\u0430\u043D\u043D\u0443\u044E \u043C\u043E\u0431\u0438\u043B\u0443 \u0432 TASWM-403, \u0441\u043A\u0440\u0438\u043D \u043F\u0440\u0438 \u044D\u0442\u043E\u043C \u0431\u044B\u043B \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D). \u0421\u043D\u0438\u043C\u0430\u0439 \u0420\u0415\u0410\u041B\u042C\u041D\u042B\u0419 \u0440\u043E\u0443\u0442 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043F\u043E\u0434 \u0436\u0438\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439; \u043A\u0430\u043A \u044D\u0442\u043E \u0441\u0434\u0435\u043B\u0430\u0442\u044C \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 \u0432 \u044D\u0442\u043E\u043C \u043F\u0440\u043E\u0435\u043A\u0442\u0435 \u2014 \u0432 \u0435\u0433\u043E \u0441\u043A\u0438\u043B\u043B\u0430\u0445. \u041F\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043A\u043E\u043D\u043D\u043E \u0440\u043E\u0432\u043D\u043E \u0442\u043E\u0433\u0434\u0430, \u043A\u043E\u0433\u0434\u0430 \u043F\u0440\u0430\u0432\u0438\u0448\u044C \u0441\u0430\u043C \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442 \u0432\u043D\u0435 \u043E\u0431\u043E\u043B\u043E\u0447\u043A\u0438 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F.",
15215
15671
  "\u0427\u0438\u0441\u0442\u043E \u0431\u044D\u043A\u0435\u043D\u0434/\u043B\u043E\u0433\u0438\u043A\u0443/API/\u0434\u0435\u043C\u043E\u043D/CI/\u0442\u0438\u043F\u044B \u2014 \u0448\u0430\u0433\u0438 3\u20134 \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438 (\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u041D\u0415 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0439 \u0438 \u0441\u043A\u0440\u0438\u043D\u044B \u043D\u0435 \u0441\u043D\u0438\u043C\u0430\u0439): \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u0442\u0435\u0441\u0442\u044B (\u0448\u0430\u0433 1) + e2e \u0432 CI \u044D\u0442\u043E \u0443\u0436\u0435 \u043F\u043E\u043A\u0440\u044B\u0432\u0430\u044E\u0442; \u044D\u043A\u043E\u043D\u043E\u043C\u0438\u043C \u0432\u0440\u0435\u043C\u044F \u0438 \u0442\u043E\u043A\u0435\u043D\u044B.",
15216
15672
  "\u0422\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u043B\u0435 \u044D\u0442\u043E\u0433\u043E \u0437\u043E\u0432\u0438 done(pr_url), \u0438 \u0432 \u0441\u0434\u0430\u0447\u0435 \u043F\u043E\u043A\u0430\u0436\u0438 \u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E (\u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u0432\u044B\u0432\u043E\u0434/\u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0435\u043C\u043E\u0435 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435), \u0430 \u043D\u0435 \xAB\u0434\u043E\u043B\u0436\u043D\u043E \u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C\xBB.",
15217
15673
  "\u0421\u043A\u0438\u043B\u043B\u044B \u043F\u0440\u0438\u0445\u043E\u0434\u044F\u0442 \u043A \u0442\u0435\u0431\u0435 \u0414\u0412\u0423\u041C\u042F \u043D\u0430\u0431\u043E\u0440\u0430\u043C\u0438, \u0438 \u043E\u0431\u0430 \u0443\u0436\u0435 \u043F\u043E\u0434\u0445\u0432\u0430\u0447\u0435\u043D\u044B (\u0441\u043F\u0430\u0432\u043D \u0438\u0437\u043E\u043B\u0438\u0440\u043E\u0432\u0430\u043D \u043E\u0442 \u044E\u0437\u0435\u0440\u0441\u043A\u043E\u0433\u043E ~/.claude, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u041B\u0418\u0427\u041D\u042B\u0415 \u0441\u043A\u0438\u043B\u043B\u044B \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 \u0442\u0435\u0431\u0435 \u043D\u0435 \u0432\u0438\u0434\u043D\u044B). \u2460 \u0420\u043E\u0439\u043D\u044B\u0435 `swarm-drone:*` \u0435\u0434\u0443\u0442 \u0441 \u0420\u041E\u0415\u041C \u0432 \u043B\u044E\u0431\u043E\u0439 \u043F\u0440\u043E\u0435\u043A\u0442 \u2014 \u0434\u0438\u0441\u0446\u0438\u043F\u043B\u0438\u043D\u0430, \u0432\u0435\u0440\u043D\u0430\u044F \u0432\u0435\u0437\u0434\u0435: swarm-save-often (\u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0438\u0440\u0430\u0435\u0442 \u0441 \u0434\u0440\u043E\u043D\u043E\u043C), swarm-merge-main (\u0431\u0430\u0437\u043E\u0432\u0443\u044E \u0432\u0435\u0442\u043A\u0443 \u0432\u043B\u0438\u0432\u0430\u0442\u044C, \u0430 \u043D\u0435 \u0440\u0435\u0431\u0435\u0439\u0437\u0438\u0442\u044C \u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u0443\u044E), swarm-await-process (\u0436\u0434\u0430\u0442\u044C \u0443\u0441\u043B\u043E\u0432\u0438\u0435, \u043D\u0435 sleep), swarm-verify-green (\u0447\u0442\u043E \u0441\u0447\u0438\u0442\u0430\u0442\u044C \u0437\u0435\u043B\u0451\u043D\u044B\u043C), systematic-debugging (\u043F\u0435\u0440\u0432\u043E\u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u0434\u043E \u0444\u0438\u043A\u0441\u0430). \u2461 \u0421\u043A\u0438\u043B\u043B\u044B \u0420\u0415\u041F\u041E\u0417\u0418\u0422\u041E\u0420\u0418\u042F (.claude/skills, \u0441\u043E\u0441\u0442\u0430\u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043E\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u0430) \u2014 \u0437\u043D\u0430\u043D\u0438\u044F \u043E\u0431 \u044D\u0442\u043E\u043C \u043F\u0440\u043E\u0435\u043A\u0442\u0435: \u043A\u0430\u043A \u043F\u043E\u0434\u043D\u044F\u0442\u044C \u0441\u0442\u0435\u043A, \u043A\u0430\u043A \u0433\u043E\u043D\u044F\u0442\u044C \u0442\u0435\u0441\u0442\u044B \u0438 \u0447\u0442\u043E \u0437\u0434\u0435\u0441\u044C \u0437\u0435\u043B\u0451\u043D\u043E\u0435, UI-\u043F\u0440\u0430\u0432\u0438\u043B\u0430. \u041F\u043E\u043B\u044C\u0437\u0443\u0439\u0441\u044F \u043E\u0431\u043E\u0438\u043C\u0438, \u043D\u0435 \u043F\u0435\u0440\u0435\u0438\u0437\u043E\u0431\u0440\u0435\u0442\u0430\u0439; \u043D\u0435\u0442 \u043D\u0443\u0436\u043D\u043E\u0433\u043E \u0437\u043D\u0430\u043D\u0438\u044F \u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0435 \u2014 \u044D\u0442\u043E \u043F\u0440\u043E\u0431\u0435\u043B \u0435\u0433\u043E \u0441\u043A\u0438\u043B\u043B\u043E\u0432, \u0441\u043A\u0430\u0436\u0438 note(), \u0430 \u043D\u0435 \u0432\u044B\u0434\u0443\u043C\u044B\u0432\u0430\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u044B."
@@ -15225,7 +15681,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
15225
15681
  }
15226
15682
  return parts.join("\n");
15227
15683
  }
15228
- var HERO_CHARTER_PATH = join29(dirname4(THIS_FILE), "..", "docs", "HERO.md");
15684
+ var HERO_CHARTER_PATH = join30(dirname4(THIS_FILE), "..", "docs", "HERO.md");
15229
15685
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
15230
15686
  const name = run.name || `#${run.id}`;
15231
15687
  const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
@@ -15525,7 +15981,7 @@ async function wakeRun(deps, runId) {
15525
15981
  if (!run) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0437\u0435\u0440\u043A\u0430\u043B\u0435 \u0440\u043E\u044F` };
15526
15982
  if (run.pid != null && deps.isAlive(run.pid)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 (pid ${run.pid}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
15527
15983
  if (isTerminalStatus(run.status)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435\u043D (${run.status}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
15528
- const worktreeExists = deps.worktreeExists ?? existsSync16;
15984
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
15529
15985
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
15530
15986
  const wt = run.worktree_path;
15531
15987
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -15610,9 +16066,9 @@ function repoHasCi(deps, run) {
15610
16066
  function repoHasCiWorkflows(worktreePath, repo) {
15611
16067
  for (const base3 of [worktreePath, repo]) {
15612
16068
  if (!base3) continue;
15613
- const dir = join29(base3, ".github", "workflows");
16069
+ const dir = join30(base3, ".github", "workflows");
15614
16070
  try {
15615
- if (readdirSync12(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
16071
+ if (readdirSync13(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
15616
16072
  } catch {
15617
16073
  }
15618
16074
  }
@@ -15761,7 +16217,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
15761
16217
  return true;
15762
16218
  }
15763
16219
  function worktreeAndTranscriptAlive(deps, worktree) {
15764
- const exists = deps.worktreeExists ?? existsSync16;
16220
+ const exists = deps.worktreeExists ?? existsSync17;
15765
16221
  const transcript = deps.hasTranscript ?? executorHasSession;
15766
16222
  return exists(worktree) && transcript(worktree);
15767
16223
  }
@@ -16064,6 +16520,15 @@ async function trySpawnQueuedRun(deps, run) {
16064
16520
  // причина одна на машину — в лог уходит одной строкой, а не по строке на прогон
16065
16521
  });
16066
16522
  }
16523
+ const spentAttempts = preStartAttempts(deps.db.listRuns(), run.task_slug);
16524
+ if (spentAttempts >= PRE_START_ATTEMPT_LIMIT) {
16525
+ const why = `${PRE_START_MARK} ${spentAttempts} \u0440\u0430\u0437\u0430 \u043F\u043E\u0434\u0440\u044F\u0434 \u2014 ${SPAWN_PARK_MARK}, \u0447\u0435\u0442\u0432\u0451\u0440\u0442\u043E\u0433\u043E \u0437\u0430\u0445\u043E\u0434\u0430 \u043D\u0435 \u0434\u0435\u043B\u0430\u044E (TASWM-422)`;
16526
+ deps.db.updateRun(run.id, { status: "stopped", stop_reason: why });
16527
+ deps.db.addEvent(run.id, "step", why);
16528
+ const d = noteDefer(deps.spawnDefer ??= /* @__PURE__ */ new Map(), `pre-start-exhausted:${run.task_slug}`, why, GATE_LOG_HEARTBEAT_TICKS);
16529
+ if (d.log) console.error(`[overmind] ${run.task_slug}: ${why}${d.changed ? "" : ` \u2014 \u0435\u0449\u0451 \u0432\u0438\u0441\u0438\u0442, \u0442\u0438\u043A ${d.ticks}`}`);
16530
+ return "closed";
16531
+ }
16067
16532
  const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
16068
16533
  if (mergedUrl) {
16069
16534
  await closeDuplicateTaskRun(deps, run, mergedUrl);
@@ -16074,7 +16539,11 @@ async function trySpawnQueuedRun(deps, run) {
16074
16539
  }
16075
16540
  const depVerdicts = deps.db.depVerdicts(run.depends_on);
16076
16541
  const unmet = depVerdicts.filter((d) => !d.met);
16077
- if (unmet.length) return deferSpawn(deps, run, `\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438: ${unmet.map((d) => d.slug).join(", ")}`);
16542
+ if (unmet.length) {
16543
+ const lost = unmet.filter((d) => d.outcome === "unknown");
16544
+ const why = lost.length ? `\u0436\u0434\u0451\u0442 \u0431\u043B\u043E\u043A\u0435\u0440\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u041D\u0415\u0422: ${lost.map((d) => d.slug).join(", ")} \u2014 \u043F\u043E \u044D\u0442\u043E\u0439 \u0441\u0441\u044B\u043B\u043A\u0435 \u043D\u0435\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430; \u0441\u0430\u043C \u043D\u0435 \u043F\u043E\u0435\u0434\u0435\u0442, \u0441\u043D\u0438\u043C\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C (swarm_run_upsert{dependsOn:[]}) \u0438\u043B\u0438 \u0437\u0430\u0432\u0435\u0434\u0438 \u0431\u043B\u043E\u043A\u0435\u0440 \u0437\u0430\u043D\u043E\u0432\u043E (TASWM-399)` : `\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438: ${unmet.map((d) => d.slug).join(", ")}`;
16545
+ return deferSpawn(deps, run, why);
16546
+ }
16078
16547
  if (!await envGateAllows(deps, run)) return "skipped";
16079
16548
  if (!await projectQuotaGateAllows(deps, run)) return "skipped";
16080
16549
  const prodOverride = await prodOverrideReason(deps, run, ["queued"]);
@@ -16084,6 +16553,8 @@ async function trySpawnQueuedRun(deps, run) {
16084
16553
  }
16085
16554
  noteExhaustedDeps(deps, run, depVerdicts);
16086
16555
  await fillSuggestedArea(deps, run);
16556
+ const repoGap = unresolvedRepoReason(run.repo, deps.reposServed ? deps.reposServed() : []);
16557
+ if (repoGap) return await failBeforeStart(deps, run, repoGap);
16087
16558
  try {
16088
16559
  await deps.spawn(run);
16089
16560
  if (deps.spawnDefer) clearDefer(deps.spawnDefer, `run:${run.id}`);
@@ -16100,10 +16571,32 @@ async function trySpawnQueuedRun(deps, run) {
16100
16571
  return "skipped";
16101
16572
  }
16102
16573
  if (e instanceof AreaBusyError) return deferSpawn(deps, run, e.message);
16103
- deps.db.updateRun(run.id, { status: "failed" });
16104
- deps.db.addEvent(run.id, "failed", String(e));
16574
+ return await failBeforeStart(deps, run, String(e));
16575
+ }
16576
+ }
16577
+ async function failBeforeStart(deps, run, reason) {
16578
+ deps.db.updateRun(run.id, { status: "failed", stop_reason: `${PRE_START_MARK}: ${reason}` });
16579
+ deps.db.addEvent(run.id, "failed", `${PRE_START_MARK} \u2014 ${reason}`);
16580
+ const attempts = preStartAttempts(deps.db.listRuns(), run.task_slug);
16581
+ if (attempts < PRE_START_ATTEMPT_LIMIT) {
16582
+ narrate(deps, `${droneLabel(run)} \u2014 ${PRE_START_MARK} (\u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempts} \u0438\u0437 ${PRE_START_ATTEMPT_LIMIT}): ${reason}`);
16105
16583
  return "failed";
16106
16584
  }
16585
+ const parked = parkReason(attempts, reason);
16586
+ deps.db.updateRun(run.id, { stop_reason: parked });
16587
+ deps.db.addEvent(run.id, "step", parked);
16588
+ narrate(deps, `${run.task_slug}: ${parked}`);
16589
+ if (deps.parkTask) {
16590
+ const ok = await deps.parkTask(run, parked).catch(() => false);
16591
+ if (!ok) {
16592
+ deps.db.addEvent(
16593
+ run.id,
16594
+ "step",
16595
+ `${SPAWN_PARK_MARK}: \u0443\u0432\u0435\u0441\u0442\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0440\u0435\u0437\u043E\u043B\u0432\u0438\u0442\u0441\u044F) \u2014 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u0435\u0451 \u044D\u0442\u0430 \u043C\u0430\u0448\u0438\u043D\u0430 \u0432\u0441\u0451 \u0440\u0430\u0432\u043D\u043E \u043D\u0435 \u0431\u0443\u0434\u0435\u0442, \u043D\u043E \u0443\u0431\u0440\u0430\u0442\u044C \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u043F\u0440\u0438\u0434\u0451\u0442\u0441\u044F \u0440\u0443\u043A\u0430\u043C\u0438`
16596
+ );
16597
+ }
16598
+ }
16599
+ return "failed";
16107
16600
  }
16108
16601
  async function batchSpawnQueue(deps, droneCap) {
16109
16602
  for (const run of orderQueue(deps.db.listRuns("queued"))) {
@@ -16125,7 +16618,7 @@ async function resumeWatchdogParked(deps, droneCap) {
16125
16618
  }
16126
16619
  if (!candidate) return;
16127
16620
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
16128
- const worktreeExists = deps.worktreeExists ?? existsSync16;
16621
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
16129
16622
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
16130
16623
  const wt = candidate.worktree_path;
16131
16624
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -16294,7 +16787,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
16294
16787
  if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
16295
16788
  if (!deps.respawn) return;
16296
16789
  const wt = resumeCandidate.worktree_path;
16297
- const worktreeExists = deps.worktreeExists ?? existsSync16;
16790
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
16298
16791
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
16299
16792
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
16300
16793
  const mode = canContinue ? "continue" : "fresh";
@@ -16563,7 +17056,7 @@ function buildEscalationCandidates(db) {
16563
17056
  }
16564
17057
  async function tickOnce(deps) {
16565
17058
  const now = deps.now ? deps.now() : Date.now();
16566
- const worktreeExists = deps.worktreeExists ?? existsSync16;
17059
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
16567
17060
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
16568
17061
  deps.sampleHost?.();
16569
17062
  if (deps.rehydrate) {
@@ -16804,7 +17297,7 @@ async function tickOnce(deps) {
16804
17297
  } else if (!st.conflicting && deps.ci && deps.respawn && !stormActive) {
16805
17298
  await handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap);
16806
17299
  }
16807
- if (run.review_verdict === "changes" && !stormActive && !st.conflicting && !deps.db.activeResolverFor(run.id) && await wakeAuthorOnChanges(deps, run, now, droneCap)) {
17300
+ if ((run.review_verdict === "changes" || isNegativeVerifyVerdict(toVerifyVerdict(run.verify_verdict))) && !stormActive && !st.conflicting && !deps.db.activeResolverFor(run.id) && await wakeAuthorOnChanges(deps, run, now, droneCap)) {
16808
17301
  continue;
16809
17302
  }
16810
17303
  const reviewerGatesOk = !st.conflicting && !st.isDraft && !!deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt) && deps.db.reviewerCountFor(run.id) === 0 && // ГЕЙТ ПЕРВЫМ: живой/успешный ревьюер уже есть — ни капа, ни спавна не трогаем
@@ -16894,6 +17387,13 @@ async function tickOnce(deps) {
16894
17387
  liveChildPpids = null;
16895
17388
  logTransientSpawnError("ps (livePpids)", e);
16896
17389
  }
17390
+ if (deps.sweepCost) {
17391
+ try {
17392
+ deps.sweepCost();
17393
+ } catch (e) {
17394
+ console.error("[overmind] \u0441\u0432\u0438\u043F \u0443\u0447\u0451\u0442\u0430 \u0441\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u0438 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
17395
+ }
17396
+ }
16897
17397
  for (const run of [...deps.db.listRuns("running"), ...deps.db.listRuns("blocked")]) {
16898
17398
  const alive = run.pid != null && deps.isAlive(run.pid);
16899
17399
  if (!alive && deps.db.takeRun && !await deps.db.takeRun(run.id)) continue;
@@ -17119,6 +17619,16 @@ async function tickOnce(deps) {
17119
17619
  if (deps.respawn && deps.resourcesOk && !stormActive) {
17120
17620
  await resurrectStormDead(deps, worktreeExists, hasTranscript, { gateOpen: deps.resourcesOk(), cap: droneCap });
17121
17621
  }
17622
+ try {
17623
+ const reaped = reapOrphanedQuestions(deps.db, now);
17624
+ if (reaped.withdrawn > 0) {
17625
+ console.log(
17626
+ `[overmind] \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B: \u0441\u043D\u044F\u0442\u043E ${reaped.withdrawn}, \u0437\u0430\u043C\u0435\u0442\u043A\u043E\u0439 \u0432 \u043B\u0435\u043D\u0442\u0443 \u0443\u0448\u043B\u043E ${reaped.noted}`
17627
+ );
17628
+ }
17629
+ } catch (e) {
17630
+ console.error("[overmind] \u0443\u0431\u043E\u0440\u043A\u0430 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0445 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e);
17631
+ }
17122
17632
  if (deps.anomalies && !deps.control?.paused) {
17123
17633
  const az = deps.anomalies;
17124
17634
  let ready = [];
@@ -17227,8 +17737,14 @@ async function replayDecision(deps, qid, chosen, by = "human") {
17227
17737
  repo: old.repo,
17228
17738
  area: old.area,
17229
17739
  decision: { question: q.text, options: q.options, chosen, prior: q.answer, priorState: old.status },
17230
- workspace_id: old.workspace_id
17740
+ workspace_id: old.workspace_id,
17231
17741
  // переигровка той же задачи — тот же воркспейс (TASWM-288)
17742
+ // TASWM-451: та же ЗАДАЧА — те же модели. Переигровка это продолжение той же работы, а не
17743
+ // новая: без переноса любой вопрос дрона оператору молча возвращал бы задачу на дефолт
17744
+ // роли, и выбор постановщика («эту делаем сильной моделью») терялся бы ровно там, где
17745
+ // работа оказалась сложной. Тот же довод, что у area и workspace_id строкой выше.
17746
+ model: old.model,
17747
+ review_model: old.review_model
17232
17748
  });
17233
17749
  deps.db.updateRun(old.id, { status: "superseded" });
17234
17750
  deps.db.supersede(old.id, fresh.id, by);
@@ -17256,10 +17772,15 @@ async function resolveConflict(deps, reviewRun) {
17256
17772
  area: reviewRun.area,
17257
17773
  // держим area-lock, пока чиним — не начинаем конфликтующее в зоне
17258
17774
  resolves_run_id: reviewRun.id,
17259
- workspace_id: reviewRun.workspace_id
17775
+ workspace_id: reviewRun.workspace_id,
17260
17776
  // та же задача, тот же воркспейс (TASWM-288)
17777
+ // TASWM-451: резолвер доделывает ТУ ЖЕ работу — разводит конфликт в её же ветке, и
17778
+ // модель ему нужна та же, что выбрали задаче. Модель проверки несём дальше, чтобы её не
17779
+ // потеряли проверяющие, которых поднимут уже по прогону резолвера.
17780
+ model: reviewRun.model,
17781
+ review_model: reviewRun.review_model
17261
17782
  });
17262
- const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
17783
+ const wt = join30(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
17263
17784
  try {
17264
17785
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
17265
17786
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -17304,6 +17825,11 @@ async function spawnReviewer(deps, reviewRun, files) {
17304
17825
  task_slug: reviewRun.task_slug,
17305
17826
  repo,
17306
17827
  kind: "reviewer",
17828
+ // TASWM-451: ЧЕМ проверять — решение постановщика, приехавшее с задачей на прогон
17829
+ // исполнителя. Пин, а не «спросим настройки на спавне»: ревьюер заводится локально, и
17830
+ // задача ему недоступна. Пусто в задаче → null → дефолт роли (модель проверки).
17831
+ model: reviewRun.review_model,
17832
+ review_model: reviewRun.review_model,
17307
17833
  reviews_run_id: reviewRun.id,
17308
17834
  review_cycle: reviewRun.review_cycle,
17309
17835
  // штамп цикла (авто-ревью v2, TASWM-162): reviewerCountFor текущего цикла
@@ -17311,7 +17837,7 @@ async function spawnReviewer(deps, reviewRun, files) {
17311
17837
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
17312
17838
  workspace_id: reviewRun.workspace_id
17313
17839
  });
17314
- const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
17840
+ const wt = join30(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
17315
17841
  try {
17316
17842
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
17317
17843
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -17376,12 +17902,17 @@ async function spawnVerifier(deps, verifiedRun, files) {
17376
17902
  task_slug: verifiedRun.task_slug,
17377
17903
  repo,
17378
17904
  kind: "verifier",
17905
+ // TASWM-451: та же модель проверки, что у ревьюера, и из того же источника — задачи.
17906
+ // Две проверки одного PR должны стоить одинаково, иначе одна из них незаметно уедет
17907
+ // на модель исполнителя (ровно это и стоило $45.94 на TAS-524).
17908
+ model: verifiedRun.review_model,
17909
+ review_model: verifiedRun.review_model,
17379
17910
  verifies_run_id: verifiedRun.id,
17380
17911
  review_cycle: verifiedRun.review_cycle,
17381
17912
  // штамп цикла: фикс-итерацию проверяет свежий верификатор
17382
17913
  workspace_id: verifiedRun.workspace_id
17383
17914
  });
17384
- const wt = join29(deps.worktreesDir, basename4(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
17915
+ const wt = join30(deps.worktreesDir, basename4(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
17385
17916
  try {
17386
17917
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
17387
17918
  await deps.git.setWorktreeIdentity(wt, verifier.name);
@@ -17419,7 +17950,7 @@ async function respawnRun(deps, run, mode, nudge) {
17419
17950
  ]);
17420
17951
  const base3 = effectiveBaseBranch(run.base_branch, projectBase);
17421
17952
  const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
17422
- if (!existsSync16(wt)) {
17953
+ if (!existsSync17(wt)) {
17423
17954
  const branch = runToSpawn.branch || runToSpawn.task_slug;
17424
17955
  const source = await addWorktree(repo, wt, branch, {
17425
17956
  ...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
@@ -17446,7 +17977,7 @@ async function respawnRun(deps, run, mode, nudge) {
17446
17977
  deps.db.updateRun(run.id, { status: "running", pid });
17447
17978
  }
17448
17979
  async function syncCrashedOnStartup(deps) {
17449
- const worktreeExists = deps.worktreeExists ?? existsSync16;
17980
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
17450
17981
  let orphaned = 0;
17451
17982
  let merged = 0;
17452
17983
  let requeued = 0;
@@ -17535,10 +18066,12 @@ async function makeStore(repo, worktreesDir, repoFlag) {
17535
18066
  roots: scanRootsList,
17536
18067
  maxDepth: SWARM_SCAN_DEPTH,
17537
18068
  cachePath: REPO_MAP_PATH,
18069
+ // TASWM-418: кэш обязан протухать — иначе новый клон на машине не находится никогда.
18070
+ scanTtlMs: SWARM_REPO_SCAN_TTL_MS,
17538
18071
  state: repoMap,
17539
18072
  log: (m) => console.log(m)
17540
18073
  });
17541
- const repoRegistry = await rebuildRegistry();
18074
+ let repoRegistry = await rebuildRegistry();
17542
18075
  if (repoRegistry.length === 0) {
17543
18076
  console.warn(
17544
18077
  `[overmind] \u041D\u0415 \u041D\u0410\u0428\u0401\u041B \u041D\u0418 \u041E\u0414\u041D\u041E\u0413\u041E \u0420\u0415\u041F\u041E\u0417\u0418\u0422\u041E\u0420\u0418\u042F \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u2014 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0431\u0440\u0430\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0443\u0442: \u0438\u0445 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043F\u0440\u0438\u0445\u043E\u0434\u0438\u0442 \u0438\u043C\u0435\u043D\u0435\u043C owner/repo, \u0430 \u0441\u043E\u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u0441 \u0447\u0435\u043C. \u0418\u0441\u043A\u0430\u043B \u0432: ${scanRootsList.slice(0, 6).join(", ")}. ` + // TASWM-336: почему пусто — три РАЗНЫЕ причины, и лечатся они по-разному. Одна
@@ -17568,8 +18101,13 @@ async function makeStore(repo, worktreesDir, repoFlag) {
17568
18101
  // Периодическая пересборка (шаг 0a4 тика): применяем новый реестр к ЖИВОМУ клиенту —
17569
18102
  // и список для claim/heartbeat, и обратную карту owner/repo → путь.
17570
18103
  refreshRepoMap: async () => {
17571
- client.setRepoRegistry(await rebuildRegistry());
17572
- }
18104
+ repoRegistry = await rebuildRegistry();
18105
+ client.setRepoRegistry(repoRegistry);
18106
+ },
18107
+ // TASWM-422: что машина обслуживает ПРЯМО СЕЙЧАС — читается из ЖИВОГО реестра (он
18108
+ // пересобирается тиком выше), чтобы причина «клона нет на этой машине» называла
18109
+ // актуальный список, а не снимок на старте демона.
18110
+ reposServed: () => repoRegistry.map((e) => e.repoFullName)
17573
18111
  };
17574
18112
  }
17575
18113
  var crashGuardsInstalled = false;
@@ -17589,22 +18127,22 @@ async function migrateWorktreesAtStartup() {
17589
18127
  legacyDir: WORKTREES_LEGACY_DIR,
17590
18128
  targetDir: WORKTREES_DIR,
17591
18129
  envOverride: process.env.SWARM_WORKTREES || "",
17592
- exists: (d) => existsSync16(d),
18130
+ exists: (d) => existsSync17(d),
17593
18131
  isNonEmptyDir: (d) => {
17594
18132
  try {
17595
- return readdirSync12(d).length > 0;
18133
+ return readdirSync13(d).length > 0;
17596
18134
  } catch {
17597
18135
  return false;
17598
18136
  }
17599
18137
  },
17600
18138
  liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
17601
- rename: (from, to) => renameSync10(from, to),
18139
+ rename: (from, to) => renameSync11(from, to),
17602
18140
  repairMoved: (t) => repairMovedWorktrees(t),
17603
18141
  log: (m) => console.log(m)
17604
18142
  });
17605
18143
  } catch (e) {
17606
18144
  console.error("[migrate] \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u044F worktrees \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C \u2014 \u0444\u043E\u043B\u0431\u044D\u043A:", e);
17607
- return existsSync16(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
18145
+ return existsSync17(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
17608
18146
  }
17609
18147
  }
17610
18148
  async function reportWorkspaceAccess(tl, db) {
@@ -17624,6 +18162,14 @@ async function runDaemon(opts) {
17624
18162
  const daemonStart = captureDaemonStart();
17625
18163
  const latestRelease = fetchLatestVersion();
17626
18164
  const worktreesDir = await migrateWorktreesAtStartup();
18165
+ {
18166
+ const swept = sweepLegacyRunLogs(RUN_LOGS_DIR);
18167
+ if (swept.moved || swept.failed) {
18168
+ console.log(
18169
+ `[overmind] run-logs: ${swept.moved} \u043A\u0430\u0442\u0430\u043B\u043E\u0433(\u043E\u0432) \u0441\u0442\u0430\u0440\u043E\u0439 \u0440\u0430\u0441\u043A\u043B\u0430\u0434\u043A\u0438 (\u0430\u0434\u0440\u0435\u0441\u0430\u0446\u0438\u044F \u043D\u043E\u043C\u0435\u0440\u043E\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u0430) \u0443\u0431\u0440\u0430\u043D\u043E \u0432 ${swept.legacyDir}` + (swept.failed ? `, \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0435\u0440\u0435\u043D\u0435\u0441\u0442\u0438: ${swept.failed}` : "")
18170
+ );
18171
+ }
18172
+ }
17627
18173
  if (!opts.repo) {
17628
18174
  const remembered = effectiveRepoPaths("", loadRememberedRepos(SWARM_HOME))[0];
17629
18175
  if (remembered) opts.repo = remembered;
@@ -17634,7 +18180,8 @@ async function runDaemon(opts) {
17634
18180
  host,
17635
18181
  repoMap: repoMapState,
17636
18182
  refreshRepoMap: rebuildRepoMap,
17637
- prodMcp
18183
+ prodMcp,
18184
+ reposServed
17638
18185
  } = await makeStore(opts.repo, worktreesDir, opts.repoFlag);
17639
18186
  await preflightAuth();
17640
18187
  const outdated = outdatedLine(PACKAGE_VERSION, await latestRelease);
@@ -17680,6 +18227,8 @@ async function runDaemon(opts) {
17680
18227
  overmindEntry: THIS_FILE,
17681
18228
  dbPath: DB_PATH,
17682
18229
  runLogsDir: RUN_LOGS_DIR,
18230
+ // TASWM-419: чем именуется каталог лога прогона — стабильные ключи из стора, не номер.
18231
+ runLogKeys: (run) => db.runLogKeys(run.id),
17683
18232
  // Кем исполнять (TASWM-349) — настройка SWARM_EXECUTOR, один резолв на весь демон.
17684
18233
  executor: EXECUTOR,
17685
18234
  droneIsolation: DRONE_ISOLATION,
@@ -17746,7 +18295,18 @@ async function runDaemon(opts) {
17746
18295
  control.state ?? null,
17747
18296
  lastOwnCapacity,
17748
18297
  hostResourcesForProd(),
17749
- { active: stormActive, reason: stormStartedAt ? `\u043D\u043E\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441 ${stormStartedAt} UTC` : null }
18298
+ { active: stormActive, reason: stormStartedAt ? `\u043D\u043E\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441 ${stormStartedAt} UTC` : null },
18299
+ worktreesDir,
18300
+ // TASWM-393: ВХОД в Taskless — та же природа, что пауза и шторм: состояние
18301
+ // машины, о котором знает только она. Пульс идёт и без входа, поэтому «на
18302
+ // связи» без этого поля читалось как «возьмёт работу» (12.08 — шесть прогонов
18303
+ // в очереди у машины, которая снаружи выглядела здоровой). Адрес шлём
18304
+ // ФАКТИЧЕСКИЙ (web.port), а не зашитый: порт настраивается.
18305
+ // signedInToTaskless(), а НЕ !authStateLine(): отказ — это «токен есть, но его
18306
+ // отвергают», а 12.08 входа не делали вовсе, и по отказу машина числилась бы
18307
+ // вошедшей. Адрес — WEB_PORT (он же SWARM_WEB_PORT из окружения, на нём и
18308
+ // поднимается морда): зашитый 7420 отправил бы человека туда, где ничего нет.
18309
+ { signedIn: signedInToTaskless(), webUrl: `http://127.0.0.1:${WEB_PORT}` }
17750
18310
  );
17751
18311
  if (reply.maxDrones !== void 0) operatorMaxDrones = reply.maxDrones;
17752
18312
  if (reply.commands.length) onHostCommands(reply.commands);
@@ -17755,7 +18315,13 @@ async function runDaemon(opts) {
17755
18315
  {
17756
18316
  runLogsDir: RUN_LOGS_DIR,
17757
18317
  daemonLogPath: DAEMON_LOG_PATH,
17758
- localRunId: (prodRunId) => client.localRunIdForProd(prodRunId),
18318
+ // TASWM-419: каталог лога адресуется стабильными ключами прогона, а не его
18319
+ // локальным номером — номера переиспользуются между жизнями зеркала, и
18320
+ // оператору приезжал хвост чужого прогона.
18321
+ runLogKeys: (prodRunId) => {
18322
+ const local = client.localRunIdForProd(prodRunId);
18323
+ return local == null ? [] : db.runLogKeys(local);
18324
+ },
17759
18325
  deliver: (requestId, payload) => client.deliverLog(requestId, payload)
17760
18326
  },
17761
18327
  reply.logRequests
@@ -17813,6 +18379,7 @@ async function runDaemon(opts) {
17813
18379
  questionUnansweredMs: SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
17814
18380
  outboxStuckMs: SWARM_ANOMALY_OUTBOX_STUCK_MS,
17815
18381
  resourceGateStallMs: SWARM_ANOMALY_RESOURCE_GATE_STALL_MS,
18382
+ depMissingMs: SWARM_ANOMALY_DEP_MISSING_MS,
17816
18383
  runDownWindowMs: SWARM_ANOMALY_RUN_DOWN_WINDOW_MS
17817
18384
  },
17818
18385
  autoReviewEnabled: SWARM_AUTO_REVIEW,
@@ -18072,6 +18639,30 @@ async function runDaemon(opts) {
18072
18639
  taskClosed: (slug, ws) => tl.forWorkspace(ws).getTask(slug).then((t) => t ? isClosingTaskCategory(t.statusCategory) : null)
18073
18640
  } : {},
18074
18641
  finishTask: (slug, ws) => tlFor(ws)?.finishTask(slug) ?? Promise.resolve(),
18642
+ ...reposServed ? { reposServed } : {},
18643
+ // TASWM-422: ОТЛОЖИТЬ задачу, по которой рой трижды не сумел поднять дрона. Механика —
18644
+ // ровно та же, что у return_task дрона (TASWM-412): `claimableTaskWhere` (api) требует
18645
+ // категорию unstarted, и start_task её ломает — задача перестаёт быть claimable, то есть
18646
+ // авто-интейк больше не берёт её раз в минуту. Успех проверяем ФАКТОМ (перечитываем
18647
+ // задачу), а не кодом возврата: клиент глушит отказ сервера внутрь (safeCall), и по
18648
+ // возврату «отложил» было бы неотличимо от «сказал, но не доехало» — то самое молчание,
18649
+ // из-за которого 17.08 круг и крутился.
18650
+ parkTask: async (run, reason) => {
18651
+ const client = tlFor(run.workspace_id);
18652
+ if (!client) return false;
18653
+ await client.addComment(
18654
+ run.task_slug,
18655
+ `${reason}
18656
+
18657
+ \u0417\u0430\u0434\u0430\u0447\u0430 \u0441\u043D\u044F\u0442\u0430 \u0441 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0440\u043E\u044F \u2014 \u043F\u043E\u043A\u0430 \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u043D\u0435 \u0443\u0441\u0442\u0440\u0430\u043D\u0435\u043D\u0430, \u0431\u0440\u0430\u0442\u044C \u0435\u0451 \u0441\u043D\u043E\u0432\u0430 \u0441\u043C\u044B\u0441\u043B\u0430 \u043D\u0435\u0442. \u0420\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C \u0438 \u0432\u0435\u0440\u043D\u0438 \u0432 \u0431\u044D\u043A\u043B\u043E\u0433.`,
18658
+ { swarmKind: "report", swarmDroneName: run.name }
18659
+ ).catch(() => {
18660
+ });
18661
+ await client.startTask(run.task_slug).catch(() => {
18662
+ });
18663
+ const after = await client.getTask(run.task_slug).catch(() => null);
18664
+ return after != null && after.statusCategory !== "unstarted";
18665
+ },
18075
18666
  // TASWM-272: отчёт/вердикт-компенсация при усыновлении мёртвого дрона — реальный
18076
18667
  // TasklessClient.addComment + gh/git хелперы. Без tl (нет токена) ключи не задаём —
18077
18668
  // postAdoptionReport/postScrapedVerdictReport становятся no-op, как раньше.
@@ -18093,9 +18684,12 @@ async function runDaemon(opts) {
18093
18684
  // TASWM-364: swarmModels — ТОТ ЖЕ снимок, что уехал в launcher: цена токенов считается
18094
18685
  // по модели, на которой прогон реально работал.
18095
18686
  accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR, swarmModels: () => lastSwarmModels }),
18687
+ // TASWM-413: тот же аккаунтант, но поводом служит завершённый ЗАПУСК, а не смерть
18688
+ // прогона в running — иначе сданный через done() прогон не получает цену никогда.
18689
+ sweepCost: makeCostSweeper({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR, swarmModels: () => lastSwarmModels, isAlive: pidAlive }),
18096
18690
  // Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
18097
18691
  // (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
18098
- exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
18692
+ exitOutcome: makeExitClassifier(RUN_LOGS_DIR, (runId) => db.runLogKeys(runId), EXECUTOR),
18099
18693
  // Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
18100
18694
  livePpids,
18101
18695
  // Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
@@ -18242,7 +18836,7 @@ async function runDaemon(opts) {
18242
18836
  mode,
18243
18837
  nudge
18244
18838
  ),
18245
- worktreeExists: (p) => existsSync16(p),
18839
+ worktreeExists: (p) => existsSync17(p),
18246
18840
  hasTranscript: executorHasSession,
18247
18841
  // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
18248
18842
  // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary