@taskless-app/swarm 0.6.14 → 0.6.16

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
@@ -58,6 +58,13 @@ var init_types = __esm({
58
58
  // вопрос дрона оператору (прод: SwarmEvent kind=question, TASWM-74)
59
59
  "answer",
60
60
  // ответ оператора на вопрос дрона (прод: SwarmEvent kind=answer)
61
+ // Черновик ответа, собранный машиной (TASWM-430): контекст задачи/репозитория + один
62
+ // вызов модели, реплика в ленте прогона рядом с вопросом. НЕ "answer" с флагом — и это
63
+ // главное в этом kind: вопрос закрывают (и дрона будят) все, кто сравнивает kind со
64
+ // строкой "answer"/"answered" — прод (state/taskRuns/timeline/fleetMetrics), веб-чат и
65
+ // синхронизация демона (droneBridge тянет вниз только ответ). Отдельный kind не знает
66
+ // никто из них, поэтому черновик физически не может разбудить дрона: он ждёт человека.
67
+ "answer_draft",
61
68
  "ci_wake",
62
69
  // review-PR покраснел (gh pr checks) — рой разбудил дрона со сжатым summary фейла (TASWM-102)
63
70
  "ci_checked",
@@ -317,21 +324,28 @@ function computeReviewPhase(input) {
317
324
  return { kind: "reviewer_dead", label: REVIEW_PHASE_TEXT.reviewer_dead, ready: true };
318
325
  }
319
326
  const p = input.ciProgress;
327
+ if (p && p.total > 0 && p.pass >= p.total) {
328
+ return { kind: "awaiting_reviewer", label: REVIEW_PHASE_TEXT.awaiting_reviewer, ready: false };
329
+ }
320
330
  const label = p && p.total > 0 ? `${REVIEW_PHASE_TEXT.checks} \u2014 CI ${p.pass}/${p.total}` : REVIEW_PHASE_TEXT.checks;
321
331
  return { kind: "checks", label, ready: false };
322
332
  }
323
- var REVIEW_PHASE_TEXT;
333
+ var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
324
334
  var init_reviewPhase = __esm({
325
335
  "../swarm-core/src/reviewPhase.ts"() {
326
336
  "use strict";
327
337
  REVIEW_PHASE_TEXT = {
328
338
  checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
339
+ awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
329
340
  ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
330
341
  in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
331
342
  changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
332
343
  ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
333
344
  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
345
  };
346
+ PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
347
+ (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
348
+ );
335
349
  }
336
350
  });
337
351
 
@@ -511,7 +525,7 @@ var init_status = __esm({
511
525
  // ../swarm-core/src/deps.ts
512
526
  function judgeDep(runs) {
513
527
  const real = runs.filter((r) => !r.service);
514
- if (real.length === 0) return { ...PENDING };
528
+ if (real.length === 0) return { ...UNKNOWN };
515
529
  if (real.some((r) => !isTerminalStatus(r.status))) return { ...PENDING };
516
530
  if (real.some((r) => r.status === "done")) {
517
531
  return { outcome: "done", met: true, status: "done", name: null, stopReason: null };
@@ -531,12 +545,13 @@ function depClearedText(slug, v) {
531
545
  const why = v.stopReason ? ` \u2014 ${v.stopReason}` : "";
532
546
  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.`;
533
547
  }
534
- var PENDING, DEP_END_LABEL;
548
+ var PENDING, UNKNOWN, DEP_END_LABEL;
535
549
  var init_deps = __esm({
536
550
  "../swarm-core/src/deps.ts"() {
537
551
  "use strict";
538
552
  init_status();
539
553
  PENDING = { outcome: "pending", met: false, status: null, name: null, stopReason: null };
554
+ UNKNOWN = { outcome: "unknown", met: false, status: null, name: null, stopReason: null };
540
555
  DEP_END_LABEL = {
541
556
  stopped: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C",
542
557
  failed: "\u0443\u043F\u0430\u043B",
@@ -555,6 +570,9 @@ function stampFromEpochSec(sec) {
555
570
  function sqliteMs(ts) {
556
571
  return Date.parse(ts.replace(" ", "T") + "Z");
557
572
  }
573
+ function stampMs(at) {
574
+ return at.includes("T") ? Date.parse(at) : sqliteMs(at);
575
+ }
558
576
  function toStamp(v) {
559
577
  if (typeof v !== "string" || !v) return null;
560
578
  const d = new Date(v);
@@ -903,9 +921,17 @@ var init_baseBranch = __esm({
903
921
  });
904
922
 
905
923
  // ../swarm-core/src/hosts.ts
924
+ function hostTickStallMs(intervalMs) {
925
+ const iv = intervalMs != null && Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : 0;
926
+ return Math.max(iv * HOST_TICK_STALL_TICKS, HOST_TICK_STALL_FLOOR_MS);
927
+ }
928
+ var HOST_TICK_STALL_TICKS, HOST_TICK_STALL_FLOOR_MS, HOST_TICK_STALLED_ACTION;
906
929
  var init_hosts = __esm({
907
930
  "../swarm-core/src/hosts.ts"() {
908
931
  "use strict";
932
+ HOST_TICK_STALL_TICKS = 10;
933
+ HOST_TICK_STALL_FLOOR_MS = 12e4;
934
+ HOST_TICK_STALLED_ACTION = '\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438 \u0434\u0435\u043C\u043E\u043D \u043C\u0430\u0448\u0438\u043D\u044B \u2014 swarm_host_command{kind:"restart"}';
909
935
  }
910
936
  });
911
937
 
@@ -936,10 +962,27 @@ var init_hostResources = __esm({
936
962
  });
937
963
 
938
964
  // ../swarm-core/src/hostPool.ts
965
+ var HOST_REJECTION_TEXT, HOST_REPO_BLOCK_TEXT;
939
966
  var init_hostPool = __esm({
940
967
  "../swarm-core/src/hostPool.ts"() {
941
968
  "use strict";
942
969
  init_hosts();
970
+ HOST_REJECTION_TEXT = {
971
+ offline: "\u043D\u0435 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438",
972
+ tick_stalled: `\u0446\u0438\u043A\u043B \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B\u0430 \u0432\u0441\u0442\u0430\u043B \u2014 \u043F\u0443\u043B\u044C\u0441 \u0438\u0434\u0451\u0442, \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442; ${HOST_TICK_STALLED_ACTION}`,
973
+ not_signed_in: "\u043D\u0435 \u0432\u043E\u0448\u043B\u0430 \u0432 Taskless \u2014 \u043D\u0430 \u0435\u0451 \u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0435 \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D \u0432\u0445\u043E\u0434",
974
+ paused: "\u043D\u0430 \u043F\u0430\u0443\u0437\u0435",
975
+ draining: "\u0432\u044B\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u043D\u0430 \u043E\u0431\u0441\u043B\u0443\u0436\u0438\u0432\u0430\u043D\u0438\u0435",
976
+ repos_off: "\u0432\u0441\u0435 \u0435\u0451 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u044B",
977
+ full: "\u0437\u0430\u043D\u044F\u0442\u0430 \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E"
978
+ };
979
+ HOST_REPO_BLOCK_TEXT = {
980
+ offline: HOST_REJECTION_TEXT.offline,
981
+ paused: HOST_REJECTION_TEXT.paused,
982
+ draining: HOST_REJECTION_TEXT.draining,
983
+ no_clone: "\u043A\u043B\u043E\u043D\u0430 \u043D\u0435\u0442",
984
+ repo_off: "\u0440\u0430\u0431\u043E\u0442\u0430 \u043F\u043E \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044E \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u0430"
985
+ };
943
986
  }
944
987
  });
945
988
 
@@ -1263,31 +1306,6 @@ function buildZoneModel(entries, opts = {}) {
1263
1306
  const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
1264
1307
  return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
1265
1308
  }
1266
- function zoneForFiles(model, files) {
1267
- if (!hasZoneModel(model) || !files.length) return null;
1268
- const spineSet = new Set(model.spine);
1269
- const touchedSpine = files.filter((f) => spineSet.has(f));
1270
- const votes = /* @__PURE__ */ new Map();
1271
- for (const f of new Set(files)) {
1272
- const z2 = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
1273
- if (z2) bump(votes, z2);
1274
- }
1275
- const top = topKey(votes);
1276
- if (!top) {
1277
- return {
1278
- zone: SPINE_ZONE,
1279
- confidence: 1,
1280
- 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(", ")})`,
1281
- spine: true
1282
- };
1283
- }
1284
- return {
1285
- zone: top.key,
1286
- confidence: top.share,
1287
- 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}`,
1288
- spine: touchedSpine.length > 0
1289
- };
1290
- }
1291
1309
  function suggestZonesFromText(model, text, limit = 3) {
1292
1310
  if (!hasZoneModel(model)) return [];
1293
1311
  const terms = tokenize(text);
@@ -2105,6 +2123,7 @@ __export(config_exports, {
2105
2123
  REPO_MAP_PATH: () => REPO_MAP_PATH,
2106
2124
  REVIEW_CUTOFF_PATH: () => REVIEW_CUTOFF_PATH,
2107
2125
  RUN_LOGS_DIR: () => RUN_LOGS_DIR,
2126
+ SWARM_ANOMALY_DEP_MISSING_MS: () => SWARM_ANOMALY_DEP_MISSING_MS,
2108
2127
  SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
2109
2128
  SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS: () => SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
2110
2129
  SWARM_ANOMALY_MAX_REPEATS: () => SWARM_ANOMALY_MAX_REPEATS,
@@ -2147,6 +2166,7 @@ __export(config_exports, {
2147
2166
  SWARM_REPO: () => SWARM_REPO,
2148
2167
  SWARM_REPO_MAP_MS: () => SWARM_REPO_MAP_MS,
2149
2168
  SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
2169
+ SWARM_REPO_SCAN_TTL_MS: () => SWARM_REPO_SCAN_TTL_MS,
2150
2170
  SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
2151
2171
  SWARM_SCAN_DEPTH: () => SWARM_SCAN_DEPTH,
2152
2172
  SWARM_SCAN_ROOTS_RAW: () => SWARM_SCAN_ROOTS_RAW,
@@ -2168,6 +2188,7 @@ __export(config_exports, {
2168
2188
  WORKTREES_LEGACY_DIR: () => WORKTREES_LEGACY_DIR,
2169
2189
  WORKTREES_NOINDEX_DIR: () => WORKTREES_NOINDEX_DIR,
2170
2190
  droneWorktreePath: () => droneWorktreePath,
2191
+ droneWorktreeRoot: () => droneWorktreeRoot,
2171
2192
  mcpEndpoint: () => mcpEndpoint,
2172
2193
  migrateWorktreesToNoindex: () => migrateWorktreesToNoindex,
2173
2194
  normalizeRepo: () => normalizeRepo,
@@ -2186,7 +2207,10 @@ function normalizeRepo(repo) {
2186
2207
  }
2187
2208
  }
2188
2209
  function droneWorktreePath(worktreesDir, repo, slug) {
2189
- return join5(worktreesDir, basename(normalizeRepo(repo)), slug);
2210
+ return join5(droneWorktreeRoot(worktreesDir, repo), slug);
2211
+ }
2212
+ function droneWorktreeRoot(worktreesDir, repo) {
2213
+ return join5(worktreesDir, basename(normalizeRepo(repo)));
2190
2214
  }
2191
2215
  function resolveEffectiveWorktreesDir(deps) {
2192
2216
  if (deps.envOverride) return deps.envOverride;
@@ -2248,7 +2272,7 @@ function reviewCutoffAt() {
2248
2272
  }
2249
2273
  return at;
2250
2274
  }
2251
- 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;
2275
+ 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;
2252
2276
  var init_config = __esm({
2253
2277
  "src/config.ts"() {
2254
2278
  "use strict";
@@ -2310,6 +2334,7 @@ var init_config = __esm({
2310
2334
  SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
2311
2335
  REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join5(SWARM_HOME, "repo-map.json");
2312
2336
  SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
2337
+ SWARM_REPO_SCAN_TTL_MS = envNum("SWARM_REPO_SCAN_TTL_MS", 24 * 60 * 6e4);
2313
2338
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
2314
2339
  SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
2315
2340
  TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
@@ -2336,6 +2361,7 @@ var init_config = __esm({
2336
2361
  SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
2337
2362
  SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
2338
2363
  SWARM_ANOMALY_RESOURCE_GATE_STALL_MS = envNum("SWARM_ANOMALY_RESOURCE_GATE_STALL_MS", 5 * 6e4);
2364
+ SWARM_ANOMALY_DEP_MISSING_MS = envNum("SWARM_ANOMALY_DEP_MISSING_MS", 2 * 6e4);
2339
2365
  SWARM_WATCHDOG_STALL_MS = envNum("SWARM_WATCHDOG_STALL_MS", 15 * 6e4);
2340
2366
  }
2341
2367
  });
@@ -2734,11 +2760,11 @@ var init_storm = __esm({
2734
2760
  });
2735
2761
 
2736
2762
  // src/heartbeat.ts
2737
- import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
2763
+ import { readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
2738
2764
  function writeHeartbeat(path, now) {
2739
2765
  const tmp = `${path}.tmp`;
2740
2766
  writeFileSync5(tmp, String(now));
2741
- renameSync3(tmp, path);
2767
+ renameSync4(tmp, path);
2742
2768
  }
2743
2769
  function readHeartbeat(path) {
2744
2770
  try {
@@ -2761,7 +2787,7 @@ var init_heartbeat = __esm({
2761
2787
 
2762
2788
  // src/agentToken.ts
2763
2789
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
2764
- import { join as join8 } from "path";
2790
+ import { join as join9 } from "path";
2765
2791
  function loadAgentToken() {
2766
2792
  try {
2767
2793
  const s = JSON.parse(readFileSync7(STORE_PATH, "utf8"));
@@ -2817,7 +2843,7 @@ var init_agentToken = __esm({
2817
2843
  "src/agentToken.ts"() {
2818
2844
  "use strict";
2819
2845
  init_config();
2820
- STORE_PATH = join8(SWARM_HOME, "agent.json");
2846
+ STORE_PATH = join9(SWARM_HOME, "agent.json");
2821
2847
  base = (api) => api.replace(/\/$/, "");
2822
2848
  TOKEN_PROBE_TIMEOUT_MS = 8e3;
2823
2849
  }
@@ -3177,9 +3203,9 @@ var init_procCleanup = __esm({
3177
3203
  });
3178
3204
 
3179
3205
  // src/git.ts
3180
- import { existsSync as existsSync9, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
3206
+ import { existsSync as existsSync10, readdirSync as readdirSync11, statSync as statSync8 } from "fs";
3181
3207
  import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
3182
- import { isAbsolute as isAbsolute2, join as join16, sep as sep2 } from "path";
3208
+ import { isAbsolute as isAbsolute2, join as join17, sep as sep2 } from "path";
3183
3209
  import { $ as $2 } from "execa";
3184
3210
  function isUnrecoverableRefError(e) {
3185
3211
  if (e instanceof MissingRefError) return true;
@@ -3191,7 +3217,7 @@ async function reclaimStaleWorktree(repo, worktree) {
3191
3217
  }
3192
3218
  function isLinkedWorktree(dir) {
3193
3219
  try {
3194
- return statSync7(join16(dir, ".git")).isFile();
3220
+ return statSync8(join17(dir, ".git")).isFile();
3195
3221
  } catch {
3196
3222
  return false;
3197
3223
  }
@@ -3203,7 +3229,7 @@ async function detachWorktree(repo, worktree) {
3203
3229
  }
3204
3230
  }
3205
3231
  async function ensureWorktreeDirGone(worktree) {
3206
- if (!existsSync9(worktree)) return false;
3232
+ if (!existsSync10(worktree)) return false;
3207
3233
  if (!isLinkedWorktree(worktree)) {
3208
3234
  console.error(
3209
3235
  `[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.`
@@ -3218,7 +3244,7 @@ async function ensureWorktreeDirGone(worktree) {
3218
3244
  );
3219
3245
  return true;
3220
3246
  }
3221
- if (existsSync9(worktree)) {
3247
+ if (existsSync10(worktree)) {
3222
3248
  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.`);
3223
3249
  }
3224
3250
  return true;
@@ -3266,7 +3292,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
3266
3292
  }
3267
3293
  const busyAt = await worktreeForBranch(repo, branch);
3268
3294
  if (busyAt) {
3269
- const exists = existsSync9(busyAt);
3295
+ const exists = existsSync10(busyAt);
3270
3296
  const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
3271
3297
  if (live) throw new WorktreeBusyError(branch, busyAt);
3272
3298
  if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
@@ -3301,8 +3327,8 @@ async function ensureGitExcluded(worktree, pattern) {
3301
3327
  try {
3302
3328
  const commonDir = await gitPath(worktree, "--git-common-dir");
3303
3329
  if (!commonDir) return false;
3304
- const infoDir = join16(commonDir, "info");
3305
- const excludeFile = join16(infoDir, "exclude");
3330
+ const infoDir = join17(commonDir, "info");
3331
+ const excludeFile = join17(infoDir, "exclude");
3306
3332
  let current = "";
3307
3333
  try {
3308
3334
  current = await readFile(excludeFile, "utf8");
@@ -3322,7 +3348,7 @@ async function gitPath(worktree, flag) {
3322
3348
  const out = await $2({ reject: false })`git -C ${worktree} rev-parse ${flag}`;
3323
3349
  const raw = out.stdout.trim();
3324
3350
  if (out.exitCode !== 0 || !raw) return null;
3325
- return isAbsolute2(raw) ? raw : join16(worktree, raw);
3351
+ return isAbsolute2(raw) ? raw : join17(worktree, raw);
3326
3352
  }
3327
3353
  async function ensureWorktreeHygiene(worktree) {
3328
3354
  for (const pattern of SWARM_EXCLUDE_PATTERNS) await ensureGitExcluded(worktree, pattern);
@@ -3332,12 +3358,12 @@ async function installGuardHooks(worktree) {
3332
3358
  try {
3333
3359
  const gitDir = await gitPath(worktree, "--git-dir");
3334
3360
  if (!gitDir) return false;
3335
- const hooksDir = join16(gitDir, "swarm-hooks");
3361
+ const hooksDir = join17(gitDir, "swarm-hooks");
3336
3362
  const prev = await previousHooksPath(worktree);
3337
3363
  await mkdir(hooksDir, { recursive: true });
3338
3364
  for (const hook of GUARD_HOOKS) {
3339
- await writeFile(join16(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
3340
- await chmod(join16(hooksDir, hook), 493);
3365
+ await writeFile(join17(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
3366
+ await chmod(join17(hooksDir, hook), 493);
3341
3367
  }
3342
3368
  await $2`git -C ${worktree} config extensions.worktreeConfig true`;
3343
3369
  await $2`git -C ${worktree} config --worktree core.hooksPath ${posix(hooksDir)}`;
@@ -3356,7 +3382,7 @@ async function previousHooksPath(worktree) {
3356
3382
  if (out.exitCode === 0 && value) return value;
3357
3383
  }
3358
3384
  const commonDir = await gitPath(worktree, "--git-common-dir");
3359
- return commonDir ? join16(commonDir, "hooks") : null;
3385
+ return commonDir ? join17(commonDir, "hooks") : null;
3360
3386
  }
3361
3387
  async function removeWorktree(repo, path) {
3362
3388
  const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
@@ -3375,7 +3401,7 @@ async function worktreeDirty(path) {
3375
3401
  async function repairMovedWorktrees(targetDir) {
3376
3402
  const subdirs = (dir) => {
3377
3403
  try {
3378
- return readdirSync10(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join16(dir, d.name));
3404
+ return readdirSync11(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join17(dir, d.name));
3379
3405
  } catch {
3380
3406
  return [];
3381
3407
  }
@@ -3502,7 +3528,7 @@ __export(droneChannel_exports, {
3502
3528
  verifyLocalCliToken: () => verifyLocalCliToken
3503
3529
  });
3504
3530
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
3505
- import { chmodSync, mkdirSync as mkdirSync7, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
3531
+ import { chmodSync, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
3506
3532
  import { dirname as dirname3 } from "path";
3507
3533
  import { serve } from "@hono/node-server";
3508
3534
  function loadChannelSecret(path) {
@@ -3512,7 +3538,7 @@ function loadChannelSecret(path) {
3512
3538
  } catch {
3513
3539
  }
3514
3540
  const secret = randomBytes(32).toString("hex");
3515
- mkdirSync7(dirname3(path), { recursive: true });
3541
+ mkdirSync8(dirname3(path), { recursive: true });
3516
3542
  writeFileSync10(path, secret, { mode: 384 });
3517
3543
  try {
3518
3544
  chmodSync(path, 384);
@@ -3857,7 +3883,7 @@ var init_droneChannel = __esm({
3857
3883
 
3858
3884
  // src/attachments.ts
3859
3885
  import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3860
- import { join as join19 } from "path";
3886
+ import { join as join20 } from "path";
3861
3887
  function attachmentsFromDoc(doc, origin) {
3862
3888
  const out = [];
3863
3889
  const walk2 = (node) => {
@@ -3948,7 +3974,7 @@ async function materializeAttachments(opts) {
3948
3974
  };
3949
3975
  const result = { files: [], skipped: [] };
3950
3976
  if (opts.refs.length === 0) return result;
3951
- const dir = join19(opts.worktree, ATTACHMENTS_DIR);
3977
+ const dir = join20(opts.worktree, ATTACHMENTS_DIR);
3952
3978
  await io.removeDir(dir);
3953
3979
  let dirReady = false;
3954
3980
  let total = 0;
@@ -3994,7 +4020,7 @@ async function materializeAttachments(opts) {
3994
4020
  await io.mkdir(dir);
3995
4021
  dirReady = true;
3996
4022
  }
3997
- await io.writeFile(join19(dir, name), file.bytes);
4023
+ await io.writeFile(join20(dir, name), file.bytes);
3998
4024
  } catch (e) {
3999
4025
  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)})`);
4000
4026
  continue;
@@ -4147,7 +4173,7 @@ var init_attachments = __esm({
4147
4173
 
4148
4174
  // src/shotUpload.ts
4149
4175
  import { readFile as fsReadFile } from "fs/promises";
4150
- import { basename as basename2, isAbsolute as isAbsolute3, join as join20 } from "path";
4176
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join21 } from "path";
4151
4177
  function shotMimetype(path) {
4152
4178
  const dot = path.lastIndexOf(".");
4153
4179
  if (dot < 0) return null;
@@ -4173,7 +4199,7 @@ async function readShots(paths, opts) {
4173
4199
  }
4174
4200
  let bytes;
4175
4201
  try {
4176
- bytes = await read(isAbsolute3(path) ? path : join20(opts.worktree, path));
4202
+ bytes = await read(isAbsolute3(path) ? path : join21(opts.worktree, path));
4177
4203
  } catch (e) {
4178
4204
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
4179
4205
  continue;
@@ -4252,7 +4278,7 @@ var init_shotUpload = __esm({
4252
4278
  // src/oauth.ts
4253
4279
  import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
4254
4280
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
4255
- import { join as join21 } from "path";
4281
+ import { join as join22 } from "path";
4256
4282
  function makePkce() {
4257
4283
  const verifier = randomBytes2(48).toString("base64url");
4258
4284
  const challenge = createHash2("sha256").update(verifier).digest("base64url");
@@ -4387,8 +4413,8 @@ var init_oauth = __esm({
4387
4413
  AUTHORIZE = "/api/oauth/authorize";
4388
4414
  TOKEN = "/api/oauth/token";
4389
4415
  SCOPE = "openid profile email taskless:full";
4390
- STORE_PATH2 = join21(SWARM_HOME, "oauth.json");
4391
- CLIENT_PATH = join21(SWARM_HOME, "oauth-client.json");
4416
+ STORE_PATH2 = join22(SWARM_HOME, "oauth.json");
4417
+ CLIENT_PATH = join22(SWARM_HOME, "oauth-client.json");
4392
4418
  base2 = (api) => api.replace(/\/$/, "");
4393
4419
  }
4394
4420
  });
@@ -4435,7 +4461,7 @@ var init_authState = __esm({
4435
4461
 
4436
4462
  // src/workspace.ts
4437
4463
  import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
4438
- import { join as join22 } from "path";
4464
+ import { join as join23 } from "path";
4439
4465
  function loadWorkspace() {
4440
4466
  try {
4441
4467
  const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
@@ -4452,7 +4478,7 @@ var init_workspace = __esm({
4452
4478
  "src/workspace.ts"() {
4453
4479
  "use strict";
4454
4480
  init_config();
4455
- WORKSPACE_PATH = join22(SWARM_HOME, "workspace.json");
4481
+ WORKSPACE_PATH = join23(SWARM_HOME, "workspace.json");
4456
4482
  }
4457
4483
  });
4458
4484
 
@@ -4558,6 +4584,28 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
4558
4584
  const text = res.content?.find((c) => c.type === "text")?.text;
4559
4585
  return typeof text === "string" && text.trim() ? text : null;
4560
4586
  }
4587
+ async function proposeRepoRules(input) {
4588
+ const res = await safeCall("swarm_repo_rule_propose", {
4589
+ repoFullName: input.repoFullName,
4590
+ rules: input.rules,
4591
+ ...input.by ? { by: input.by } : {},
4592
+ ...input.taskSlug ? { taskSlug: input.taskSlug } : {}
4593
+ });
4594
+ if (!res || res.isError) return null;
4595
+ const text = res.content?.find((c) => c.type === "text")?.text;
4596
+ return typeof text === "string" && text.trim() ? text : null;
4597
+ }
4598
+ async function decideRepoRules(input) {
4599
+ const res = await safeCall("swarm_repo_rule_decide", {
4600
+ repoFullName: input.repoFullName,
4601
+ decisions: input.decisions,
4602
+ ...input.by ? { by: input.by } : {},
4603
+ ...input.taskSlug ? { taskSlug: input.taskSlug } : {}
4604
+ });
4605
+ if (!res || res.isError) return null;
4606
+ const text = res.content?.find((c) => c.type === "text")?.text;
4607
+ return typeof text === "string" && text.trim() ? text : null;
4608
+ }
4561
4609
  async function resolveId(idOrSlug) {
4562
4610
  const cached = idCache.get(idOrSlug);
4563
4611
  if (cached) return cached;
@@ -4638,6 +4686,8 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
4638
4686
  getDocument,
4639
4687
  getTaskContext,
4640
4688
  reportAcceptanceCheck,
4689
+ proposeRepoRules,
4690
+ decideRepoRules,
4641
4691
  createTask: async (input) => {
4642
4692
  if (!input.projectId) {
4643
4693
  throw new Error("createTask: projectId \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u2014 \u0430\u0432\u0442\u043E-\u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0440\u043E\u0435\u043C \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u043E (TASWM-204)");
@@ -4947,7 +4997,8 @@ __export(db_exports, {
4947
4997
  SwarmDb: () => SwarmDb
4948
4998
  });
4949
4999
  import { createRequire as createRequire2 } from "module";
4950
- import { existsSync as existsSync11 } from "fs";
5000
+ import { randomUUID as randomUUID3 } from "crypto";
5001
+ import { existsSync as existsSync12 } from "fs";
4951
5002
  import { resolve as resolve3 } from "path";
4952
5003
  function rowToQuestion(row) {
4953
5004
  return {
@@ -5109,6 +5160,12 @@ CREATE INDEX IF NOT EXISTS idx_runs_area ON runs(area);
5109
5160
  CREATE INDEX IF NOT EXISTS idx_questions_run ON questions(run_id);
5110
5161
  CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
5111
5162
  CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id);
5163
+ -- \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
5164
+ -- (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.
5165
+ CREATE TABLE IF NOT EXISTS meta (
5166
+ key TEXT PRIMARY KEY,
5167
+ value TEXT NOT NULL
5168
+ );
5112
5169
  -- \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)
5113
5170
  CREATE TABLE IF NOT EXISTS imports (
5114
5171
  source TEXT PRIMARY KEY,
@@ -5121,6 +5178,8 @@ CREATE TABLE IF NOT EXISTS imports (
5121
5178
  defaultRepo;
5122
5179
  databaseCtor;
5123
5180
  // для importDb (второй Database, source)
5181
+ /** Идентификатор этой жизни файла базы (TASWM-419) — им неймспейсятся каталоги run-log'ов. */
5182
+ instanceId;
5124
5183
  constructor(path = "swarm.db", defaultRepo = "") {
5125
5184
  this.defaultRepo = normalizeRepo(defaultRepo);
5126
5185
  this.databaseCtor = loadBetterSqlite3();
@@ -5188,6 +5247,22 @@ CREATE TABLE IF NOT EXISTS imports (
5188
5247
  if (this.defaultRepo) {
5189
5248
  this.db.prepare("UPDATE runs SET repo = ? WHERE repo IS NULL").run(this.defaultRepo);
5190
5249
  }
5250
+ this.instanceId = this.ensureInstanceId();
5251
+ }
5252
+ /**
5253
+ * Идентификатор ЭТОЙ жизни файла базы (TASWM-419). Заводится при первом открытии и живёт
5254
+ * в самой базе, поэтому переживает рестарт демона, но НЕ переживает пересоздание файла:
5255
+ * снесли базу — новый id, и каталоги run-log'ов нового прогона #1 не подселяются к файлам
5256
+ * прогона #1 из прошлой жизни. Именно это и было корнем «лог не того прогона»: номер там
5257
+ * был вся информация об адресе.
5258
+ */
5259
+ ensureInstanceId() {
5260
+ const read = () => this.db.prepare("SELECT value FROM meta WHERE key = 'instance_id'").get()?.value;
5261
+ const existing = read();
5262
+ if (existing) return existing;
5263
+ const fresh = randomUUID3().replace(/-/g, "").slice(0, 12);
5264
+ this.db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES ('instance_id', ?)").run(fresh);
5265
+ return read() ?? fresh;
5191
5266
  }
5192
5267
  close() {
5193
5268
  this.db.close();
@@ -5282,6 +5357,15 @@ CREATE TABLE IF NOT EXISTS imports (
5282
5357
  const m = /^local:(\d+)$/.exec(key);
5283
5358
  return m ? this.getRun(Number(m[1])) : null;
5284
5359
  }
5360
+ /**
5361
+ * TASWM-419: адрес каталога run-log'ов — номер, НЕЙМСПЕЙСНУТЫЙ идентификатором файла базы
5362
+ * (`local:<instance>:<id>`). Тот же номер в пересозданной базе даёт другой instance, то
5363
+ * есть другой каталог: подселиться к файлам чужого прогона с тем же номером больше нечем.
5364
+ * Псевдонимов у sqlite-прогона нет — ключ не меняется, пока жив файл базы.
5365
+ */
5366
+ runLogKeys(id) {
5367
+ return this.getRun(id) ? [`local:${this.instanceId}:${id}`] : [];
5368
+ }
5285
5369
  listRuns(status) {
5286
5370
  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();
5287
5371
  return rows.map(rowToRun);
@@ -5522,7 +5606,7 @@ CREATE TABLE IF NOT EXISTS imports (
5522
5606
  const repoN = normalizeRepo(repo);
5523
5607
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
5524
5608
  if (already) return { imported: false, runs: already.run_count };
5525
- if (!existsSync11(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
5609
+ if (!existsSync12(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
5526
5610
  const src = new this.databaseCtor(abs);
5527
5611
  try {
5528
5612
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -5891,8 +5975,8 @@ var init_droneStore = __esm({
5891
5975
 
5892
5976
  // src/overmind.ts
5893
5977
  import { homedir as homedir5 } from "os";
5894
- import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync12, renameSync as renameSync10 } from "fs";
5895
- import { basename as basename4, dirname as dirname4, join as join29 } from "path";
5978
+ import { existsSync as existsSync17, readFileSync as readFileSync20, readdirSync as readdirSync13, renameSync as renameSync11 } from "fs";
5979
+ import { basename as basename4, dirname as dirname4, join as join30 } from "path";
5896
5980
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
5897
5981
  import { spawn as spawn3 } from "child_process";
5898
5982
 
@@ -5972,13 +6056,114 @@ init_config();
5972
6056
  init_resources();
5973
6057
  init_names2();
5974
6058
  init_storm();
5975
- import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
6059
+ import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
5976
6060
  import { dirname } from "path";
5977
6061
  import { randomUUID } from "crypto";
5978
6062
 
6063
+ // src/repoRules.ts
6064
+ var RULE_FIELDS = ["brief", "zones", "env", "verify", "conventions", "spine"];
6065
+ var DRONE_RULES_PER_RUN_MAX = 2;
6066
+ function parsePendingRepoRules(raw) {
6067
+ if (!Array.isArray(raw)) return [];
6068
+ const out = [];
6069
+ for (const item of raw) {
6070
+ if (!item || typeof item !== "object") continue;
6071
+ const r = item;
6072
+ const field = RULE_FIELDS.find((f) => f === r.field);
6073
+ if (!field) continue;
6074
+ out.push({
6075
+ field,
6076
+ value: r.value,
6077
+ by: typeof r.by === "string" ? r.by : null,
6078
+ task: typeof r.task === "string" ? r.task : null,
6079
+ why: typeof r.why === "string" ? r.why : null,
6080
+ cheap: r.cheap === true,
6081
+ overwritesHuman: r.overwritesHuman === true
6082
+ });
6083
+ }
6084
+ return out;
6085
+ }
6086
+ function ruleValueLine(value) {
6087
+ const json = JSON.stringify(value ?? null);
6088
+ return json.length > 400 ? `${json.slice(0, 400)}\u2026 (\u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E)` : json;
6089
+ }
6090
+ function renderPendingRulesBlock(rules) {
6091
+ if (rules.length === 0) return "";
6092
+ const lines = rules.map((r) => {
6093
+ const who = r.by ? `\u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u043B ${r.by}` : "\u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u043B \u0434\u0440\u043E\u043D";
6094
+ const on = r.task ? ` \u043D\u0430 ${r.task}` : "";
6095
+ const fate = r.cheap && !r.overwritesHuman ? "\u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u0441\u044F \u0432 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0443 \u0441\u0440\u0430\u0437\u0443" : r.overwritesHuman ? "\u043F\u0435\u0440\u0435\u043F\u0438\u0441\u0430\u043B\u043E \u0431\u044B \u043F\u0440\u0430\u0432\u043A\u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u2014 \u0443\u0439\u0434\u0451\u0442 \u0435\u043C\u0443 \u043D\u0430 \u043F\u0440\u0438\u0451\u043C\u043A\u0443, \u0434\u0430\u0436\u0435 \u0435\u0441\u043B\u0438 \u043F\u0440\u0438\u043C\u0435\u0448\u044C" : "\u0443\u0439\u0434\u0451\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443 \u043D\u0430 \u043F\u0440\u0438\u0451\u043C\u043A\u0443, \u0434\u0430\u0436\u0435 \u0435\u0441\u043B\u0438 \u043F\u0440\u0438\u043C\u0435\u0448\u044C (env/zones/spine \u0440\u043E\u0439 \u0441\u0430\u043C \u043D\u0435 \u043F\u0438\u0448\u0435\u0442)";
6096
+ return [
6097
+ `\u2022 ${r.field} \u2014 ${who}${on}. ${fate}.`,
6098
+ ` \u043F\u043E\u0447\u0435\u043C\u0443: ${r.why?.trim() || "(\u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0430 \u2014 \u044D\u0442\u043E \u0441\u0430\u043C\u043E \u043F\u043E \u0441\u0435\u0431\u0435 \u043F\u043E\u0432\u043E\u0434 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442\u044C)"}`,
6099
+ ` \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435: ${ruleValueLine(r.value)}`
6100
+ ].join("\n");
6101
+ });
6102
+ return [
6103
+ "\u{1F4CC} \u041F\u0420\u0410\u0412\u0418\u041B\u0410 \u041E \u0420\u0415\u041F\u041E\u0417\u0418\u0422\u041E\u0420\u0418\u0418, \u041F\u0420\u0418\u041D\u0415\u0421\u0401\u041D\u041D\u042B\u0415 \u0414\u0420\u041E\u041D\u041E\u041C (TASWM-431) \u2014 \u0440\u0435\u0448\u0430\u0435\u0448\u044C \u0442\u044B, \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u043E\u043C.",
6104
+ "\u0414\u0440\u043E\u043D \u043F\u0440\u0435\u0434\u043B\u0430\u0433\u0430\u0435\u0442 \u0442\u043E, \u043E\u0431\u043E \u0447\u0442\u043E \u0441\u043F\u043E\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u0438 \u0447\u0442\u043E \u0432 \u043A\u043E\u0434\u0435 \u043D\u0435 \u043D\u0430\u043F\u0438\u0441\u0430\u043D\u043E (\u0440\u0430\u0437\u0432\u0435\u0434\u0447\u0438\u043A \u0442\u0430\u043A\u043E\u0435 \u043D\u0435 \u043D\u0430\u0445\u043E\u0434\u0438\u0442). \u0422\u044B \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043B \u0434\u0438\u0444\u0444 \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430, \u0432\u0438\u0434\u0438\u0448\u044C \u0447\u0438\u0441\u043B\u043E \u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0439 \u0438 \u0447\u0442\u043E \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u043B \u2014 \u0442\u043E \u0435\u0441\u0442\u044C \u0435\u0434\u0438\u043D\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439, \u043A\u0442\u043E \u043E\u0442\u043B\u0438\u0447\u0438\u0442 \u043D\u0430\u0441\u0442\u043E\u044F\u0449\u0435\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u043E \u043E\u0442 \u043E\u0442\u043F\u0438\u0441\u043A\u0438.",
6105
+ ...lines,
6106
+ '\u0420\u0435\u0448\u0438 \u043F\u043E \u041A\u0410\u0416\u0414\u041E\u041C\u0423 \u0438 \u043F\u0435\u0440\u0435\u0434\u0430\u0439 \u0440\u0435\u0448\u0435\u043D\u0438\u044F \u0432 done: `rule_verdicts: [{ field: "verify", apply: true }, \u2026]`. apply=true \u2014 \xAB\u043F\u0440\u0430\u0432\u0434\u0430, \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0434\u0440\u043E\u043D \u043E\u0431\u044F\u0437\u0430\u043D \u044D\u0442\u043E \u0437\u043D\u0430\u0442\u044C\xBB; apply=false \u2014 \u043E\u0442\u043F\u0438\u0441\u043A\u0430/\u043E\u0431\u0449\u0435\u0435 \u043C\u0435\u0441\u0442\u043E (\xAB\u0442\u0435\u0441\u0442\u044B \u0433\u043E\u043D\u044F\u044E\u0442\u0441\u044F \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 \u0438\u0437 README\xBB), \u043F\u0440\u0430\u0432\u0438\u043B\u043E \u0441\u043D\u0438\u043C\u0435\u0442\u0441\u044F. \u041D\u0435 \u0443\u0432\u0435\u0440\u0435\u043D \u2014 apply=false: \u043F\u0440\u0430\u0432\u0438\u043B\u043E, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u043D\u0435\u0442, \u0441\u0442\u043E\u0438\u0442 \u0434\u0435\u0448\u0435\u0432\u043B\u0435 \u043D\u0435\u0432\u0435\u0440\u043D\u043E\u0433\u043E.',
6107
+ "\u041C\u043E\u043B\u0447\u0430\u043D\u0438\u0435 \u2014 \u0442\u043E\u0436\u0435 \u0438\u0441\u0445\u043E\u0434: \u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043B \u0440\u0435\u0448\u0435\u043D\u0438\u0435, \u0438 \u043F\u0440\u0430\u0432\u0438\u043B\u043E \u043E\u0441\u0442\u0430\u043D\u0435\u0442\u0441\u044F \u0432\u0438\u0441\u0435\u0442\u044C \u043D\u0430 \u043F\u0440\u0438\u0451\u043C\u043A\u0435 \u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430. \u042D\u0442\u043E \u043D\u0435 \u043E\u0448\u0438\u0431\u043A\u0430, \u043D\u043E \u0438 \u043D\u0435 \u0440\u0430\u0431\u043E\u0442\u0430."
6108
+ ].join("\n");
6109
+ }
6110
+
5979
6111
  // src/logTail.ts
5980
- import { closeSync, existsSync as existsSync4, openSync, readSync, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
6112
+ import { closeSync, existsSync as existsSync5, openSync, readSync, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
6113
+ import { join as join7 } from "path";
6114
+
6115
+ // src/runLogDir.ts
6116
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync4, renameSync as renameSync3, statSync as statSync3 } from "fs";
5981
6117
  import { join as join6 } from "path";
6118
+ var LEGACY_RUN_LOGS_SUBDIR = "legacy";
6119
+ function runLogDirName(runLogKey) {
6120
+ const safe2 = runLogKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
6121
+ 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)}`);
6122
+ return /^\d+$/.test(safe2) ? `k-${safe2}` : safe2;
6123
+ }
6124
+ function runLogDirPath(runLogsDir, runLogKey) {
6125
+ return join6(runLogsDir, runLogDirName(runLogKey));
6126
+ }
6127
+ function findRunLogDir(runLogsDir, keys) {
6128
+ for (const key of keys) {
6129
+ const dir = runLogDirPath(runLogsDir, key);
6130
+ if (existsSync4(dir)) return dir;
6131
+ }
6132
+ return null;
6133
+ }
6134
+ function runLogDirForWrite(runLogsDir, keys) {
6135
+ const canonical = keys[0];
6136
+ if (!canonical) return null;
6137
+ return findRunLogDir(runLogsDir, keys) ?? runLogDirPath(runLogsDir, canonical);
6138
+ }
6139
+ function sweepLegacyRunLogs(runLogsDir) {
6140
+ const legacyDir = join6(runLogsDir, LEGACY_RUN_LOGS_SUBDIR);
6141
+ let entries;
6142
+ try {
6143
+ entries = readdirSync4(runLogsDir);
6144
+ } catch {
6145
+ return { moved: 0, failed: 0, legacyDir };
6146
+ }
6147
+ let moved = 0;
6148
+ let failed = 0;
6149
+ for (const name of entries) {
6150
+ if (!/^\d+$/.test(name)) continue;
6151
+ const from = join6(runLogsDir, name);
6152
+ try {
6153
+ if (!statSync3(from).isDirectory()) continue;
6154
+ mkdirSync4(legacyDir, { recursive: true });
6155
+ let to = join6(legacyDir, name);
6156
+ for (let n = 2; existsSync4(to) && n < 100; n++) to = join6(legacyDir, `${name}-${n}`);
6157
+ renameSync3(from, to);
6158
+ moved++;
6159
+ } catch {
6160
+ failed++;
6161
+ }
6162
+ }
6163
+ return { moved, failed, legacyDir };
6164
+ }
6165
+
6166
+ // src/logTail.ts
5982
6167
  var TAIL_LINES_FALLBACK = 200;
5983
6168
  var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
5984
6169
  function fileHeader(name, size, shown) {
@@ -5988,7 +6173,7 @@ function fileHeader(name, size, shown) {
5988
6173
  function readTail(path, opts) {
5989
6174
  let size;
5990
6175
  try {
5991
- size = statSync3(path).size;
6176
+ size = statSync4(path).size;
5992
6177
  } catch {
5993
6178
  return null;
5994
6179
  }
@@ -6018,11 +6203,12 @@ function readTail(path, opts) {
6018
6203
  text = rows.join("\n");
6019
6204
  return { text, size, shown: Buffer.byteLength(text, "utf8") };
6020
6205
  }
6021
- function latestRunLaunch(runLogsDir, localRunId) {
6022
- const dir = join6(runLogsDir, String(localRunId));
6206
+ function latestRunLaunch(runLogsDir, runLogKeys) {
6207
+ const dir = findRunLogDir(runLogsDir, runLogKeys);
6208
+ if (!dir) return null;
6023
6209
  let files;
6024
6210
  try {
6025
- files = readdirSync4(dir);
6211
+ files = readdirSync5(dir);
6026
6212
  } catch {
6027
6213
  return null;
6028
6214
  }
@@ -6033,13 +6219,17 @@ function latestRunLaunch(runLogsDir, localRunId) {
6033
6219
  }
6034
6220
  if (indexes.size === 0) return null;
6035
6221
  const index = Math.max(...indexes);
6036
- const out = [join6(dir, `${index}.json`), join6(dir, `${index}.json.done`)].find((p) => existsSync4(p)) ?? null;
6037
- const err = existsSync4(join6(dir, `${index}.err.log`)) ? join6(dir, `${index}.err.log`) : null;
6222
+ const out = [join7(dir, `${index}.json`), join7(dir, `${index}.json.done`)].find((p) => existsSync5(p)) ?? null;
6223
+ const err = existsSync5(join7(dir, `${index}.err.log`)) ? join7(dir, `${index}.err.log`) : null;
6038
6224
  return { index, total: indexes.size, out, err };
6039
6225
  }
6040
- function runLogTail(runLogsDir, localRunId, opts) {
6041
- const launch = latestRunLaunch(runLogsDir, localRunId);
6042
- 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)` };
6226
+ function runLogTail(runLogsDir, runLogKeys, opts) {
6227
+ const launch = latestRunLaunch(runLogsDir, runLogKeys);
6228
+ if (!launch) {
6229
+ const canonical = runLogKeys[0];
6230
+ const where = canonical ? runLogDirPath(runLogsDir, canonical) : runLogsDir;
6231
+ 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)` };
6232
+ }
6043
6233
  const parts = [];
6044
6234
  let budget = opts.maxBytes;
6045
6235
  if (launch.err) {
@@ -6094,8 +6284,8 @@ async function serveLogRequests(deps, commands) {
6094
6284
  } else if (!cmd.runId) {
6095
6285
  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" };
6096
6286
  } else {
6097
- const local = deps.localRunId(cmd.runId);
6098
- 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);
6287
+ const keys = deps.runLogKeys(cmd.runId);
6288
+ 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);
6099
6289
  }
6100
6290
  } catch (e) {
6101
6291
  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)}` };
@@ -6191,6 +6381,11 @@ var SwarmClient = class _SwarmClient {
6191
6381
  // Живёт в памяти: рестарт демона просто переспросит прод.
6192
6382
  depsProd = /* @__PURE__ */ new Map();
6193
6383
  depsAskedAt = /* @__PURE__ */ new Map();
6384
+ // TASWM-399: ссылка dependsOn → человеко-слаг блокера, как его назвал прод. В колонке
6385
+ // лежит cuid прогона или задачи, а в лог демона, в событие прогона и в аномалию должен
6386
+ // уходить «TASWM-392», а не «cmsqqpphd011y2xtbckkut6bj» — иначе оператору всё равно
6387
+ // нечего искать. Резолв делает прод (см. resolveDepRefs), здесь только запоминаем.
6388
+ depSlug = /* @__PURE__ */ new Map();
6194
6389
  static DEPS_TTL_MS = 6e4;
6195
6390
  // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
6196
6391
  // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
@@ -6250,25 +6445,25 @@ var SwarmClient = class _SwarmClient {
6250
6445
  this.repoFullNames = registry.map((e) => e.repoFullName);
6251
6446
  this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
6252
6447
  this.host = opts.host ?? null;
6253
- this.repoExists = opts.repoExists ?? existsSync5;
6448
+ this.repoExists = opts.repoExists ?? existsSync6;
6254
6449
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
6255
6450
  this.executorId = opts.executorId ?? EXECUTOR.id;
6256
6451
  this.outboxPath = opts.outboxPath;
6257
6452
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
6258
6453
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
6259
- mkdirSync4(dirname(this.outboxPath), { recursive: true });
6454
+ mkdirSync5(dirname(this.outboxPath), { recursive: true });
6260
6455
  this.recover();
6261
6456
  }
6262
6457
  // ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
6263
6458
  recover() {
6264
- if (existsSync5(this.mapPath)) {
6459
+ if (existsSync6(this.mapPath)) {
6265
6460
  try {
6266
6461
  const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
6267
6462
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
6268
6463
  } catch {
6269
6464
  }
6270
6465
  }
6271
- if (existsSync5(this.outboxPath)) {
6466
+ if (existsSync6(this.outboxPath)) {
6272
6467
  const txt = readFileSync4(this.outboxPath, "utf8");
6273
6468
  for (const line of txt.split("\n")) {
6274
6469
  const s = line.trim();
@@ -6289,7 +6484,7 @@ var SwarmClient = class _SwarmClient {
6289
6484
  // eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
6290
6485
  // переприсваиваем локально (id событий — только порядок, не корреляция с продом).
6291
6486
  replayGlobalEvents() {
6292
- if (!existsSync5(this.stormPath)) return;
6487
+ if (!existsSync6(this.stormPath)) return;
6293
6488
  let txt;
6294
6489
  try {
6295
6490
  txt = readFileSync4(this.stormPath, "utf8");
@@ -6554,6 +6749,24 @@ var SwarmClient = class _SwarmClient {
6554
6749
  const viaProd = [...this.runs.values()].find((r) => r.runKey === `p:${prodId}` || this.prodIdByKey.get(r.runKey) === prodId);
6555
6750
  return viaProd ? this.mat(viaProd) : null;
6556
6751
  }
6752
+ /**
6753
+ * TASWM-419: адреса каталога run-log'ов прогона. КАНОНИЧЕСКИЙ — прод-вида `p:<cuid>`, как
6754
+ * только cuid известен: он один и тот же во всех жизнях демона, а локальный uuid прогона
6755
+ * после рестарта уже не воскресает (гидрированный прогон приезжает под прод-ключом).
6756
+ * Дальше — псевдонимы: uuid, под которым каталог мог завестись в окно между спавном и
6757
+ * первым флашем в прод. По ним каталог ищется, чтобы запуски одного прогона не расползлись
6758
+ * по двум каталогам (иначе cost недосчитал бы часть, а хвост показал бы не тот запуск).
6759
+ * Номер прогона в адресе не участвует вовсе — он и есть источник «лога чужого прогона».
6760
+ */
6761
+ runLogKeys(id) {
6762
+ const key = this.runs.get(id)?.runKey;
6763
+ if (!key) return [];
6764
+ const prodId = key.startsWith("p:") ? key.slice(2) : this.prodIdByKey.get(key);
6765
+ if (!prodId) return [key];
6766
+ const canonical = `p:${prodId}`;
6767
+ const aliases = [key, ...[...this.prodIdByKey.entries()].filter(([, v]) => v === prodId).map(([k]) => k)];
6768
+ return [canonical, ...new Set(aliases.filter((k) => k !== canonical))];
6769
+ }
6557
6770
  listRuns(status) {
6558
6771
  const all = [...this.runs.values()].filter((r) => status ? r.status === status : true);
6559
6772
  all.sort((a, b) => b.id - a.id);
@@ -6670,16 +6883,25 @@ var SwarmClient = class _SwarmClient {
6670
6883
  /**
6671
6884
  * TASWM-361: вердикт по каждой зависимости — см. докблок SwarmStore.depVerdicts.
6672
6885
  *
6673
- * АСИММЕТРИЯ ЗЕРКАЛА И ПРОДА, и почему она здесь намеренная. Зеркало — ЧАСТИЧНАЯ выборка
6674
- * (только прогоны этой машины, только окно гидрации), и по частичной выборке два вывода
6675
- * стоят разного:
6676
- * • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он не
6677
- * портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
6678
- * • «живых прогонов слага не осталось» — вывод от ОТСУТСТВИЯ: невидимый зеркалу прогон
6679
- * на соседней машине как раз и может быть живым. Поэтому «исчерпана неуспешно»
6680
- * принимаем ТОЛЬКО из ответа прода (refreshDeps) — он видит всех.
6681
- * Прод недоступен → вердикта нет → прогон честно ждёт следующего окна: лишняя минута
6682
- * очереди дешевле дрона, поднятого поверх работающего блокера.
6886
+ * ИСТОЧНИК ПРАВДЫ — ПРОД (TASWM-399), зеркало здесь только быстрый путь. Зеркало —
6887
+ * ЧАСТИЧНАЯ выборка (прогоны этой машины, окно гидрации), и по частичной выборке два
6888
+ * вывода стоят разного:
6889
+ * • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он
6890
+ * не портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
6891
+ * • всё остальное — вывод от ОТСУТСТВИЯ, и его принимаем ТОЛЬКО из ответа прода
6892
+ * (refreshDeps): невидимый зеркалу прогон на соседней машине как раз и может быть
6893
+ * живым, а закрытый с прода — давно смерженным.
6894
+ *
6895
+ * ССЫЛКА — НЕ ОБЯЗАТЕЛЬНО СЛАГ. В `dependsOn` лежит чаще всего cuid ПРОГОНА (так пишет
6896
+ * overmind), реже cuid задачи, и только дев-спавн `--after` пишет человеко-слаг. Быстрый
6897
+ * путь по зеркалу сравнивает `task_slug`, то есть срабатывает лишь на третьей форме —
6898
+ * это нормально: остальные две резолвит прод. А вот считать «раз в зеркале не нашёл —
6899
+ * значит не готово» было корнем TASWM-399.
6900
+ *
6901
+ * Прода ещё не спрашивали (первый тик) или он моргнул → `pending`: прогон честно ждёт
6902
+ * следующего окна. Лишняя минута очереди дешевле дрона, поднятого поверх работающего
6903
+ * блокера. `unknown` (блокера НЕ ЗНАЕТ И ПРОД) ставит только ответ прода — местным
6904
+ * незнанием такой вывод не делается.
6683
6905
  */
6684
6906
  depVerdicts(depends_on) {
6685
6907
  if (!depends_on || depends_on.length === 0) return [];
@@ -6689,57 +6911,89 @@ var SwarmClient = class _SwarmClient {
6689
6911
  doneInMirror.add(r.task_slug);
6690
6912
  }
6691
6913
  }
6692
- return depends_on.map((slug) => {
6693
- if (doneInMirror.has(slug)) {
6694
- return { slug, outcome: "done", met: true, status: "done", name: null, stopReason: null };
6914
+ return depends_on.map((ref) => {
6915
+ if (doneInMirror.has(ref)) {
6916
+ return { slug: ref, outcome: "done", met: true, status: "done", name: null, stopReason: null };
6917
+ }
6918
+ const fromProd = this.depsProd.get(ref);
6919
+ if (fromProd) {
6920
+ if (fromProd.outcome === "unknown" && this.knowsDepRef(ref)) {
6921
+ return { slug: this.depSlug.get(ref) ?? ref, outcome: "pending", met: false, status: null, name: null, stopReason: null };
6922
+ }
6923
+ return { slug: this.depSlug.get(ref) ?? ref, ...fromProd };
6695
6924
  }
6696
- const fromProd = this.depsProd.get(slug);
6697
- if (fromProd) return { slug, ...fromProd };
6698
- return { slug, outcome: "pending", met: false, status: null, name: null, stopReason: null };
6925
+ return { slug: ref, outcome: "pending", met: false, status: null, name: null, stopReason: null };
6699
6926
  });
6700
6927
  }
6701
6928
  /**
6702
- * TASWM-300: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
6929
+ * Знает ли ЗЕРКАЛО хоть один прогон по ссылке `dependsOn` (TASWM-399). Ссылка бывает
6930
+ * слагом, cuid'ом задачи и cuid'ом прогона — последний сверяем с прод-id, как это
6931
+ * делают runByProdId/refreshRun. Служебные сабраны (ревьюер/резолвер/верификатор) не
6932
+ * в счёт: их существование не означает, что жив сам блокер.
6933
+ */
6934
+ knowsDepRef(ref) {
6935
+ for (const r of this.runs.values()) {
6936
+ if (r.resolves_run_id != null || r.reviews_run_id != null || r.verifies_run_id != null) continue;
6937
+ if (r.task_slug === ref) return true;
6938
+ if (r.runKey === `p:${ref}` || this.prodIdByKey.get(r.runKey) === ref) return true;
6939
+ }
6940
+ return false;
6941
+ }
6942
+ /**
6943
+ * TASWM-300 → TASWM-399: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
6944
+ *
6945
+ * Почему зеркала мало. В нём лежат только прогоны ЭТОЙ машины (гейт isMine, hydrateRun) и
6946
+ * только те, что попали в окно гидрации (`swarm_list_runs limit`, сортировка по updatedAt
6947
+ * desc — сутки работы роя). Зависимость, закрытая на соседней машине, обнулившаяся при
6948
+ * рестарте демона или просто позавчерашняя, в зеркало не попадала — и `dependsOn` держал
6949
+ * прогон в очереди вечно.
6703
6950
  *
6704
- * Почему зеркала было мало. В нём лежат только прогоны ЭТОЙ машины (гейт isMine,
6705
- * hydrateRun) и только те, что попали в окно гидрации (`swarm_list_runs limit`, сортировка
6706
- * по updatedAt desc — сутки работы роя). Зависимость, закрытая на соседней машине или
6707
- * просто позавчера, в зеркало не попадала — и `--after` держал прогон в очереди вечно
6708
- * (04.08: снимали руками).
6951
+ * ЧЕМ СПРАШИВАЕМ И ПОЧЕМУ НЕ `swarm_list_runs` (корень TASWM-399). До этой задачи здесь
6952
+ * шёл `swarm_list_runs { taskId: <ref> }` — то есть ссылка трактовалась как ЗАДАЧА. Но в
6953
+ * `dependsOn` чаще всего лежит cuid ПРОГОНА: `resolveTaskRef` его задачей не признаёт,
6954
+ * ответ приходит `isError`, вердикт не появляется НИКОГДА, и прогон стоит молча при
6955
+ * свободной машине (12–13.08 — пять остановок роя за сутки). `swarm_deps_status` понимает
6956
+ * все три формы ссылки и отвечает по единому правилу роя (`judgeDep` на прод-стороне).
6709
6957
  *
6710
6958
  * Кеш. «Смержено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
6711
6959
  * положительный ответ помним навсегда и больше не спрашиваем. Остальные вердикты (включая
6712
- * «исчерпана неуспешно», TASWM-361 — блокер могут переиграть новым прогоном) переспрашиваем
6713
- * не чаще DEPS_TTL_MS: тик идёт раз в ~4с, а ждущих зависимость прогонов может быть
6714
- * много — без TTL это был бы поход в прод на каждый слаг каждый тик.
6960
+ * «исчерпана неуспешно» и «блокера не знает никто») переспрашиваем не чаще DEPS_TTL_MS:
6961
+ * оператор вправе переиграть блокер новым прогоном, а тик идёт раз в ~4 с — без TTL это
6962
+ * был бы поход в прод на каждую ссылку каждый тик.
6963
+ *
6964
+ * Одним вызовом на все просроченные ссылки, а не по вызову на ссылку: очередь ждущих
6965
+ * бывает в десятки прогонов, и N запросов на тик — это тот же самый поход в прод, только
6966
+ * дороже.
6715
6967
  */
6716
- async refreshDeps(slugs, nowMs = Date.now()) {
6717
- for (const slug of new Set(slugs)) {
6718
- if (!slug || this.depsProd.get(slug)?.outcome === "done") continue;
6719
- const asked = this.depsAskedAt.get(slug);
6720
- if (asked !== void 0 && nowMs - asked < _SwarmClient.DEPS_TTL_MS) continue;
6721
- this.depsAskedAt.set(slug, nowMs);
6722
- let res;
6723
- try {
6724
- res = await this.call("swarm_list_runs", { taskId: slug });
6725
- } catch {
6726
- this.depsAskedAt.delete(slug);
6727
- continue;
6728
- }
6729
- if (res?.isError) continue;
6730
- const runs = res.structuredContent?.runs ?? [];
6731
- const facts = runs.slice().sort((a, b) => String(a.createdAt ?? "").localeCompare(String(b.createdAt ?? ""))).map((r) => ({
6732
- status: String(r.status ?? ""),
6733
- // Служебный сабран под тем же слагом (резолвер конфликта / ревьюер): его финал —
6734
- // не финал задачи. Резолвер вычёркивался и до TASWM-361, ревьюер добавлен вместе
6735
- // с расширением критерия — иначе остановленный ревьюер закрывал бы зависимость.
6736
- service: Boolean(r.resolvesRunId) || Boolean(r.reviewsRunId) || Boolean(r.verifiesRunId),
6737
- // В проде имя дрона — колонка droneName (см. hydrateRun); `name` терпим как
6738
- // запасной вариант ради фикстур и старых ответов.
6739
- name: r.droneName ?? r.name ?? null,
6740
- stopReason: r.stopReason ?? null
6741
- }));
6742
- this.depsProd.set(slug, judgeDep(facts));
6968
+ async refreshDeps(refs, nowMs = Date.now()) {
6969
+ const ask = [...new Set(refs)].filter((ref) => {
6970
+ if (!ref || this.depsProd.get(ref)?.outcome === "done") return false;
6971
+ const asked = this.depsAskedAt.get(ref);
6972
+ return asked === void 0 || nowMs - asked >= _SwarmClient.DEPS_TTL_MS;
6973
+ });
6974
+ if (!ask.length) return;
6975
+ for (const ref of ask) this.depsAskedAt.set(ref, nowMs);
6976
+ let res;
6977
+ try {
6978
+ res = await this.call("swarm_deps_status", { refs: ask });
6979
+ } catch {
6980
+ for (const ref of ask) this.depsAskedAt.delete(ref);
6981
+ return;
6982
+ }
6983
+ if (res?.isError) return;
6984
+ const deps = res.structuredContent?.deps ?? [];
6985
+ for (const d of deps) {
6986
+ const ref = typeof d.ref === "string" ? d.ref : null;
6987
+ if (!ref) continue;
6988
+ const outcome = String(d.outcome ?? "pending");
6989
+ this.depsProd.set(ref, {
6990
+ outcome,
6991
+ met: Boolean(d.met),
6992
+ status: d.status ?? null,
6993
+ name: d.name ?? null,
6994
+ stopReason: d.stopReason ?? null
6995
+ });
6996
+ if (typeof d.slug === "string" && d.slug) this.depSlug.set(ref, d.slug);
6743
6997
  }
6744
6998
  }
6745
6999
  /**
@@ -7058,7 +7312,7 @@ var SwarmClient = class _SwarmClient {
7058
7312
  * транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
7059
7313
  */
7060
7314
  reconcileRuntime(opts) {
7061
- const wtExists = opts.worktreeExists ?? existsSync5;
7315
+ const wtExists = opts.worktreeExists ?? existsSync6;
7062
7316
  const pidByWorktree = /* @__PURE__ */ new Map();
7063
7317
  try {
7064
7318
  for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
@@ -7368,6 +7622,25 @@ var SwarmClient = class _SwarmClient {
7368
7622
  const sc = res.structuredContent ?? {};
7369
7623
  return normalizeBaseBranch(sc.config?.baseBranch) ?? null;
7370
7624
  }
7625
+ /**
7626
+ * ПРАВИЛА ОТ ДРОНОВ, ждущие вердикта ревьюера (TASWM-431): их демон кладёт в промпт
7627
+ * ревьюера — тот решает по ним в своём же вердикте. `taskSlug` — правила ЭТОГО
7628
+ * прогона: ревьюер отвечает за находки своего дрона, а не за чужие.
7629
+ *
7630
+ * Прод недоступен / старый прод (тула нет) / ответ не той формы → пустой список:
7631
+ * ревью обязано подниматься и без этого блока — правила тут не главное.
7632
+ */
7633
+ async repoPendingRules(repoFullName, taskSlug) {
7634
+ let res;
7635
+ try {
7636
+ res = await this.call("swarm_repo_rule_pending", { repoFullName, ...taskSlug ? { taskSlug } : {} });
7637
+ } catch {
7638
+ return [];
7639
+ }
7640
+ if (res?.isError) return [];
7641
+ const sc = res.structuredContent ?? {};
7642
+ return parsePendingRepoRules(sc.rules);
7643
+ }
7371
7644
  /**
7372
7645
  * Live КОМАНДЫ ПОДГОТОВКИ worktree из Taskless (TASWM-312): тот же swarm_repo_config по
7373
7646
  * repoFullName — Project.swarmConfig.prepare. Настройка сильнее детекта демона (deps.ts):
@@ -7411,29 +7684,11 @@ var SwarmClient = class _SwarmClient {
7411
7684
  const p = sc.profile;
7412
7685
  return p && typeof p === "object" && !Array.isArray(p) ? p : null;
7413
7686
  }
7414
- /**
7415
- * РАЗВЕДКА репозитория (TASWM-382): отправить в прод то, что разведчик вычитал в коде.
7416
- * Живую разметку это НЕ меняет — предложение ложится рядом и ждёт человека; поэтому
7417
- * ошибка здесь дорога ровно на стоимость прогона разведки, и мы её возвращаем, а не
7418
- * глотаем, как чтения выше.
7419
- *
7420
- * Мимо outbox (в отличие от мутаций прогонов): предложение имеет смысл только вместе
7421
- * со свежим состоянием репозитория, и доехавшее через сутки после разведки — хуже,
7422
- * чем не доехавшее вовсе. Прод недоступен — человек перезапустит разведку.
7423
- */
7424
- async reportRepoScout(repoFullName, report, by) {
7425
- let res;
7426
- try {
7427
- res = await this.call("swarm_repo_scout_report", { repoFullName, report, ...by ? { by } : {} });
7428
- } catch (e) {
7429
- return { ok: false, error: `\u043F\u0440\u043E\u0434 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D: ${e.message}` };
7430
- }
7431
- const text = res?.content?.map((c) => "text" in c ? c.text : "").join(" ") ?? "";
7432
- if (res?.isError) return { ok: false, error: text || "\u043F\u0440\u043E\u0434 \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0440\u0430\u0437\u0432\u0435\u0434\u043A\u0438" };
7433
- const sc = res.structuredContent ?? {};
7434
- const diff = Array.isArray(sc.diff) ? sc.diff : [];
7435
- return { ok: true, diff, display: text };
7436
- }
7687
+ // TASWM-441: отправка предложения разведки в прод (`swarm_repo_scout_report`) убрана —
7688
+ // разведчик больше не предлагает разметку в базу, а ведёт файл В САМОМ репозитории
7689
+ // (docs/AGENT_BRIEF.md, см. repoScout.ts). Прод-сторона тула и экран приёмки уезжают
7690
+ // отдельной задачей TASWM-442; `repoProfile` выше — её же остаток, читателей у него
7691
+ // в демоне не осталось.
7437
7692
  /**
7438
7693
  * Live ТРЕБОВАНИЯ К ОКРУЖЕНИЮ машины из Taskless (TASWM-313): тот же swarm_repo_config по
7439
7694
  * repoFullName — Project.swarmConfig.requires. Демон проверяет их ПЕРЕД спавном дрона и не
@@ -7547,6 +7802,12 @@ var SwarmClient = class _SwarmClient {
7547
7802
  ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
7548
7803
  ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
7549
7804
  ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {},
7805
+ // TASWM-424: «доступ отозван» — состояние ровно того же рода, что пауза, и
7806
+ // формулировка у него ОДНА на все поверхности (HOST_ACCESS_REVOKED_TEXT):
7807
+ // `hostLines` зовёт тот же `hostStateText`, что и парк машин в /swarm. Без
7808
+ // этого поля `swarm hosts` показывал отвергнутую машину просто молчащей —
7809
+ // то самое различие, ради которого флаг и заводили (TASWM-324).
7810
+ ...typeof h.accessRevoked === "boolean" ? { accessRevoked: h.accessRevoked } : {},
7550
7811
  // Показаний нет (старый прод, машина со старым демоном) — ключа не заводим вовсе,
7551
7812
  // ровно как с паузой выше: «не знаем» и так значит «не знаем».
7552
7813
  ...resources ? { resources } : {}
@@ -7632,9 +7893,18 @@ var SwarmClient = class _SwarmClient {
7632
7893
  * ВСЕГДА (в т.ч. `storm:false`), как паузу: это состояние, а не команда, и молчание тут
7633
7894
  * означало бы «прод помнит вчерашний шторм», когда рой давно работает.
7634
7895
  *
7896
+ * TASWM-402: тем же пульсом уезжают КОРНИ WORKTREE'ЕВ этой машины (`worktreeRoots` —
7897
+ * `owner/repo` → `<worktreesDir>/<repo-basename>`). Это НЕ рантайм и правило «pid и путь
7898
+ * дрона в прод не уходят» не трогает: корень — статичная конфигурация машины, того же
7899
+ * рода, что реестр репо и ёмкость, он не зависит ни от прогона, ни от живости процесса и
7900
+ * не протухает между пульсами. Прод склеивает `<корень>/<слаг>` сам — и отвечает человеку
7901
+ * на вопрос «куда идти руками», когда команда машине не доиграла (а в этом случае, по
7902
+ * определению, спросить рантайм у молчащей машины уже нельзя). Не передали корень (не
7903
+ * знаем своего каталога) → ключа нет, прод прошлое не трогает — как с capacity/resources.
7904
+ *
7635
7905
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
7636
7906
  */
7637
- async heartbeat(daemonVersion, pause, capacity, resources, storm, signIn) {
7907
+ async heartbeat(daemonVersion, pause, capacity, resources, storm, worktreesDir, signIn, tick) {
7638
7908
  try {
7639
7909
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
7640
7910
  if (daemonVersion) args.daemonVersion = daemonVersion;
@@ -7652,10 +7922,18 @@ var SwarmClient = class _SwarmClient {
7652
7922
  args.storm = storm.active;
7653
7923
  if (storm.reason) args.stormReason = storm.reason;
7654
7924
  }
7925
+ if (worktreesDir) {
7926
+ const roots = {};
7927
+ for (const [full, path] of this.repoPathByFullName) roots[full] = droneWorktreeRoot(worktreesDir, path);
7928
+ if (Object.keys(roots).length) args.worktreeRoots = roots;
7929
+ }
7655
7930
  if (this.host) {
7656
7931
  args.hostId = this.host.hostId;
7657
7932
  args.hostName = this.host.hostName;
7658
7933
  }
7934
+ if (tick && Number.isFinite(tick.seq) && Number.isFinite(tick.ageMs) && tick.intervalMs > 0) {
7935
+ args.tick = { seq: Math.max(0, Math.trunc(tick.seq)), ageMs: Math.max(0, Math.trunc(tick.ageMs)), intervalMs: Math.trunc(tick.intervalMs) };
7936
+ }
7659
7937
  const res = await this.call("swarm_heartbeat", args);
7660
7938
  if (res?.isError) return EMPTY_HEARTBEAT_REPLY;
7661
7939
  this.heartbeatOkAt = this.now();
@@ -7800,10 +8078,20 @@ var SwarmClient = class _SwarmClient {
7800
8078
  * именно путь. Не нашли в реестре (репо не наш / ручной `--repo <path>` спавн, где
7801
8079
  * repo — ВСЕГДА уже путь, старый формат) → возвращаем как есть без изменений
7802
8080
  * (обратная совместимость: раньше pr.repo ВСЕГДА был локальным путём).
8081
+ *
8082
+ * РЕГИСТР НЕ РАЗЛИЧАЕМ (TASWM-422). Ключи карты приведены к нижнему регистру
8083
+ * (parseRepoFullName — «GitHub сам не различает case в owner/repo»), а прод присылает
8084
+ * `run.repo` тем регистром, каким репозиторий записан в проекте. 17.08 это разошлось:
8085
+ * машина объявляла `ilyasmurov/ansamblist`, задача пришла с `ilyasmurov/Ansamblist`,
8086
+ * `Map.get` промахнулся — и вместо пути в прогон уехало полное имя репозитория. Дальше
8087
+ * git-операции шли в несуществующий каталог, прогон умирал за 5 секунд, а задача
8088
+ * возвращалась в очередь и бралась снова: пять павших прогонов за пять минут.
8089
+ * Возвращаем ИСХОДНУЮ строку, а не приведённую: промах бывает и на локальном пути
8090
+ * (ручной спавн), а путь регистр различает — вернув lowercase, мы сломали бы его.
7803
8091
  */
7804
8092
  resolveRepo(raw) {
7805
8093
  if (!raw) return "";
7806
- return this.repoPathByFullName.get(raw) ?? raw;
8094
+ return this.repoPathByFullName.get(raw) ?? this.repoPathByFullName.get(raw.toLowerCase()) ?? raw;
7807
8095
  }
7808
8096
  /**
7809
8097
  * TASWM-281: МОЯ ли это работа. Три исхода:
@@ -8243,21 +8531,49 @@ function parseDecision(v) {
8243
8531
  init_config();
8244
8532
  init_heartbeat();
8245
8533
 
8534
+ // src/tickHealth.ts
8535
+ init_src();
8536
+ function initialTickHealth(now) {
8537
+ return { seq: 0, lastAt: null, startedAt: now, lastError: null };
8538
+ }
8539
+ function markTickDone(health, now, error) {
8540
+ health.seq += 1;
8541
+ health.lastAt = now;
8542
+ health.lastError = error === void 0 ? null : describeTickError(error);
8543
+ }
8544
+ function describeTickError(error) {
8545
+ if (error instanceof Error) return error.message || error.name;
8546
+ return String(error);
8547
+ }
8548
+ function tickAgeMs(health, now) {
8549
+ return Math.max(0, now - (health.lastAt ?? health.startedAt));
8550
+ }
8551
+ var TICK_SELF_HEAL_FLOOR_MS = 6e5;
8552
+ function tickSelfHealMs(intervalMs) {
8553
+ return Math.max(hostTickStallMs(intervalMs) * 2, TICK_SELF_HEAL_FLOOR_MS);
8554
+ }
8555
+ function tickSelfHealDue(health, now, intervalMs) {
8556
+ return tickAgeMs(health, now) > tickSelfHealMs(intervalMs);
8557
+ }
8558
+ function tickPulse(health, now, intervalMs) {
8559
+ return { seq: health.seq, ageMs: tickAgeMs(health, now), intervalMs };
8560
+ }
8561
+
8246
8562
  // src/logRotate.ts
8247
- import { copyFileSync, renameSync as renameSync4, statSync as statSync4, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
8563
+ import { copyFileSync, renameSync as renameSync5, statSync as statSync5, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync7 } from "fs";
8248
8564
  var realIo = {
8249
8565
  size: (path) => {
8250
8566
  try {
8251
- return statSync4(path).size;
8567
+ return statSync5(path).size;
8252
8568
  } catch {
8253
8569
  return null;
8254
8570
  }
8255
8571
  },
8256
8572
  copy: copyFileSync,
8257
- rename: renameSync4,
8573
+ rename: renameSync5,
8258
8574
  truncate: (path) => truncateSync(path, 0),
8259
8575
  unlink: unlinkSync2,
8260
- exists: existsSync6
8576
+ exists: existsSync7
8261
8577
  };
8262
8578
  var realOwnerIo = {
8263
8579
  statFd: (fd) => {
@@ -8270,7 +8586,7 @@ var realOwnerIo = {
8270
8586
  },
8271
8587
  statPath: (path) => {
8272
8588
  try {
8273
- const s = statSync4(path);
8589
+ const s = statSync5(path);
8274
8590
  return { dev: s.dev, ino: s.ino };
8275
8591
  } catch {
8276
8592
  return null;
@@ -8342,12 +8658,12 @@ async function buildRepoRegistry(paths) {
8342
8658
 
8343
8659
  // src/repoMemory.ts
8344
8660
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
8345
- import { join as join7 } from "path";
8661
+ import { join as join8 } from "path";
8346
8662
  var FILE = "repos.json";
8347
8663
  function loadRememberedRepos(home) {
8348
8664
  let raw;
8349
8665
  try {
8350
- raw = readFileSync6(join7(home, FILE), "utf8");
8666
+ raw = readFileSync6(join8(home, FILE), "utf8");
8351
8667
  } catch {
8352
8668
  return [];
8353
8669
  }
@@ -8362,7 +8678,7 @@ function loadRememberedRepos(home) {
8362
8678
  function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
8363
8679
  const clean = paths.filter((p) => p.trim().length > 0);
8364
8680
  if (clean.length === 0) return;
8365
- writeFileSync6(join7(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
8681
+ writeFileSync6(join8(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
8366
8682
  `);
8367
8683
  }
8368
8684
  function pickRepoPaths(args) {
@@ -8377,12 +8693,12 @@ init_src();
8377
8693
  init_config();
8378
8694
  init_linkFailure();
8379
8695
  import { homedir as homedir3 } from "os";
8380
- import { readFileSync as readFileSync9, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
8381
- import { isAbsolute, join as join10, resolve as resolve2 } from "path";
8696
+ import { readFileSync as readFileSync9, renameSync as renameSync6, writeFileSync as writeFileSync8 } from "fs";
8697
+ import { isAbsolute, join as join11, resolve as resolve2 } from "path";
8382
8698
 
8383
8699
  // src/repoScan.ts
8384
- import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
8385
- import { join as join9 } from "path";
8700
+ import { readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
8701
+ import { join as join10 } from "path";
8386
8702
  var SKIP = /* @__PURE__ */ new Set([
8387
8703
  "node_modules",
8388
8704
  "vendor",
@@ -8412,7 +8728,7 @@ function parseOriginFromGitConfig(text) {
8412
8728
  }
8413
8729
  function originOf(dir) {
8414
8730
  try {
8415
- return parseOriginFromGitConfig(readFileSync8(join9(dir, ".git", "config"), "utf8"));
8731
+ return parseOriginFromGitConfig(readFileSync8(join10(dir, ".git", "config"), "utf8"));
8416
8732
  } catch {
8417
8733
  return null;
8418
8734
  }
@@ -8441,15 +8757,15 @@ function walk(dir, depth, maxDepth, found) {
8441
8757
  }
8442
8758
  let entries;
8443
8759
  try {
8444
- entries = readdirSync5(dir);
8760
+ entries = readdirSync6(dir);
8445
8761
  } catch {
8446
8762
  return;
8447
8763
  }
8448
8764
  for (const name of entries) {
8449
8765
  if (name.startsWith(".") || SKIP.has(name)) continue;
8450
- const child = join9(dir, name);
8766
+ const child = join10(dir, name);
8451
8767
  try {
8452
- if (!statSync5(child).isDirectory()) continue;
8768
+ if (!statSync6(child).isDirectory()) continue;
8453
8769
  } catch {
8454
8770
  continue;
8455
8771
  }
@@ -8469,7 +8785,7 @@ function defaultScanRoots(home, knownPaths = []) {
8469
8785
  if (parent && parent !== p) add(parent);
8470
8786
  }
8471
8787
  for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
8472
- add(join9(home, name));
8788
+ add(join10(home, name));
8473
8789
  }
8474
8790
  return out;
8475
8791
  }
@@ -8483,7 +8799,7 @@ function resolveScanRoots(raw, home = homedir3(), knownPaths = []) {
8483
8799
  for (const part of v.split(",")) {
8484
8800
  const p = part.trim();
8485
8801
  if (!p) continue;
8486
- const abs = p === "~" ? home : p.startsWith("~/") ? join10(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
8802
+ const abs = p === "~" ? home : p.startsWith("~/") ? join11(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
8487
8803
  if (!out.includes(abs)) out.push(abs);
8488
8804
  }
8489
8805
  return out;
@@ -8548,6 +8864,28 @@ function describeReposFailure(kind, detail) {
8548
8864
  function emptyRepoMapState(roots) {
8549
8865
  return { clones: [], missing: [], roots: [...roots], at: null, unknown: true };
8550
8866
  }
8867
+ var SCAN_REASON_TEXT = {
8868
+ "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",
8869
+ "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",
8870
+ "cache-expired": "\u043A\u044D\u0448 \u043A\u0430\u0440\u0442\u044B \u043F\u0440\u043E\u0442\u0443\u0445"
8871
+ };
8872
+ var REPO_SCAN_TTL_MS = 24 * 60 * 6e4;
8873
+ var UNKNOWN_LIST_SCAN_TTL_MS = 60 * 6e4;
8874
+ function isScanStale(scannedAt, ttlMs, now = Date.now()) {
8875
+ if (!scannedAt) return true;
8876
+ const t = stampMs(scannedAt);
8877
+ if (!Number.isFinite(t)) return true;
8878
+ return now - t >= ttlMs;
8879
+ }
8880
+ function decideScan(opts) {
8881
+ if (!opts.known) {
8882
+ const ttl = Math.min(opts.ttlMs, UNKNOWN_LIST_SCAN_TTL_MS);
8883
+ return isScanStale(opts.scannedAt, ttl, opts.now) ? { scan: true, reason: "list-unknown" } : { scan: false, reason: null };
8884
+ }
8885
+ if (opts.missingWanted > 0) return { scan: true, reason: "wanted-missing" };
8886
+ if (isScanStale(opts.scannedAt, opts.ttlMs, opts.now)) return { scan: true, reason: "cache-expired" };
8887
+ return { scan: false, reason: null };
8888
+ }
8551
8889
  async function refreshRepoMap(opts) {
8552
8890
  const log = opts.log ?? (() => {
8553
8891
  });
@@ -8561,14 +8899,21 @@ async function refreshRepoMap(opts) {
8561
8899
  clones.set(repoFullName, { repoFullName, path, source, alternatives });
8562
8900
  };
8563
8901
  for (const e of opts.configured) take(e.repoFullName.toLowerCase(), e.path, "configured");
8564
- for (const e of readRepoMapCache(opts.cachePath)) {
8902
+ const cache = loadRepoMapCache(opts.cachePath);
8903
+ for (const e of cache.entries) {
8565
8904
  const name = e.repoFullName.toLowerCase();
8566
8905
  if (clones.has(name)) continue;
8567
8906
  if (repoAt(e.path) === name) take(name, e.path, "cache");
8568
8907
  }
8569
8908
  const wantedNames = wanted ? normalizeFullNames(wanted.map((r) => r.repoFullName)) : [];
8570
- const needScan = wanted ? wantedNames.some((n) => !clones.has(n)) : clones.size === 0;
8571
- if (needScan) {
8909
+ const decision = decideScan({
8910
+ known: !!wanted,
8911
+ missingWanted: wantedNames.filter((n) => !clones.has(n)).length,
8912
+ scannedAt: cache.scannedAt,
8913
+ ttlMs: opts.scanTtlMs ?? REPO_SCAN_TTL_MS
8914
+ });
8915
+ let scannedAt = cache.scannedAt;
8916
+ if (decision.scan) {
8572
8917
  const startedAt = Date.now();
8573
8918
  const scanned = scan(opts.roots, opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {});
8574
8919
  let added = 0;
@@ -8577,9 +8922,10 @@ async function refreshRepoMap(opts) {
8577
8922
  take(full, paths[0], "scan", paths.slice(1));
8578
8923
  added++;
8579
8924
  }
8580
- if (added > 0) {
8581
- 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`);
8582
- }
8925
+ scannedAt = stamp();
8926
+ log(
8927
+ `[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)"}`
8928
+ );
8583
8929
  }
8584
8930
  const registry = [...clones.values()].map((c) => ({
8585
8931
  repoFullName: c.repoFullName,
@@ -8587,10 +8933,12 @@ async function refreshRepoMap(opts) {
8587
8933
  }));
8588
8934
  writeRepoMapCache(
8589
8935
  opts.cachePath,
8590
- [...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path }))
8936
+ [...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path })),
8937
+ scannedAt
8591
8938
  );
8592
8939
  if (!answer.ok) {
8593
8940
  log(describeReposFailure(answer.kind, answer.detail));
8941
+ 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)`);
8594
8942
  return registry;
8595
8943
  }
8596
8944
  opts.state.unknown = false;
@@ -8602,7 +8950,7 @@ async function refreshRepoMap(opts) {
8602
8950
  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");
8603
8951
  return registry;
8604
8952
  }
8605
- 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:`);
8953
+ log(`[overmind] ${describeMapSummary(opts.state, opts.roots)}`);
8606
8954
  for (const c of opts.state.clones) log(`[overmind] ${describeClone(c)}`);
8607
8955
  const prefixOf = new Map(answer.repos.map((r) => [r.repoFullName, r.taskPrefix]));
8608
8956
  for (const name of opts.state.missing) {
@@ -8611,6 +8959,11 @@ async function refreshRepoMap(opts) {
8611
8959
  }
8612
8960
  return registry;
8613
8961
  }
8962
+ function describeMapSummary(state, roots) {
8963
+ const found = state.clones.length;
8964
+ 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)";
8965
+ 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}`;
8966
+ }
8614
8967
  function describeClone(c) {
8615
8968
  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";
8616
8969
  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` : "";
@@ -8631,40 +8984,43 @@ function repoMapRows(state) {
8631
8984
  ...state.missing.map((repoFullName) => ({ repoFullName, path: null, source: null, alternatives: [] }))
8632
8985
  ];
8633
8986
  }
8634
- function readRepoMapCache(path) {
8987
+ function loadRepoMapCache(path) {
8635
8988
  try {
8636
8989
  const raw = JSON.parse(readFileSync9(path, "utf8"));
8637
- if (!Array.isArray(raw?.entries)) return [];
8638
- return raw.entries.filter(
8639
- (e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
8640
- );
8990
+ if (!Array.isArray(raw?.entries)) return { scannedAt: null, entries: [] };
8991
+ return {
8992
+ scannedAt: typeof raw.scannedAt === "string" && raw.scannedAt ? raw.scannedAt : null,
8993
+ entries: raw.entries.filter(
8994
+ (e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
8995
+ )
8996
+ };
8641
8997
  } catch {
8642
- return [];
8998
+ return { scannedAt: null, entries: [] };
8643
8999
  }
8644
9000
  }
8645
- function writeRepoMapCache(path, entries) {
9001
+ function writeRepoMapCache(path, entries, scannedAt = stamp()) {
8646
9002
  try {
8647
9003
  const tmp = `${path}.tmp`;
8648
- writeFileSync8(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
8649
- renameSync5(tmp, path);
9004
+ writeFileSync8(tmp, JSON.stringify({ at: stamp(), scannedAt, entries }, null, 2));
9005
+ renameSync6(tmp, path);
8650
9006
  } catch {
8651
9007
  }
8652
9008
  }
8653
9009
 
8654
9010
  // src/hostIdentity.ts
8655
9011
  import { hostname } from "os";
8656
- import { existsSync as existsSync7, readFileSync as readFileSync10, renameSync as renameSync6, writeFileSync as writeFileSync9 } from "fs";
8657
- import { join as join11 } from "path";
9012
+ import { existsSync as existsSync8, readFileSync as readFileSync10, renameSync as renameSync7, writeFileSync as writeFileSync9 } from "fs";
9013
+ import { join as join12 } from "path";
8658
9014
  import { randomUUID as randomUUID2 } from "crypto";
8659
9015
  function hostIdentityPath(home) {
8660
- return join11(home, "host.json");
9016
+ return join12(home, "host.json");
8661
9017
  }
8662
9018
  function loadHostIdentity(deps) {
8663
- const exists = deps.exists ?? existsSync7;
9019
+ const exists = deps.exists ?? existsSync8;
8664
9020
  const readFile2 = deps.readFile ?? ((p) => readFileSync10(p, "utf8"));
8665
9021
  const writeFile3 = deps.writeFile ?? ((p, data) => {
8666
9022
  writeFileSync9(`${p}.tmp`, data);
8667
- renameSync6(`${p}.tmp`, p);
9023
+ renameSync7(`${p}.tmp`, p);
8668
9024
  });
8669
9025
  const host = deps.hostname ?? hostname;
8670
9026
  const newId = deps.newId ?? randomUUID2;
@@ -8846,15 +9202,15 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
8846
9202
 
8847
9203
  // src/launcher.ts
8848
9204
  import { spawn } from "child_process";
8849
- import { closeSync as closeSync2, existsSync as existsSync8, mkdirSync as mkdirSync6, openSync as openSync2, readdirSync as readdirSync7 } from "fs";
9205
+ import { closeSync as closeSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, openSync as openSync2, readdirSync as readdirSync8 } from "fs";
8850
9206
  import { createRequire } from "module";
8851
9207
  import { homedir as homedir4 } from "os";
8852
- import { dirname as dirname2, join as join13 } from "path";
9208
+ import { dirname as dirname2, join as join14 } from "path";
8853
9209
  import { fileURLToPath } from "url";
8854
9210
 
8855
9211
  // src/droneHome.ts
8856
- import { lstatSync, mkdirSync as mkdirSync5, readdirSync as readdirSync6, rmSync, statSync as statSync6, symlinkSync as symlinkSync2 } from "fs";
8857
- import { join as join12, relative, sep } from "path";
9212
+ import { lstatSync, mkdirSync as mkdirSync6, readdirSync as readdirSync7, rmSync, statSync as statSync7, symlinkSync as symlinkSync2 } from "fs";
9213
+ import { join as join13, relative, sep } from "path";
8858
9214
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
8859
9215
  function droneHomeDeny(env = process.env) {
8860
9216
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -8872,29 +9228,29 @@ function buildDroneHome(opts) {
8872
9228
  const { realHome, droneHome } = opts;
8873
9229
  const deny = opts.deny ?? droneHomeDeny();
8874
9230
  const platform = opts.platform ?? process.platform;
8875
- mkdirSync5(droneHome, { recursive: true, mode: 448 });
8876
- const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
9231
+ mkdirSync6(droneHome, { recursive: true, mode: 448 });
9232
+ const wanted = new Set(mirroredEntries(readdirSync7(realHome), deny, platform));
8877
9233
  const rel = relative(realHome, droneHome);
8878
9234
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
8879
- for (const existing of readdirSync6(droneHome)) {
9235
+ for (const existing of readdirSync7(droneHome)) {
8880
9236
  if (wanted.has(existing)) continue;
8881
- const p = join12(droneHome, existing);
9237
+ const p = join13(droneHome, existing);
8882
9238
  try {
8883
9239
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
8884
9240
  } catch {
8885
9241
  }
8886
9242
  }
8887
9243
  for (const name of wanted) {
8888
- const link = join12(droneHome, name);
9244
+ const link = join13(droneHome, name);
8889
9245
  try {
8890
9246
  lstatSync(link);
8891
9247
  continue;
8892
9248
  } catch {
8893
9249
  }
8894
- const target = join12(realHome, name);
9250
+ const target = join13(realHome, name);
8895
9251
  let isDir;
8896
9252
  try {
8897
- isDir = statSync6(target).isDirectory();
9253
+ isDir = statSync7(target).isDirectory();
8898
9254
  } catch {
8899
9255
  continue;
8900
9256
  }
@@ -8912,7 +9268,7 @@ var REAL_PATH_ENV_PINS = [
8912
9268
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
8913
9269
  ];
8914
9270
  function realPathEnvPins(realHome) {
8915
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join12(realHome, ...segments)]));
9271
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join13(realHome, ...segments)]));
8916
9272
  }
8917
9273
  function dronePrivatePaths(realHome, swarmHome) {
8918
9274
  return [
@@ -9024,7 +9380,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
9024
9380
  const env = { ...base3 };
9025
9381
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
9026
9382
  if (isolate && !base3.TURBO_CACHE_DIR) {
9027
- env.TURBO_CACHE_DIR = join13(worktree, ".turbo", "cache");
9383
+ env.TURBO_CACHE_DIR = join14(worktree, ".turbo", "cache");
9028
9384
  }
9029
9385
  Object.assign(env, droneTestEnv(droneCap, base3));
9030
9386
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -9052,20 +9408,21 @@ var DAEMON_STATE_ENV = [
9052
9408
  "TASKLESS_TOKEN"
9053
9409
  ];
9054
9410
  function bundledRoleDir(kind) {
9055
- return join13(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
9411
+ return join14(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
9056
9412
  }
9057
9413
  function roleDir(worktree, kind) {
9058
- return join13(worktree, ".claude", "roles", kind);
9414
+ return join14(worktree, ".claude", "roles", kind);
9059
9415
  }
9060
9416
  function roleDirs(worktree, kind) {
9061
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync8(dir));
9417
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync9(dir));
9062
9418
  }
9063
- function nextRunLogPath(runLogsDir, runId) {
9064
- const dir = join13(runLogsDir, String(runId));
9065
- mkdirSync6(dir, { recursive: true });
9419
+ function nextRunLogPath(runLogsDir, runLogKeys) {
9420
+ const dir = runLogDirForWrite(runLogsDir, runLogKeys);
9421
+ 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)");
9422
+ mkdirSync7(dir, { recursive: true });
9066
9423
  let files = [];
9067
9424
  try {
9068
- files = readdirSync7(dir);
9425
+ files = readdirSync8(dir);
9069
9426
  } catch {
9070
9427
  }
9071
9428
  let next = 0;
@@ -9073,11 +9430,11 @@ function nextRunLogPath(runLogsDir, runId) {
9073
9430
  const m = /^(\d+)\.json/.exec(f);
9074
9431
  if (m) next = Math.max(next, Number(m[1]) + 1);
9075
9432
  }
9076
- return join13(dir, `${next}.json`);
9433
+ return join14(dir, `${next}.json`);
9077
9434
  }
9078
- function openNextRunLog(runLogsDir, runId) {
9435
+ function openNextRunLog(runLogsDir, runLogKeys) {
9079
9436
  for (let attempt = 0; attempt < 50; attempt++) {
9080
- const path = nextRunLogPath(runLogsDir, runId);
9437
+ const path = nextRunLogPath(runLogsDir, runLogKeys);
9081
9438
  let out;
9082
9439
  try {
9083
9440
  out = openSync2(path, "wx");
@@ -9093,7 +9450,7 @@ function openNextRunLog(runLogsDir, runId) {
9093
9450
  }
9094
9451
  return { out, err };
9095
9452
  }
9096
- 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?)`);
9453
+ 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?`);
9097
9454
  }
9098
9455
  function makeLauncher(cfg) {
9099
9456
  return ({ worktree, run, prompt, continueSession }) => {
@@ -9156,9 +9513,9 @@ function makeLauncher(cfg) {
9156
9513
  }
9157
9514
  const realHome = cfg.droneIsolation?.realHome ?? homedir4();
9158
9515
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
9159
- if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join13(cfg.droneIsolation.swarmHome, "token"))) {
9516
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync9(join14(cfg.droneIsolation.swarmHome, "token"))) {
9160
9517
  console.error(
9161
- `[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.`
9518
+ `[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.`
9162
9519
  );
9163
9520
  }
9164
9521
  const executor = cfg.executor ?? resolveExecutor();
@@ -9185,7 +9542,13 @@ function makeLauncher(cfg) {
9185
9542
  let outFd;
9186
9543
  let errFd;
9187
9544
  if (cfg.runLogsDir) {
9188
- const fds = openNextRunLog(cfg.runLogsDir, run.id);
9545
+ const logKeys = cfg.runLogKeys?.(run) ?? [];
9546
+ if (logKeys.length === 0) {
9547
+ throw new Error(
9548
+ `\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).`
9549
+ );
9550
+ }
9551
+ const fds = openNextRunLog(cfg.runLogsDir, logKeys);
9189
9552
  outFd = fds.out;
9190
9553
  errFd = fds.err ?? void 0;
9191
9554
  stdio = ["ignore", outFd, errFd ?? "ignore"];
@@ -9213,21 +9576,21 @@ function makeLauncher(cfg) {
9213
9576
  init_procCleanup();
9214
9577
 
9215
9578
  // src/cost.ts
9579
+ import { readFileSync as readFileSync11, readdirSync as readdirSync9, renameSync as renameSync8 } from "fs";
9580
+ import { join as join15 } from "path";
9216
9581
  init_executors();
9217
- import { readFileSync as readFileSync11, readdirSync as readdirSync8, renameSync as renameSync7 } from "fs";
9218
- import { join as join14 } from "path";
9219
9582
  var UNPROCESSED_LOG_RE = /^\d+\.json$/;
9220
9583
  function unprocessedLogFiles(dir) {
9221
9584
  try {
9222
- return readdirSync8(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
9585
+ return readdirSync9(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
9223
9586
  } catch {
9224
9587
  return [];
9225
9588
  }
9226
9589
  }
9227
9590
  function accountRunCost(deps, run) {
9228
- const dir = join14(deps.runLogsDir, String(run.id));
9229
- const files = unprocessedLogFiles(dir);
9230
- if (files.length === 0) return;
9591
+ const dir = findRunLogDir(deps.runLogsDir, deps.db.runLogKeys(run.id));
9592
+ const files = dir ? unprocessedLogFiles(dir) : [];
9593
+ if (!dir || files.length === 0) return;
9231
9594
  const model = resolveRunModel(run, deps.swarmModels?.() ?? null);
9232
9595
  const executor = deps.executor ?? resolveExecutor();
9233
9596
  let added = 0;
@@ -9236,7 +9599,7 @@ function accountRunCost(deps, run) {
9236
9599
  let durationMs = 0;
9237
9600
  const processed = [];
9238
9601
  for (const f of files) {
9239
- const full = join14(dir, f);
9602
+ const full = join15(dir, f);
9240
9603
  let raw = null;
9241
9604
  try {
9242
9605
  raw = readFileSync11(full, "utf8");
@@ -9265,7 +9628,7 @@ function accountRunCost(deps, run) {
9265
9628
  }
9266
9629
  for (const full of processed) {
9267
9630
  try {
9268
- renameSync7(full, `${full}.done`);
9631
+ renameSync8(full, `${full}.done`);
9269
9632
  } catch {
9270
9633
  }
9271
9634
  }
@@ -9273,20 +9636,34 @@ function accountRunCost(deps, run) {
9273
9636
  function makeCostAccountant(deps) {
9274
9637
  return (run) => accountRunCost(deps, run);
9275
9638
  }
9639
+ function sweepPendingRunCosts(deps) {
9640
+ for (const run of deps.db.listRuns()) {
9641
+ if (run.pid != null && deps.isAlive(run.pid)) continue;
9642
+ try {
9643
+ accountRunCost(deps, run);
9644
+ } catch (e) {
9645
+ 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)}`);
9646
+ }
9647
+ }
9648
+ }
9649
+ function makeCostSweeper(deps) {
9650
+ return () => sweepPendingRunCosts(deps);
9651
+ }
9276
9652
 
9277
9653
  // src/runOutcome.ts
9654
+ import { readdirSync as readdirSync10, readFileSync as readFileSync12 } from "fs";
9655
+ import { join as join16 } from "path";
9278
9656
  init_executors();
9279
- import { readdirSync as readdirSync9, readFileSync as readFileSync12 } from "fs";
9280
- import { join as join15 } from "path";
9281
9657
  function endedItself(outcome) {
9282
9658
  return outcome === "graceful" || outcome === "budget";
9283
9659
  }
9284
9660
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
9285
- function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
9286
- const dir = join15(runLogsDir, String(runId));
9661
+ function classifyExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
9662
+ const dir = findRunLogDir(runLogsDir, runLogKeys);
9663
+ if (!dir) return "unknown";
9287
9664
  let files;
9288
9665
  try {
9289
- files = readdirSync9(dir);
9666
+ files = readdirSync10(dir);
9290
9667
  } catch {
9291
9668
  return "unknown";
9292
9669
  }
@@ -9300,7 +9677,7 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
9300
9677
  if (!latest) return "unknown";
9301
9678
  let raw;
9302
9679
  try {
9303
- raw = readFileSync12(join15(dir, latest.name), "utf8");
9680
+ raw = readFileSync12(join16(dir, latest.name), "utf8");
9304
9681
  } catch {
9305
9682
  return "unknown";
9306
9683
  }
@@ -9308,9 +9685,9 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
9308
9685
  if (!result) return "crash";
9309
9686
  return result.stoppedByBudget ? "budget" : "graceful";
9310
9687
  }
9311
- function makeExitClassifier(runLogsDir, executor) {
9688
+ function makeExitClassifier(runLogsDir, runLogKeys, executor) {
9312
9689
  const exec = executor ?? resolveExecutor();
9313
- return (run) => classifyExit(runLogsDir, run.id, exec);
9690
+ return (run) => classifyExit(runLogsDir, runLogKeys(run.id), exec);
9314
9691
  }
9315
9692
 
9316
9693
  // src/orchestrator.ts
@@ -9318,20 +9695,20 @@ init_src();
9318
9695
  init_git();
9319
9696
 
9320
9697
  // src/deps.ts
9321
- import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync13 } from "fs";
9322
- import { join as join18 } from "path";
9698
+ import { existsSync as existsSync11, readdirSync as readdirSync12, readFileSync as readFileSync13 } from "fs";
9699
+ import { join as join19 } from "path";
9323
9700
  import { execa } from "execa";
9324
9701
 
9325
9702
  // src/prepareLock.ts
9326
9703
  import { createHash } from "crypto";
9327
- import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as statSync8, writeSync } from "fs";
9328
- import { join as join17 } from "path";
9704
+ import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as statSync9, writeSync } from "fs";
9705
+ import { join as join18 } from "path";
9329
9706
  import { tmpdir } from "os";
9330
9707
  var WAIT_TIMEOUT_MS = 5 * 6e4;
9331
9708
  var POLL_MS = 300;
9332
9709
  var STALE_MS = 20 * 6e4;
9333
9710
  function machineLockPath(key, dir = tmpdir()) {
9334
- return join17(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
9711
+ return join18(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
9335
9712
  }
9336
9713
  function defaultTryCreate(path, content) {
9337
9714
  try {
@@ -9348,7 +9725,7 @@ function defaultTryCreate(path, content) {
9348
9725
  }
9349
9726
  function defaultAgeMs(path) {
9350
9727
  try {
9351
- return Date.now() - statSync8(path).mtimeMs;
9728
+ return Date.now() - statSync9(path).mtimeMs;
9352
9729
  } catch {
9353
9730
  return null;
9354
9731
  }
@@ -9412,14 +9789,14 @@ function safeReadText(p) {
9412
9789
  }
9413
9790
  function safeListDirs(p) {
9414
9791
  try {
9415
- return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
9792
+ return readdirSync12(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
9416
9793
  } catch {
9417
9794
  return [];
9418
9795
  }
9419
9796
  }
9420
9797
  function safeListFiles(p) {
9421
9798
  try {
9422
- return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
9799
+ return readdirSync12(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
9423
9800
  } catch {
9424
9801
  return [];
9425
9802
  }
@@ -9445,21 +9822,21 @@ function overridePlan(override) {
9445
9822
  };
9446
9823
  }
9447
9824
  function detectPackageManager(worktree, io = {}) {
9448
- const exists = io.exists ?? existsSync10;
9825
+ const exists = io.exists ?? existsSync11;
9449
9826
  const readText = io.readText ?? safeReadText;
9450
- const hasPkgJson = exists(join18(worktree, "package.json"));
9827
+ const hasPkgJson = exists(join19(worktree, "package.json"));
9451
9828
  if (!hasPkgJson) {
9452
- if (exists(join18(worktree, "deno.json")) || exists(join18(worktree, "deno.jsonc")) || exists(join18(worktree, "deno.lock"))) return "deno";
9829
+ if (exists(join19(worktree, "deno.json")) || exists(join19(worktree, "deno.jsonc")) || exists(join19(worktree, "deno.lock"))) return "deno";
9453
9830
  return null;
9454
9831
  }
9455
- const pkg = readJson(readText, join18(worktree, "package.json"));
9832
+ const pkg = readJson(readText, join19(worktree, "package.json"));
9456
9833
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
9457
9834
  if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
9458
- if (exists(join18(worktree, "bun.lock")) || exists(join18(worktree, "bun.lockb"))) return "bun";
9459
- if (exists(join18(worktree, "pnpm-lock.yaml"))) return "pnpm";
9460
- if (exists(join18(worktree, "yarn.lock"))) return "yarn";
9461
- if (exists(join18(worktree, "package-lock.json"))) return "npm";
9462
- if (exists(join18(worktree, "deno.lock"))) return "deno";
9835
+ if (exists(join19(worktree, "bun.lock")) || exists(join19(worktree, "bun.lockb"))) return "bun";
9836
+ if (exists(join19(worktree, "pnpm-lock.yaml"))) return "pnpm";
9837
+ if (exists(join19(worktree, "yarn.lock"))) return "yarn";
9838
+ if (exists(join19(worktree, "package-lock.json"))) return "npm";
9839
+ if (exists(join19(worktree, "deno.lock"))) return "deno";
9463
9840
  return "npm";
9464
9841
  }
9465
9842
  function jsInstallCommand(pm) {
@@ -9512,7 +9889,7 @@ var ECOSYSTEM_MARKERS = [
9512
9889
  // конфигурация — дело сборки, не подготовки
9513
9890
  ];
9514
9891
  function detectPlan(worktree, io) {
9515
- const exists = io.exists ?? existsSync10;
9892
+ const exists = io.exists ?? existsSync11;
9516
9893
  const listFiles = io.listFiles ?? safeListFiles;
9517
9894
  const pm = detectPackageManager(worktree, io);
9518
9895
  if (pm) {
@@ -9520,7 +9897,7 @@ function detectPlan(worktree, io) {
9520
9897
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
9521
9898
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
9522
9899
  // заново, но он дешёвый: тянет из того же общего кэша.
9523
- { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join18(worktree, "node_modules"), source: "detect" }
9900
+ { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join19(worktree, "node_modules"), source: "detect" }
9524
9901
  ];
9525
9902
  for (const target of workspaceBuildTargets(worktree, io)) {
9526
9903
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
@@ -9529,12 +9906,12 @@ function detectPlan(worktree, io) {
9529
9906
  return { source: "detect", ecosystem: `node/${pm}`, commands };
9530
9907
  }
9531
9908
  for (const marker of ECOSYSTEM_MARKERS) {
9532
- const hit = marker.files.some((f) => exists(join18(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
9909
+ const hit = marker.files.some((f) => exists(join19(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
9533
9910
  if (!hit) continue;
9534
9911
  let ecosystem = marker.ecosystem;
9535
9912
  let command = marker.command;
9536
9913
  for (const refine of marker.refine ?? []) {
9537
- if (!exists(join18(worktree, refine.file))) continue;
9914
+ if (!exists(join19(worktree, refine.file))) continue;
9538
9915
  ecosystem = refine.ecosystem;
9539
9916
  command = refine.command;
9540
9917
  break;
@@ -9591,12 +9968,12 @@ var CI_INSTALL_PATTERNS = [
9591
9968
  function ciWorkflowPlan(worktree, io) {
9592
9969
  const readText = io.readText ?? safeReadText;
9593
9970
  const listFiles = io.listFiles ?? safeListFiles;
9594
- const dir = join18(worktree, ".github", "workflows");
9971
+ const dir = join19(worktree, ".github", "workflows");
9595
9972
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
9596
9973
  const seen = /* @__PURE__ */ new Set();
9597
9974
  const commands = [];
9598
9975
  for (const name of files.sort()) {
9599
- const yaml = readText(join18(dir, name));
9976
+ const yaml = readText(join19(dir, name));
9600
9977
  if (yaml === null) continue;
9601
9978
  for (const raw of workflowRunLines(yaml)) {
9602
9979
  const line = raw.replace(/\s+/g, " ").trim();
@@ -9611,7 +9988,7 @@ function ciWorkflowPlan(worktree, io) {
9611
9988
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
9612
9989
  }
9613
9990
  function workspaceGlobs(worktree, readText) {
9614
- const yaml = readText(join18(worktree, "pnpm-workspace.yaml"));
9991
+ const yaml = readText(join19(worktree, "pnpm-workspace.yaml"));
9615
9992
  if (yaml !== null) {
9616
9993
  const globs = [];
9617
9994
  let inPackages = false;
@@ -9631,7 +10008,7 @@ function workspaceGlobs(worktree, readText) {
9631
10008
  }
9632
10009
  if (globs.length) return globs;
9633
10010
  }
9634
- const pkg = readJson(readText, join18(worktree, "package.json"));
10011
+ const pkg = readJson(readText, join19(worktree, "package.json"));
9635
10012
  const ws = pkg?.workspaces;
9636
10013
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
9637
10014
  return list.filter((g) => typeof g === "string");
@@ -9642,14 +10019,14 @@ function workspaceDirs(worktree, globs, io) {
9642
10019
  if (glob.startsWith("!")) continue;
9643
10020
  const star = glob.indexOf("*");
9644
10021
  if (star === -1) {
9645
- const dir = join18(worktree, glob);
9646
- if (io.exists(join18(dir, "package.json"))) dirs.add(dir);
10022
+ const dir = join19(worktree, glob);
10023
+ if (io.exists(join19(dir, "package.json"))) dirs.add(dir);
9647
10024
  continue;
9648
10025
  }
9649
- const base3 = join18(worktree, glob.slice(0, star).replace(/\/$/, ""));
10026
+ const base3 = join19(worktree, glob.slice(0, star).replace(/\/$/, ""));
9650
10027
  for (const name of io.listDirs(base3)) {
9651
- const dir = join18(base3, name);
9652
- if (io.exists(join18(dir, "package.json"))) dirs.add(dir);
10028
+ const dir = join19(base3, name);
10029
+ if (io.exists(join19(dir, "package.json"))) dirs.add(dir);
9653
10030
  }
9654
10031
  }
9655
10032
  return [...dirs];
@@ -9670,18 +10047,18 @@ function entryPaths(pkg) {
9670
10047
  return out;
9671
10048
  }
9672
10049
  function workspaceBuildTargets(worktree, io = {}) {
9673
- const exists = io.exists ?? existsSync10;
10050
+ const exists = io.exists ?? existsSync11;
9674
10051
  const readText = io.readText ?? safeReadText;
9675
10052
  const listDirs = io.listDirs ?? safeListDirs;
9676
10053
  const globs = workspaceGlobs(worktree, readText);
9677
10054
  if (!globs.length) return [];
9678
10055
  const pkgs = [];
9679
10056
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
9680
- const pkg = readJson(readText, join18(dir, "package.json"));
10057
+ const pkg = readJson(readText, join19(dir, "package.json"));
9681
10058
  if (!pkg || typeof pkg.name !== "string") continue;
9682
10059
  const scripts = pkg.scripts ?? {};
9683
10060
  const entries = entryPaths(pkg);
9684
- const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join18(dir, e)));
10061
+ const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join19(dir, e)));
9685
10062
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
9686
10063
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
9687
10064
  }
@@ -9709,7 +10086,7 @@ function devDatabaseUrl(worktree, io = {}) {
9709
10086
  const fromEnv = (io.env ?? process.env).DATABASE_URL;
9710
10087
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
9711
10088
  for (const file of [".env", ".env.example"]) {
9712
- const raw = readText(join18(worktree, file));
10089
+ const raw = readText(join19(worktree, file));
9713
10090
  if (raw === null) continue;
9714
10091
  const m = /^\s*(?:export\s+)?DATABASE_URL\s*=\s*(.*)$/m.exec(raw);
9715
10092
  const value = (m?.[1] ?? "").trim().replace(/^['"]|['"]$/g, "");
@@ -9725,11 +10102,11 @@ function isLocalDatabase(url) {
9725
10102
  }
9726
10103
  }
9727
10104
  function prismaPackages(worktree, io) {
9728
- const exists = io.exists ?? existsSync10;
10105
+ const exists = io.exists ?? existsSync11;
9729
10106
  const readText = io.readText ?? safeReadText;
9730
10107
  const listDirs = io.listDirs ?? safeListDirs;
9731
10108
  const dirs = [worktree, ...workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs })];
9732
- return dirs.filter((dir) => exists(join18(dir, "prisma", "schema.prisma")) && listDirs(join18(dir, "prisma", "migrations")).length > 0);
10109
+ return dirs.filter((dir) => exists(join19(dir, "prisma", "schema.prisma")) && listDirs(join19(dir, "prisma", "migrations")).length > 0);
9733
10110
  }
9734
10111
  function jsExecBin(pm, command) {
9735
10112
  switch (pm) {
@@ -9762,7 +10139,7 @@ function planPrepare(worktree, io = {}) {
9762
10139
  return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
9763
10140
  }
9764
10141
  async function ensureWorktreeDeps(worktree, io = {}) {
9765
- const exists = io.exists ?? existsSync10;
10142
+ const exists = io.exists ?? existsSync11;
9766
10143
  const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
9767
10144
  const approve = io.approve ?? (() => true);
9768
10145
  const plan = planPrepare(worktree, io);
@@ -9947,6 +10324,43 @@ function pruneDefers(episodes, live) {
9947
10324
  for (const key of episodes.keys()) if (!live.has(key)) episodes.delete(key);
9948
10325
  }
9949
10326
 
10327
+ // src/spawnAttempts.ts
10328
+ init_src();
10329
+ var PRE_START_ATTEMPT_LIMIT = 3;
10330
+ var PRE_START_MARK = "\u0434\u0440\u043E\u043D \u043D\u0435 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u0430\u043B";
10331
+ var SPAWN_PARK_MARK = "\u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0442\u043B\u043E\u0436\u0435\u043D\u0430 \u0440\u043E\u0435\u043C";
10332
+ function isPreStartFailure(run) {
10333
+ return run.status === "failed" && (run.stop_reason ?? "").includes(PRE_START_MARK);
10334
+ }
10335
+ function isSpawnParked(run) {
10336
+ return (run.stop_reason ?? "").includes(SPAWN_PARK_MARK);
10337
+ }
10338
+ var SPAWN_ATTEMPT_WINDOW_MS = 60 * 6e4;
10339
+ function preStartAttempts(runs, taskSlug, opts = {}) {
10340
+ const windowMs = opts.windowMs ?? SPAWN_ATTEMPT_WINDOW_MS;
10341
+ const now = opts.now ?? Date.now();
10342
+ 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);
10343
+ let n = 0;
10344
+ for (const run of own) {
10345
+ if (run.status === "queued" || run.status === "running") continue;
10346
+ if (isSpawnParked(run) && run.status !== "failed") continue;
10347
+ if (!isPreStartFailure(run)) break;
10348
+ const at = run.created_at ? stampMs(run.created_at) : Number.NaN;
10349
+ if (Number.isFinite(at) && now - at >= windowMs) break;
10350
+ n += 1;
10351
+ }
10352
+ return n;
10353
+ }
10354
+ function unresolvedRepoReason(repo, served) {
10355
+ const raw = repo.trim();
10356
+ if (!raw || raw.startsWith("/") || raw.startsWith("~") || /^[a-z]:[\\/]/i.test(raw)) return null;
10357
+ 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)";
10358
+ 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.`;
10359
+ }
10360
+ function parkReason(attempts, reason) {
10361
+ 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}`;
10362
+ }
10363
+
9950
10364
  // src/overmind.ts
9951
10365
  init_git();
9952
10366
 
@@ -10210,6 +10624,7 @@ init_attachments();
10210
10624
  init_shotUpload();
10211
10625
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10212
10626
  import { z } from "zod";
10627
+ init_git();
10213
10628
  init_src();
10214
10629
 
10215
10630
  // src/reviewCycle.ts
@@ -10586,10 +11001,57 @@ ${notFoundText}` : notFoundText }],
10586
11001
  ),
10587
11002
  screenshots: z.array(z.string()).optional().describe(
10588
11003
  `\u041F\u0443\u0442\u0438 \u043A \u0441\u043D\u044F\u0442\u044B\u043C \u0441\u043A\u0440\u0438\u043D\u0430\u043C (png/jpg/webp/gif) \u2014 \u0438\u0445 \u043F\u0440\u0438\u043B\u043E\u0436\u0430\u0442 \u043A \u0442\u0432\u043E\u0435\u043C\u0443 \u043E\u0442\u0447\u0451\u0442\u0443 \u0412 \u0417\u0410\u0414\u0410\u0427\u0415, \u0447\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. \u041F\u0443\u0442\u044C \u043E\u0442\u043D\u043E\u0441\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0439 (\u043E\u0442 worktree) \u0438\u043B\u0438 \u0430\u0431\u0441\u043E\u043B\u044E\u0442\u043D\u044B\u0439, \u0434\u043E ${SHOT_MAX_FILES} \u0444\u0430\u0439\u043B\u043E\u0432. \u041C\u0435\u043D\u044F\u043B \u0432\u0438\u0434\u0438\u043C\u044B\u0439 UI \u2014 \u043F\u0440\u0438\u043A\u043B\u0430\u0434\u044B\u0432\u0430\u0439 \u043E\u0431\u0430 \u0432\u044C\u044E\u043F\u043E\u0440\u0442\u0430 (desktop+mobile), \u044D\u0442\u043E \u0438 \u0435\u0441\u0442\u044C \u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E \u0440\u0430\u0431\u043E\u0442\u044B.`
11004
+ ),
11005
+ // TASWM-431: правило о репозитории — то, обо что дрон СПОТКНУЛСЯ. Форму значения
11006
+ // проверяет прод (там живёт схема разметки), здесь она намеренно свободная: гейт
11007
+ // на форму в двух местах разъехался бы, а отказ прода дрон и так увидит в ответе.
11008
+ rules: z.array(
11009
+ z.object({
11010
+ field: z.enum([...RULE_FIELDS]).describe("\u043A\u0430\u043A\u043E\u0435 \u043F\u043E\u043B\u0435 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u044D\u0442\u043E \u043F\u0440\u0430\u0432\u0438\u043B\u043E: verify | conventions | brief | env | zones | spine"),
11011
+ value: z.unknown().describe("\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u043F\u043E\u043B\u044F \u2014 \u0442\u043E\u0439 \u0436\u0435 \u0444\u043E\u0440\u043C\u044B, \u0447\u0442\u043E \u0432 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0435 (verify: {commands,green}, env: {database,migrations,vars}, conventions: {commitLanguage,slugInCommit,baseBranch}, brief: \u0441\u0442\u0440\u043E\u043A\u0430, zones: \u043C\u0430\u0441\u0441\u0438\u0432 \u0437\u043E\u043D, spine: \u043C\u0430\u0441\u0441\u0438\u0432 \u043F\u0443\u0442\u0435\u0439)"),
11012
+ why: z.string().describe("\u043E\u0431\u043E \u0447\u0442\u043E \u0441\u043F\u043E\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u0438 \u0447\u0435\u0433\u043E \u044D\u0442\u043E \u0441\u0442\u043E\u0438\u043B\u043E \u2014 \u043F\u043E \u044D\u0442\u043E\u043C\u0443 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0438 \u043E\u0442\u043B\u0438\u0447\u0438\u0442 \u043F\u0440\u0430\u0432\u0438\u043B\u043E \u043E\u0442 \u043E\u0442\u043F\u0438\u0441\u043A\u0438")
11013
+ })
11014
+ ).optional().describe(
11015
+ `\u041F\u0420\u0410\u0412\u0418\u041B\u041E \u041E \u0420\u0415\u041F\u041E\u0417\u0418\u0422\u041E\u0420\u0418\u0418, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u0432 \u043D\u0451\u043C \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043E (TASWM-431) \u2014 \u043D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${DRONE_RULES_PER_RUN_MAX} \u0437\u0430 \u043F\u0440\u043E\u0433\u043E\u043D \u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u0442\u043E, \u043E\u0431\u043E \u0447\u0442\u043E \u0442\u044B \u0420\u0415\u0410\u041B\u042C\u041D\u041E \u0441\u043F\u043E\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u0438 \u043F\u043E\u0442\u0440\u0430\u0442\u0438\u043B \u0432\u0440\u0435\u043C\u044F (\xAB\u0438\u043D\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u043E\u043D\u043D\u044B\u0435 \u0442\u0435\u0441\u0442\u044B \u0436\u0434\u0443\u0442 \u043F\u043E\u0441\u0442\u0433\u0440\u0435\u0441 \u043D\u0430 5433\xBB). \u041E\u0431\u0449\u0435\u0435 \u043C\u0435\u0441\u0442\u043E (\xAB\u0442\u0435\u0441\u0442\u044B \u0433\u043E\u043D\u044F\u044E\u0442\u0441\u044F \u0447\u0435\u0440\u0435\u0437 pnpm test\xBB) \u043F\u0440\u0430\u0432\u0438\u043B\u043E\u043C \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F. \u0423\u0436\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043D\u043E\u0435 \u043E\u0442\u0441\u0435\u0435\u0442\u0441\u044F \u0441\u0430\u043C\u043E. \u0420\u0435\u0448\u0430\u0435\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0432 \u0441\u0432\u043E\u0451\u043C \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0435.`
11016
+ ),
11017
+ rule_verdicts: z.array(z.object({ field: z.enum([...RULE_FIELDS]), apply: z.boolean() })).optional().describe(
11018
+ "\u0422\u041E\u041B\u042C\u041A\u041E \u0414\u041B\u042F \u0420\u0415\u0412\u042C\u042E\u0415\u0420\u0410 (TASWM-431): \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u043F\u043E \u043F\u0440\u0430\u0432\u0438\u043B\u0430\u043C, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u043F\u0440\u0438\u043D\u0451\u0441 \u0434\u0440\u043E\u043D (\u043E\u043D\u0438 \u043F\u0435\u0440\u0435\u0447\u0438\u0441\u043B\u0435\u043D\u044B \u0432 \u0442\u0432\u043E\u0451\u043C \u043F\u0440\u043E\u043C\u043F\u0442\u0435). apply=true \u2014 \u043F\u0440\u0430\u0432\u0438\u043B\u043E \u043D\u0430\u0441\u0442\u043E\u044F\u0449\u0435\u0435, apply=false \u2014 \u043E\u0442\u043F\u0438\u0441\u043A\u0430, \u0441\u043D\u044F\u0442\u044C. verify/conventions/brief \u043F\u0440\u0438\u043C\u0435\u043D\u044F\u0442\u0441\u044F \u0432 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0443 \u0441\u0440\u0430\u0437\u0443, env/zones/spine \u0443\u0439\u0434\u0443\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443 \u0432 \u043B\u044E\u0431\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435."
10589
11019
  )
10590
11020
  },
10591
- guard(async ({ pr_url, verdict, summary, screenshots }) => {
11021
+ guard(async ({ pr_url, verdict, summary, screenshots, rules, rule_verdicts }) => {
10592
11022
  const run = await store.myRun();
11023
+ let repoFullNameCache = null;
11024
+ const repoFullNameOnce = () => repoFullNameCache ??= remoteRepoFullName(opts.worktree ?? process.cwd()).catch(() => null);
11025
+ const offerRules = async (taskSlug) => {
11026
+ if (!rules?.length) return "";
11027
+ if (!opts.taskless?.proposeRepoRules) return "\n\n\u26A0\uFE0F \u041F\u0440\u0430\u0432\u0438\u043B\u043E \u043E \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E: Taskless \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D.";
11028
+ const repoFullName = await repoFullNameOnce();
11029
+ if (!repoFullName) {
11030
+ await store.addEvent(null, "step", "\u043F\u0440\u0430\u0432\u0438\u043B\u043E \u043E \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u044C owner/repo \u043F\u043E origin");
11031
+ return "\n\n\u26A0\uFE0F \u041F\u0440\u0430\u0432\u0438\u043B\u043E \u043E \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u044C owner/repo \u2014 \u0441\u043A\u0430\u0436\u0438 \u044D\u0442\u043E \u0432 PR.";
11032
+ }
11033
+ const text = await opts.taskless.proposeRepoRules({ repoFullName, rules, ...run?.name ? { by: run.name } : {}, ...taskSlug ? { taskSlug } : {} }).catch((e) => `\u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E: ${e instanceof Error ? e.message : String(e)}`);
11034
+ const line = text ?? "\u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E (\u043F\u0440\u043E\u0434 \u043E\u0442\u043A\u0430\u0437\u0430\u043B \u0438\u043B\u0438 \u0442\u0443\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D)";
11035
+ await store.addEvent(null, "step", `\u043F\u0440\u0430\u0432\u0438\u043B\u043E \u043E \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043E\u0442 \u0434\u0440\u043E\u043D\u0430: ${line}`);
11036
+ return `
11037
+
11038
+ \u{1F4CC} ${line}`;
11039
+ };
11040
+ const decideRules = async (taskSlug) => {
11041
+ if (!rule_verdicts?.length) return "";
11042
+ if (!opts.taskless?.decideRepoRules) return "";
11043
+ const repoFullName = await repoFullNameOnce();
11044
+ if (!repoFullName) {
11045
+ await store.addEvent(null, "step", "\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u043F\u0440\u0430\u0432\u0438\u043B\u0430\u043C \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u044C owner/repo \u043F\u043E origin");
11046
+ return "\n\n\u26A0\uFE0F \u0412\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u043F\u0440\u0430\u0432\u0438\u043B\u0430\u043C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u041D\u0415 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u044C owner/repo.";
11047
+ }
11048
+ const text = await opts.taskless.decideRepoRules({ repoFullName, decisions: rule_verdicts, ...run?.name ? { by: run.name } : {}, ...taskSlug ? { taskSlug } : {} }).catch((e) => `\u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${e instanceof Error ? e.message : String(e)}`);
11049
+ const line = text ?? "\u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D (\u043F\u0440\u043E\u0434 \u043E\u0442\u043A\u0430\u0437\u0430\u043B \u0438\u043B\u0438 \u0442\u0443\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D)";
11050
+ await store.addEvent(null, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u043F\u0440\u0430\u0432\u0438\u043B\u0430\u043C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F: ${line}`);
11051
+ return `
11052
+
11053
+ \u{1F4CC} ${line}`;
11054
+ };
10593
11055
  const reportSlug = (slug) => opts.expectedTaskSlug ?? slug;
10594
11056
  const uploadShots = async () => {
10595
11057
  if (!screenshots?.length) return { attachments: [], warning: "" };
@@ -10675,13 +11137,14 @@ ${notFoundText}` : notFoundText }],
10675
11137
  commentWarning = opts.taskless ? "\u0437\u0430\u0434\u0430\u0447\u0430 \u0440\u0435\u0432\u044C\u044E \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430" : "\u043A\u043B\u0438\u0435\u043D\u0442 Taskless \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0451\u043D";
10676
11138
  await store.addEvent(run.reviews_run_id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442-\u043A\u043E\u043C\u043C\u0435\u043D\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${commentWarning}`);
10677
11139
  }
11140
+ const rulesNote = await decideRules(reviewedSlug);
10678
11141
  const trustedPrUrl = reviewed?.pr_url ?? pr_url;
10679
11142
  await store.updateRun(null, { status: "review", pr_url: trustedPrUrl });
10680
11143
  await store.addEvent(null, "pr_opened", trustedPrUrl);
10681
11144
  const commentNote = commentWarning ? `
10682
11145
 
10683
11146
  \u26A0\uFE0F \u0412\u0435\u0440\u0434\u0438\u043A\u0442-\u043A\u043E\u043C\u043C\u0435\u043D\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 \u041D\u0415 \u0443\u0435\u0445\u0430\u043B (${commentWarning}) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0435\u0441\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u043D\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0435. \u041F\u0440\u043E\u0434\u0443\u0431\u043B\u0438\u0440\u0443\u0439 \u0435\u0433\u043E \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 PR: \`gh pr comment ${trustedPrUrl} --body "${verdictText} \u2026"\`.` : "";
10684
- return { content: [{ type: "text", text: `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0437\u0430\u043F\u0438\u0441\u0430\u043D, \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E${shotsNote(shots)}${commentNote}` }] };
11147
+ return { content: [{ type: "text", text: `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0437\u0430\u043F\u0438\u0441\u0430\u043D, \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E${shotsNote(shots)}${commentNote}${rulesNote}` }] };
10685
11148
  }
10686
11149
  if (iAmVerifier(run) && run?.verifies_run_id == null) {
10687
11150
  return {
@@ -10788,22 +11251,24 @@ ${report}${shots.warning}`,
10788
11251
  await store.updateRun(null, { ...t.patch, ...branchFix?.patch ?? {} });
10789
11252
  await store.addEvent(null, "pr_opened", pr_url);
10790
11253
  await postReport(reportSlug(run.task_slug), t.cycle ?? run.review_cycle ?? 0);
11254
+ const rulesNote = await offerRules(reportSlug(run.task_slug));
10791
11255
  if (t.isFixIteration) {
10792
11256
  await store.addEvent(
10793
11257
  null,
10794
11258
  "step",
10795
11259
  `\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u044F: \u043D\u043E\u0432\u044B\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u043F\u043E\u0441\u043B\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB \u2014 \u043E\u0442\u043A\u0440\u044B\u0442 \u0446\u0438\u043A\u043B \u0440\u0435-\u0440\u0435\u0432\u044C\u044E #${t.cycle}`
10796
11260
  );
10797
- return { content: [{ type: "text", text: `\u043F\u0440\u0438\u043D\u044F\u0442\u043E, \u0443\u0448\u043B\u043E \u043D\u0430 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E (\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u044F)${shotsNote(shots)}` }] };
11261
+ return { content: [{ type: "text", text: `\u043F\u0440\u0438\u043D\u044F\u0442\u043E, \u0443\u0448\u043B\u043E \u043D\u0430 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E (\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u044F)${shotsNote(shots)}${rulesNote}` }] };
10798
11262
  }
10799
- return { content: [{ type: "text", text: `\u043F\u0440\u0438\u043D\u044F\u0442\u043E, \u0443\u0448\u043B\u043E \u043D\u0430 \u0440\u0435\u0432\u044C\u044E${shotsNote(shots)}` }] };
11263
+ return { content: [{ type: "text", text: `\u043F\u0440\u0438\u043D\u044F\u0442\u043E, \u0443\u0448\u043B\u043E \u043D\u0430 \u0440\u0435\u0432\u044C\u044E${shotsNote(shots)}${rulesNote}` }] };
10800
11264
  }
10801
11265
  await store.updateRun(null, { status: "review", pr_url, ...branchFix?.patch ?? {} });
10802
11266
  await store.addEvent(null, "pr_opened", pr_url);
10803
11267
  const fallbackSlug = reportSlug(void 0);
10804
11268
  if (fallbackSlug) await postReport(fallbackSlug, void 0);
10805
11269
  else await store.addEvent(null, "step", `\u043E\u0442\u0447\u0451\u0442 \u0434\u0440\u043E\u043D\u0430 (\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430 \u2014 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435\u0442 \u0432 \u0441\u0442\u043E\u0440\u0435): ${report}`);
10806
- return { content: [{ type: "text", text: `\u043F\u0440\u0438\u043D\u044F\u0442\u043E, \u0443\u0448\u043B\u043E \u043D\u0430 \u0440\u0435\u0432\u044C\u044E${shotsNote(shots)}` }] };
11270
+ const fallbackRulesNote = await offerRules(fallbackSlug);
11271
+ return { content: [{ type: "text", text: `\u043F\u0440\u0438\u043D\u044F\u0442\u043E, \u0443\u0448\u043B\u043E \u043D\u0430 \u0440\u0435\u0432\u044C\u044E${shotsNote(shots)}${fallbackRulesNote}` }] };
10807
11272
  }
10808
11273
  await store.updateRun(null, { status: "done" });
10809
11274
  await store.addEvent(null, "done", pr_url ? `\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E \u0431\u0435\u0437 GitHub-PR (${pr_url})` : "\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E \u0431\u0435\u0437 GitHub-PR");
@@ -11204,7 +11669,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11204
11669
  };
11205
11670
  const closedBeforeStart = (run) => opts.daemonStartedAt != null && sqliteMs(run.updated_at) < opts.daemonStartedAt;
11206
11671
  const recentFailed = allRuns.filter(
11207
- (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r)
11672
+ (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r) && !isPreStartFailure(r)
11208
11673
  );
11209
11674
  if (recentFailed.length >= thresholds.failedSpikeCount) {
11210
11675
  candidates.push({
@@ -11263,6 +11728,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11263
11728
  if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
11264
11729
  if (killedByOperator(run)) continue;
11265
11730
  if (closedBeforeStart(run)) continue;
11731
+ if (isPreStartFailure(run)) continue;
11266
11732
  const repeat = isRepeatDown(run);
11267
11733
  candidates.push({
11268
11734
  key: `run_failed:${run.id}`,
@@ -11277,6 +11743,23 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11277
11743
  })
11278
11744
  });
11279
11745
  }
11746
+ for (const run of allRuns) {
11747
+ if (run.status !== "failed" || !isSpawnParked(run)) continue;
11748
+ if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
11749
+ if (closedBeforeStart(run)) continue;
11750
+ candidates.push({
11751
+ key: `spawn_parked:${run.id}`,
11752
+ minDurationMs: 0,
11753
+ // серия попыток УЖЕ прошла свой PRE_START_ATTEMPT_LIMIT — отлёживать нечего
11754
+ build: () => ({
11755
+ runId: run.id,
11756
+ level: "decision",
11757
+ 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"),
11758
+ body: `${run.task_slug}: ${run.stop_reason ?? SPAWN_PARK_MARK}`,
11759
+ ...hrefOf(run.task_url)
11760
+ })
11761
+ });
11762
+ }
11280
11763
  const queuedCount = allRuns.filter((r) => r.status === "queued").length;
11281
11764
  const activeDroneCount2 = allRuns.filter((r) => r.kind !== "hero" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
11282
11765
  if (queuedCount > 0 && activeDroneCount2 === 0) {
@@ -11294,6 +11777,31 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11294
11777
  })
11295
11778
  });
11296
11779
  }
11780
+ const GROUP_DEP_MISSING = {
11781
+ id: "dep_missing",
11782
+ 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`
11783
+ };
11784
+ for (const run of allRuns) {
11785
+ if (run.status !== "queued") continue;
11786
+ if (!run.depends_on || run.depends_on.length === 0) continue;
11787
+ const lost = db.depVerdicts(run.depends_on).filter((d) => d.outcome === "unknown");
11788
+ if (!lost.length) continue;
11789
+ candidates.push({
11790
+ key: `dep_missing:${run.task_slug}`,
11791
+ // по СЛАГУ, а не по id зеркала: тот меняется на рестарте (TAS-485)
11792
+ minDurationMs: thresholds.depMissingMs,
11793
+ group: GROUP_DEP_MISSING,
11794
+ build: () => ({
11795
+ runId: run.id,
11796
+ // Пуш: рой сам из этого не выйдет НИКОГДА, а лечение — одно действие человека.
11797
+ // Ровно тот случай, ради которого уровень decision и заведён.
11798
+ level: "decision",
11799
+ 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"),
11800
+ 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.`,
11801
+ ...hrefOf(run.task_url)
11802
+ })
11803
+ });
11804
+ }
11297
11805
  if (opts.gateReason && queuedCount > 0) {
11298
11806
  const reason = opts.gateReason;
11299
11807
  candidates.push({
@@ -11378,6 +11886,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11378
11886
  for (const q of db.openQuestions()) {
11379
11887
  const run = runsById.get(q.run_id);
11380
11888
  if (!run) continue;
11889
+ if (isTerminalStatus(run.status)) continue;
11381
11890
  candidates.push({
11382
11891
  key: `question_unanswered:${q.id}`,
11383
11892
  minDurationMs: thresholds.questionUnansweredMs,
@@ -11790,13 +12299,35 @@ function makeExclusiveReader(opts) {
11790
12299
  });
11791
12300
  }
11792
12301
  function makeAutoReviewReader(opts) {
11793
- return makeRepoValueReader({
11794
- fetch: opts.fetch,
11795
- fallback: opts.fallback,
11796
- valid: (v) => typeof v === "boolean",
11797
- ...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {},
11798
- ...opts.now !== void 0 ? { now: opts.now } : {}
11799
- });
12302
+ const ttl = opts.ttlMs ?? 6e4;
12303
+ const now = opts.now ?? Date.now;
12304
+ const log = opts.log ?? ((msg) => console.warn(msg));
12305
+ const cache = /* @__PURE__ */ new Map();
12306
+ const lastSource = /* @__PURE__ */ new Map();
12307
+ const say = (repo, source, msg) => {
12308
+ if (lastSource.get(repo) === source) return;
12309
+ lastSource.set(repo, source);
12310
+ log(msg);
12311
+ };
12312
+ return async (repo) => {
12313
+ const t = now();
12314
+ const hit = cache.get(repo);
12315
+ if (hit && t - hit.at < ttl) return hit.value;
12316
+ const live = await opts.fetch(repo).catch(() => null);
12317
+ if (typeof live === "boolean") {
12318
+ const changed = hit?.value !== live;
12319
+ cache.set(repo, { value: live, at: t });
12320
+ if (changed) lastSource.delete(repo);
12321
+ say(repo, "live", `[autoReview] ${repo}: \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u0430 \u2014 autoReview=${live}`);
12322
+ return live;
12323
+ }
12324
+ if (hit) {
12325
+ say(repo, "stale", `[autoReview] ${repo}: \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u043D\u0435 \u0432\u044B\u0448\u043B\u043E \u2014 \u0434\u0435\u0440\u0436\u0443 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u043E\u0435 autoReview=${hit.value}`);
12326
+ return hit.value;
12327
+ }
12328
+ say(repo, "env", `[autoReview] ${repo}: \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u0430 \u043D\u0438 \u0440\u0430\u0437\u0443 \u2014 \u0431\u0435\u0440\u0443 env SWARM_AUTO_REVIEW=${opts.fallback}`);
12329
+ return opts.fallback;
12330
+ };
11800
12331
  }
11801
12332
  function makeDroneCapReader(opts) {
11802
12333
  const read = makeRepoValueReader({
@@ -11860,22 +12391,22 @@ function makeMergeGateResolver(deps) {
11860
12391
  }
11861
12392
 
11862
12393
  // src/envGate.ts
11863
- import { existsSync as existsSync13 } from "fs";
11864
- import { join as join24 } from "path";
12394
+ import { existsSync as existsSync14 } from "fs";
12395
+ import { join as join25 } from "path";
11865
12396
  import { createConnection } from "net";
11866
12397
  import { execa as execa2 } from "execa";
11867
12398
 
11868
12399
  // src/platform.ts
11869
- import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
11870
- import { delimiter, join as join23 } from "path";
12400
+ import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
12401
+ import { delimiter, join as join24 } from "path";
11871
12402
  function resolveCommandPath(name, env = process.env, platform = process.platform) {
11872
- if (name.includes("/") || name.includes("\\")) return existsSync12(name) ? name : null;
12403
+ if (name.includes("/") || name.includes("\\")) return existsSync13(name) ? name : null;
11873
12404
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
11874
12405
  const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
11875
12406
  for (const dir of dirs) {
11876
12407
  for (const ext of exts) {
11877
- const candidate = join23(dir, name + ext);
11878
- if (existsSync12(candidate)) return candidate;
12408
+ const candidate = join24(dir, name + ext);
12409
+ if (existsSync13(candidate)) return candidate;
11879
12410
  }
11880
12411
  }
11881
12412
  return null;
@@ -11958,8 +12489,8 @@ var COMPOSE_MARKERS = [
11958
12489
  "compose.yaml"
11959
12490
  ];
11960
12491
  function detectRequirements(repo, io = {}) {
11961
- const exists = io.exists ?? existsSync13;
11962
- const marker = COMPOSE_MARKERS.find((f) => exists(join24(repo, f)));
12492
+ const exists = io.exists ?? existsSync14;
12493
+ const marker = COMPOSE_MARKERS.find((f) => exists(join25(repo, f)));
11963
12494
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
11964
12495
  }
11965
12496
  function resolveRequirements(repo, declared, io = {}) {
@@ -12095,8 +12626,8 @@ import { basename as basename3 } from "path";
12095
12626
  import { serve as serve2 } from "@hono/node-server";
12096
12627
 
12097
12628
  // src/diff.ts
12098
- import { statSync as statSync9 } from "fs";
12099
- import { join as join25 } from "path";
12629
+ import { statSync as statSync10 } from "fs";
12630
+ import { join as join26 } from "path";
12100
12631
  import { $ as $4 } from "execa";
12101
12632
  var MAX_FILES = 200;
12102
12633
  var MAX_LINES_PER_FILE = 1500;
@@ -12239,7 +12770,7 @@ async function resolveBase(wt, baseBranch) {
12239
12770
  }
12240
12771
  function mtime(path) {
12241
12772
  try {
12242
- return statSync9(path).mtimeMs;
12773
+ return statSync10(path).mtimeMs;
12243
12774
  } catch {
12244
12775
  return 0;
12245
12776
  }
@@ -12264,7 +12795,7 @@ async function collectRunDiff(worktree, opts = {}) {
12264
12795
  files.push(pf);
12265
12796
  }
12266
12797
  }
12267
- files.sort((a, b) => mtime(join25(worktree, b.path)) - mtime(join25(worktree, a.path)));
12798
+ files.sort((a, b) => mtime(join26(worktree, b.path)) - mtime(join26(worktree, a.path)));
12268
12799
  const truncatedFiles = files.length > MAX_FILES;
12269
12800
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
12270
12801
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -12297,7 +12828,7 @@ async function changedPaths(worktree, opts = {}) {
12297
12828
  // package.json
12298
12829
  var package_default = {
12299
12830
  name: "@taskless-app/swarm",
12300
- version: "0.6.14",
12831
+ version: "0.6.16",
12301
12832
  type: "module",
12302
12833
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
12303
12834
  keywords: [
@@ -12347,7 +12878,7 @@ var package_default = {
12347
12878
  execa: "^10.0.0",
12348
12879
  zod: "3.25.76"
12349
12880
  },
12350
- gitHead: "a3b11498dbed5db3642fe4fc83bcd2054efde055"
12881
+ gitHead: "785b1478f2b81406c9b6524913939707b6c096fb"
12351
12882
  };
12352
12883
 
12353
12884
  // src/version.ts
@@ -13980,10 +14511,10 @@ async function checkDaemonVersion(start, opts = {}) {
13980
14511
 
13981
14512
  // src/hostCommands.ts
13982
14513
  init_src();
13983
- import { readFileSync as readFileSync18, renameSync as renameSync8, writeFileSync as writeFileSync13 } from "fs";
13984
- import { join as join26 } from "path";
14514
+ import { readFileSync as readFileSync18, renameSync as renameSync9, writeFileSync as writeFileSync13 } from "fs";
14515
+ import { join as join27 } from "path";
13985
14516
  function hostCommandStatePath(home) {
13986
- return join26(home, "host-command.json");
14517
+ return join27(home, "host-command.json");
13987
14518
  }
13988
14519
  function readState(path) {
13989
14520
  try {
@@ -13995,7 +14526,7 @@ function readState(path) {
13995
14526
  function writeState(path, state) {
13996
14527
  try {
13997
14528
  writeFileSync13(`${path}.tmp`, JSON.stringify(state));
13998
- renameSync8(`${path}.tmp`, path);
14529
+ renameSync9(`${path}.tmp`, path);
13999
14530
  } catch {
14000
14531
  }
14001
14532
  }
@@ -14157,8 +14688,8 @@ async function doRestart(cmd, opts, note) {
14157
14688
 
14158
14689
  // src/selfUpdate.ts
14159
14690
  init_git();
14160
- import { existsSync as existsSync14 } from "fs";
14161
- import { join as join27 } from "path";
14691
+ import { existsSync as existsSync15 } from "fs";
14692
+ import { join as join28 } from "path";
14162
14693
  import { $ as $5 } from "execa";
14163
14694
  var DAEMON_PACKAGE = "@taskless-app/swarm";
14164
14695
  var defaultRun = async (cmd, args, cwd) => {
@@ -14171,7 +14702,7 @@ function tail(text, max = 400) {
14171
14702
  }
14172
14703
  async function runSelfUpdate(io) {
14173
14704
  const run = io.run ?? defaultRun;
14174
- const exists = io.exists ?? existsSync14;
14705
+ const exists = io.exists ?? existsSync15;
14175
14706
  const repo = io.repo;
14176
14707
  await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
14177
14708
  const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
@@ -14200,7 +14731,7 @@ async function runSelfUpdate(io) {
14200
14731
  };
14201
14732
  }
14202
14733
  await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
14203
- if (!exists(join27(repo, "pnpm-lock.yaml"))) {
14734
+ if (!exists(join28(repo, "pnpm-lock.yaml"))) {
14204
14735
  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" };
14205
14736
  }
14206
14737
  const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
@@ -14258,6 +14789,47 @@ function makeOvermindNarrator() {
14258
14789
  };
14259
14790
  }
14260
14791
 
14792
+ // src/orphanQuestions.ts
14793
+ init_src();
14794
+ var ORPHAN_NOTE_WINDOW_MS = DAY_MS;
14795
+ 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";
14796
+ function orphanWithdrawReason(status) {
14797
+ 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`;
14798
+ }
14799
+ function reapOrphanedQuestions(db, now, windowMs = ORPHAN_NOTE_WINDOW_MS) {
14800
+ const result = { withdrawn: 0, noted: 0 };
14801
+ let open;
14802
+ let runsById;
14803
+ try {
14804
+ open = db.openQuestions();
14805
+ if (open.length === 0) return result;
14806
+ runsById = new Map(db.listRuns().map((r) => [r.id, r]));
14807
+ } catch {
14808
+ return result;
14809
+ }
14810
+ for (const q of open) {
14811
+ const run = runsById.get(q.run_id);
14812
+ if (!run) continue;
14813
+ if (!isTerminalStatus(run.status) && run.superseded_by == null) continue;
14814
+ try {
14815
+ if (now - sqliteMs(run.updated_at) <= windowMs) {
14816
+ db.addEvent(run.id, NOTE_KIND, orphanNoteText(run, q), NOTE_ACTOR);
14817
+ result.noted++;
14818
+ }
14819
+ db.withdrawQuestion(q.id, orphanWithdrawReason(run.status));
14820
+ result.withdrawn++;
14821
+ } catch (e) {
14822
+ 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);
14823
+ }
14824
+ }
14825
+ return result;
14826
+ }
14827
+ var NOTE_KIND = "note";
14828
+ var NOTE_ACTOR = "orchestrator";
14829
+ function orphanNoteText(run, q) {
14830
+ 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`;
14831
+ }
14832
+
14261
14833
  // src/probe.ts
14262
14834
  init_executors();
14263
14835
  import { spawn as spawn2 } from "child_process";
@@ -14318,18 +14890,18 @@ async function fetchExternalStorm(url, opts = {}) {
14318
14890
  }
14319
14891
 
14320
14892
  // src/anomalyState.ts
14321
- import { existsSync as existsSync15, readFileSync as readFileSync19, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
14322
- import { join as join28 } from "path";
14893
+ import { existsSync as existsSync16, readFileSync as readFileSync19, renameSync as renameSync10, writeFileSync as writeFileSync14 } from "fs";
14894
+ import { join as join29 } from "path";
14323
14895
  var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
14324
14896
  var MAX_RESOLVED = 200;
14325
14897
  function emptyState() {
14326
14898
  return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
14327
14899
  }
14328
14900
  function anomalyStatePath(home) {
14329
- return join28(home, "anomaly-state.json");
14901
+ return join29(home, "anomaly-state.json");
14330
14902
  }
14331
14903
  function loadAnomalyState(home, now = Date.now(), io = {}) {
14332
- const exists = io.exists ?? existsSync15;
14904
+ const exists = io.exists ?? existsSync16;
14333
14905
  const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
14334
14906
  const path = anomalyStatePath(home);
14335
14907
  if (!exists(path)) return emptyState();
@@ -14361,7 +14933,7 @@ function saveAnomalyState(home, state, io = {}) {
14361
14933
  const path = anomalyStatePath(home);
14362
14934
  const writeFile3 = io.writeFile ?? ((p, data) => {
14363
14935
  writeFileSync14(`${p}.tmp`, data);
14364
- renameSync9(`${p}.tmp`, p);
14936
+ renameSync10(`${p}.tmp`, p);
14365
14937
  });
14366
14938
  writeFile3(path, JSON.stringify(state, null, 2));
14367
14939
  }
@@ -14687,6 +15259,9 @@ function short(files, n = 3) {
14687
15259
  return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
14688
15260
  }
14689
15261
  var SPINE_PREDICTION_WEIGHT = 0.7;
15262
+ function spineOfPrediction(spine, predicted) {
15263
+ return predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
15264
+ }
14690
15265
  function decideConflict({ model, candidate, active }) {
14691
15266
  if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
14692
15267
  if (!hasZoneModel(model)) return { kind: "unknown" };
@@ -14694,23 +15269,16 @@ function decideConflict({ model, candidate, active }) {
14694
15269
  if (!predicted.length) return { kind: "unknown" };
14695
15270
  if (!active.length) return { kind: "clear" };
14696
15271
  const spine = new Set(model.spine);
14697
- const candidateSpine = predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
14698
- if (candidateSpine.length) {
14699
- const blocker = active[0];
14700
- return {
14701
- kind: "wait",
14702
- blockedBy: blocker.run,
14703
- 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})`
14704
- };
14705
- }
14706
- for (const w of active) {
14707
- if (!w.actual) continue;
14708
- const theirSpine = w.files.filter((f) => spine.has(f));
14709
- if (theirSpine.length && zoneForFiles(model, w.files)?.zone === SPINE_ZONE) {
15272
+ const candidateSpine = new Set(spineOfPrediction(spine, predicted));
15273
+ if (candidateSpine.size) {
15274
+ for (const w of active) {
15275
+ const theirSpine = w.actual ? w.files.filter((f) => spine.has(f)) : spineOfPrediction(spine, predictForRun(model, w.run));
15276
+ const shared = theirSpine.filter((f) => candidateSpine.has(f));
15277
+ if (!shared.length) continue;
14710
15278
  return {
14711
15279
  kind: "wait",
14712
15280
  blockedBy: w.run,
14713
- 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`
15281
+ 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)")
14714
15282
  };
14715
15283
  }
14716
15284
  }
@@ -15194,6 +15762,7 @@ async function spawnVerifiersForOpenPrs(deps, stormActive) {
15194
15762
  }
15195
15763
  if (deps.resourcesOk && !deps.resourcesOk()) continue;
15196
15764
  if (deps.reviewCap !== void 0 && countActiveVerifiers(deps.db) >= deps.reviewCap) continue;
15765
+ if (!await projectWantsChecks(deps, run)) continue;
15197
15766
  const st = await deps.prStatus(run.pr_url).catch(() => null);
15198
15767
  if (prLiveness(st) !== "open") continue;
15199
15768
  if (st?.isDraft || st?.conflicting) continue;
@@ -15311,6 +15880,12 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
15311
15880
  "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.",
15312
15881
  "\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.",
15313
15882
  "\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.",
15883
+ // TASWM-431: половина правил репозитория в коде не написана, и выясняет их ДРОН —
15884
+ // а закрытая задача уносила это знание с собой. Планка намеренно высокая: «не
15885
+ // больше двух» и «обо что споткнулся» — единственное, что отделяет правило от
15886
+ // пересказа README, а лишнее правило дороже отсутствующего (его читает каждый
15887
+ // следующий дрон).
15888
+ `\u{1F4CC} \u0421\u041F\u041E\u0422\u041A\u041D\u0423\u041B\u0421\u042F \u041E \u041D\u0415\u0417\u0410\u041F\u0418\u0421\u0410\u041D\u041D\u041E\u0415 \u2014 \u041F\u0420\u0415\u0414\u041B\u041E\u0416\u0418 \u041F\u0420\u0410\u0412\u0418\u041B\u041E. \u041F\u043E\u0442\u0440\u0430\u0442\u0438\u043B \u0432\u0440\u0435\u043C\u044F \u043D\u0430 \u0442\u043E, \u0447\u0435\u0433\u043E \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0438\u0433\u0434\u0435 \u043D\u0435 \u043D\u0430\u043F\u0438\u0441\u0430\u043D\u043E (\u0438\u043D\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u043E\u043D\u043D\u044B\u0435 \u0442\u0435\u0441\u0442\u044B \u0436\u0434\u0443\u0442 \u043F\u043E\u0441\u0442\u0433\u0440\u0435\u0441 \u043D\u0430 5433; \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u043D\u0430\u043A\u0430\u0442\u044B\u0432\u0430\u044E\u0442\u0441\u044F \u043D\u0435 \u0442\u0435\u043C, \u0447\u0435\u043C \u043A\u0430\u0436\u0435\u0442\u0441\u044F; \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0437\u0435\u043B\u0451\u043D\u0430\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u0441 \u0442\u0430\u043A\u0438\u043C \u0444\u043B\u0430\u0433\u043E\u043C) \u2014 \u043F\u0435\u0440\u0435\u0434\u0430\u0439 \u044D\u0442\u043E \u0432 done: \`rules: [{ field: "verify"|"conventions"|"brief"|"env"|"zones"|"spine", value: <\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u043F\u043E\u043B\u044F \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438>, why: "\u043E\u0431\u043E \u0447\u0442\u043E \u0441\u043F\u043E\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u0438 \u0447\u0435\u0433\u043E \u044D\u0442\u043E \u0441\u0442\u043E\u0438\u043B\u043E" }]\`. \u041D\u0435 \u0431\u043E\u043B\u044C\u0448\u0435 ${DRONE_RULES_PER_RUN_MAX} \u0437\u0430 \u043F\u0440\u043E\u0433\u043E\u043D \u0438 \u0422\u041E\u041B\u042C\u041A\u041E \u0442\u043E, \u043E\u0431\u043E \u0447\u0442\u043E \u0440\u0435\u0430\u043B\u044C\u043D\u043E \u0441\u043F\u043E\u0442\u043A\u043D\u0443\u043B\u0441\u044F: \xAB\u0442\u0435\u0441\u0442\u044B \u0433\u043E\u043D\u044F\u044E\u0442\u0441\u044F \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439, \u043E\u043D\u0430 \u0432 README\xBB \u2014 \u043D\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u043E, \u0430 \u043F\u0435\u0440\u0435\u0441\u043A\u0430\u0437 README. \u0423\u0436\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043D\u043E\u0435 \u043E\u0442\u0441\u0435\u0435\u0442\u0441\u044F \u0441\u0430\u043C\u043E; \u0440\u0435\u0448\u0438\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u043E\u043C. \u041D\u0438\u0447\u0435\u0433\u043E \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435 \u0431\u044B\u043B\u043E \u2014 \u043D\u0435 \u0432\u044B\u0434\u0443\u043C\u044B\u0432\u0430\u0439, \u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u043F\u0438\u0441\u043E\u043A \u044D\u0442\u043E \u043D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0438\u0441\u0445\u043E\u0434 \u043F\u0440\u043E\u0433\u043E\u043D\u0430.`,
15314
15889
  "\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."
15315
15890
  ];
15316
15891
  if (run.decision) {
@@ -15322,7 +15897,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
15322
15897
  }
15323
15898
  return parts.join("\n");
15324
15899
  }
15325
- var HERO_CHARTER_PATH = join29(dirname4(THIS_FILE), "..", "docs", "HERO.md");
15900
+ var HERO_CHARTER_PATH = join30(dirname4(THIS_FILE), "..", "docs", "HERO.md");
15326
15901
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
15327
15902
  const name = run.name || `#${run.id}`;
15328
15903
  const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
@@ -15461,8 +16036,9 @@ var REVIEW_MISSION_BLOCK = [
15461
16036
  "\u2022 \u041D\u0430\u0445\u043E\u0434\u043A\u0430 \u0434\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u0421\u0426\u0415\u041D\u0410\u0420\u0418\u0415\u041C: \xAB\u0442\u0430\u043A\u043E\u0439 \u0432\u0445\u043E\u0434 \u0438\u043B\u0438 \u0442\u0430\u043A\u043E\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u2192 \u0432\u043E\u0442 \u0442\u0430\u043A\u043E\u0439 \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\xBB. \xAB\u0412\u044B\u0433\u043B\u044F\u0434\u0438\u0442 \u043D\u0435\u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\xBB, \xAB\u0441\u0442\u043E\u0438\u0442 \u043F\u0435\u0440\u0435\u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C\xBB, \xAB\u0436\u0435\u043B\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043F\u043E\u043A\u0440\u044B\u0442\u044C \u0442\u0435\u0441\u0442\u0430\u043C\u0438\xBB \u2014 \u043D\u0435 \u043D\u0430\u0445\u043E\u0434\u043A\u0438; \u0442\u0430\u043A\u043E\u0435 \u043D\u0435 \u043F\u0438\u0448\u0438.",
15462
16037
  "\u2022 \u041D\u043E\u043B\u044C \u043D\u0430\u0445\u043E\u0434\u043E\u043A \u2014 \u0437\u0430\u043A\u043E\u043D\u043D\u044B\u0439 \u0432\u0435\u0440\u0434\u0438\u043A\u0442, \u043D\u043E \u0442\u043E\u0433\u0434\u0430 \u043D\u0430\u0437\u043E\u0432\u0438, \u0427\u0422\u041E \u0438\u043C\u0435\u043D\u043D\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u043A\u0430\u043A\u0438\u043C \u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0435\u043C. APPROVE \u0431\u0435\u0437 \u044D\u0442\u043E\u0433\u043E \u0440\u043E\u0439 \u0447\u0438\u0442\u0430\u0435\u0442 \u043A\u0430\u043A \u043F\u0435\u0440\u0435\u0441\u043A\u0430\u0437, \u0438 \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442 PR \u0437\u0430\u043D\u043E\u0432\u043E \u0432\u0440\u0443\u0447\u043D\u0443\u044E."
15463
16038
  ].join("\n");
15464
- function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachmentsBlock, verifyMode = SWARM_VERIFY) {
16039
+ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachmentsBlock, verifyMode = SWARM_VERIFY, pendingRules = []) {
15465
16040
  const name = reviewer.name || `#${reviewer.id}`;
16041
+ const rulesBlock = renderPendingRulesBlock(pendingRules);
15466
16042
  const prUrl = reviewRun.pr_url ?? "(\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 PR \u2014 \u0441\u043C. \u043B\u043E\u0433 \u043F\u0440\u043E\u0433\u043E\u043D\u0430)";
15467
16043
  const classesStep = reviewRun.review_cycle > 0 ? "" : "\u043F\u0440\u043E\u0439\u0434\u0438 \u043A\u043B\u0430\u0441\u0441\u044B \u0434\u0435\u0444\u0435\u043A\u0442\u043E\u0432 (\u043F\u043E \u043A\u0430\u0436\u0434\u043E\u043C\u0443 \u2014 \u043E\u0442\u0432\u0435\u0442 \u0441 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439, \xAB\u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\xBB \u0442\u043E\u0436\u0435), ";
15468
16044
  return [
@@ -15504,6 +16080,7 @@ function buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachments
15504
16080
  `2. \u0427\u0435\u043A\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0438\u0441\u044C (\u0437\u0435\u043B\u0451\u043D\u044B\u0435 \u0438\u043B\u0438 \u0441 \u043A\u0440\u0430\u0441\u043D\u044B\u043C) \u0438\u043B\u0438 \u043D\u0430 \u044D\u0442\u043E\u043C \u043A\u043E\u043C\u043C\u0438\u0442\u0435 \u0438\u0445 \u0432\u043E\u0432\u0441\u0435 \u043D\u0435\u0442 \u2014 \u043E\u0441\u0442\u0430\u0432\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043D\u0430 \u0441\u0430\u043C PR: \`gh pr comment ${prUrl} --body "..."\` (\u041D\u0415 \`gh pr review\` \u2014 GitHub \u0435\u0433\u043E \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442 \u043D\u0430 \u0441\u0432\u043E\u0451\u043C PR). \u041D\u0430\u0447\u043D\u0438 \u0441 **APPROVE** \u0438\u043B\u0438 **REQUEST CHANGES** \u0437\u0430\u0433\u043B\u0430\u0432\u043D\u044B\u043C\u0438, \u0434\u0430\u043B\u044C\u0448\u0435 \u2014 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u0438\u043A\u0430: \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B, \u0447\u0442\u043E \u043D\u0430\u0448\u0451\u043B; \u0434\u043B\u044F REQUEST CHANGES \u2014 \u0442\u043E\u0447\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B/\u0441\u0442\u0440\u043E\u043A\u0438 \u0438 \u0447\u0442\u043E \u0447\u0438\u043D\u0438\u0442\u044C.`
15505
16081
  ],
15506
16082
  `3. \u0412\u044B\u0437\u043E\u0432\u0438 done(${prUrl}, verdict: "approve"|"changes", summary: "\u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u0447\u0442\u043E \u0440\u0435\u0448\u0438\u043B") \u2014 \u0440\u043E\u0439 \u0441\u0430\u043C \u0440\u0430\u0437\u043D\u0435\u0441\u0451\u0442 summary \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A, \u0430 verdict \u0437\u0430\u043F\u0438\u0448\u0435\u0442 \u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D (\u0431\u0435\u0439\u0434\u0436 \u0432 \u0432\u0435\u0431\u0435). \u0422\u044B \u0441\u0430\u043C \u0440\u0435\u0432\u044C\u044E \u041D\u0415 \u043F\u043E\u0434\u043B\u0435\u0436\u0438\u0448\u044C \u2014 \u0440\u0435\u043A\u0443\u0440\u0441\u0438\u0438 \u043D\u0435\u0442.`,
16083
+ ...rulesBlock ? ["", rulesBlock] : [],
15507
16084
  '\u0412\u0435\u0440\u0434\u0438\u043A\u0442 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D: \u0442\u0440\u0435\u0442\u044C\u0435\u0433\u043E \u0438\u0441\u0445\u043E\u0434\u0430 \u043D\u0435\u0442, \u0438 \u0443\u0441\u043F\u0435\u0445\u043E\u043C \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0435 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u0442\u0441\u044F (TASWM-375) \u2014 done() \u0431\u0435\u0437 verdict \u043E\u0442\u043A\u0430\u0436\u0435\u0442, \u0430 \u043F\u0440\u043E\u0433\u043E\u043D, \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u0432\u0448\u0438\u0439\u0441\u044F \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430, \u0440\u043E\u0439 \u043F\u043E\u043C\u0435\u0442\u0438\u0442 "\u0443\u043F\u0430\u043B" \u0438 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442 \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043D\u043E\u0432\u043E. \u041E\u0442\u043A\u0430\u0437\u0430\u043B done() \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u0441\u0451 \u0440\u0430\u0432\u043D\u043E \u043E\u0441\u0442\u0430\u0432\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 PR (\u0448\u0430\u0433 2): \u0434\u0435\u043C\u043E\u043D \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u0438\u0442 \u0435\u0433\u043E \u043E\u0442\u0442\u0443\u0434\u0430.'
15508
16085
  ].join("\n");
15509
16086
  }
@@ -15622,7 +16199,7 @@ async function wakeRun(deps, runId) {
15622
16199
  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` };
15623
16200
  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` };
15624
16201
  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` };
15625
- const worktreeExists = deps.worktreeExists ?? existsSync16;
16202
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
15626
16203
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
15627
16204
  const wt = run.worktree_path;
15628
16205
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -15707,9 +16284,9 @@ function repoHasCi(deps, run) {
15707
16284
  function repoHasCiWorkflows(worktreePath, repo) {
15708
16285
  for (const base3 of [worktreePath, repo]) {
15709
16286
  if (!base3) continue;
15710
- const dir = join29(base3, ".github", "workflows");
16287
+ const dir = join30(base3, ".github", "workflows");
15711
16288
  try {
15712
- if (readdirSync12(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
16289
+ if (readdirSync13(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
15713
16290
  } catch {
15714
16291
  }
15715
16292
  }
@@ -15818,7 +16395,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
15818
16395
  if (stats.count >= CI_WAKE_LIMIT || !canContinue) {
15819
16396
  const why = !canContinue ? "worktree/\u0441\u0435\u0441\u0441\u0438\u044F \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B \u2014 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E" : `${CI_WAKE_LIMIT} \u043F\u043E\u0431\u0443\u0434(\u043E\u043A) \u043F\u043E \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 CI \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E`;
15820
16397
  deps.db.updateRun(reviewer.id, { status: "stalled" });
15821
- const freshWanted = await projectWantsReviewer(deps, run);
16398
+ const freshWanted = await projectWantsChecks(deps, run);
15822
16399
  const freshPlan = freshWanted ? "\u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 PR" : "\u0441\u0432\u0435\u0436\u0435\u0433\u043E \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E: \u0443 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0430\u0432\u0442\u043E\u0440\u0435\u0432\u044C\u044E \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E";
15823
16400
  deps.db.addEvent(reviewer.id, "stalled", `CI \u0433\u043E\u0442\u043E\u0432, \u043D\u043E \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442\u044C \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u0432\u044B\u0448\u043B\u043E: ${why} \u2014 ${freshPlan}`);
15824
16401
  narrate(deps, `${droneLabel(reviewer)} \u0436\u0434\u0430\u043B CI, \u043D\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${why}) \u2014 ${freshPlan}.`);
@@ -15858,13 +16435,16 @@ async function wakeReviewerOnCiReady(deps, run, over) {
15858
16435
  return true;
15859
16436
  }
15860
16437
  function worktreeAndTranscriptAlive(deps, worktree) {
15861
- const exists = deps.worktreeExists ?? existsSync16;
16438
+ const exists = deps.worktreeExists ?? existsSync17;
15862
16439
  const transcript = deps.hasTranscript ?? executorHasSession;
15863
16440
  return exists(worktree) && transcript(worktree);
15864
16441
  }
15865
- async function projectWantsReviewer(deps, run) {
16442
+ async function projectWantsChecks(deps, run) {
15866
16443
  if (!deps.autoReviewForRepo) return true;
15867
- return await deps.autoReviewForRepo(effectiveRepo(deps, run)).catch(() => true);
16444
+ return await deps.autoReviewForRepo(effectiveRepo(deps, run)).catch((e) => {
16445
+ console.warn(`[overmind] autoReview \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (${effectiveRepo(deps, run)}) \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443 \u043A\u0430\u043A \u0440\u0430\u043D\u044C\u0448\u0435:`, e);
16446
+ return true;
16447
+ });
15868
16448
  }
15869
16449
  async function spawnFreshReviewerAfterStall(deps, run, why) {
15870
16450
  if (!deps.spawnReviewer) return;
@@ -16161,6 +16741,15 @@ async function trySpawnQueuedRun(deps, run) {
16161
16741
  // причина одна на машину — в лог уходит одной строкой, а не по строке на прогон
16162
16742
  });
16163
16743
  }
16744
+ const spentAttempts = preStartAttempts(deps.db.listRuns(), run.task_slug);
16745
+ if (spentAttempts >= PRE_START_ATTEMPT_LIMIT) {
16746
+ 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)`;
16747
+ deps.db.updateRun(run.id, { status: "stopped", stop_reason: why });
16748
+ deps.db.addEvent(run.id, "step", why);
16749
+ const d = noteDefer(deps.spawnDefer ??= /* @__PURE__ */ new Map(), `pre-start-exhausted:${run.task_slug}`, why, GATE_LOG_HEARTBEAT_TICKS);
16750
+ 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}`}`);
16751
+ return "closed";
16752
+ }
16164
16753
  const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
16165
16754
  if (mergedUrl) {
16166
16755
  await closeDuplicateTaskRun(deps, run, mergedUrl);
@@ -16171,7 +16760,11 @@ async function trySpawnQueuedRun(deps, run) {
16171
16760
  }
16172
16761
  const depVerdicts = deps.db.depVerdicts(run.depends_on);
16173
16762
  const unmet = depVerdicts.filter((d) => !d.met);
16174
- 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(", ")}`);
16763
+ if (unmet.length) {
16764
+ const lost = unmet.filter((d) => d.outcome === "unknown");
16765
+ 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(", ")}`;
16766
+ return deferSpawn(deps, run, why);
16767
+ }
16175
16768
  if (!await envGateAllows(deps, run)) return "skipped";
16176
16769
  if (!await projectQuotaGateAllows(deps, run)) return "skipped";
16177
16770
  const prodOverride = await prodOverrideReason(deps, run, ["queued"]);
@@ -16181,6 +16774,8 @@ async function trySpawnQueuedRun(deps, run) {
16181
16774
  }
16182
16775
  noteExhaustedDeps(deps, run, depVerdicts);
16183
16776
  await fillSuggestedArea(deps, run);
16777
+ const repoGap = unresolvedRepoReason(run.repo, deps.reposServed ? deps.reposServed() : []);
16778
+ if (repoGap) return await failBeforeStart(deps, run, repoGap);
16184
16779
  try {
16185
16780
  await deps.spawn(run);
16186
16781
  if (deps.spawnDefer) clearDefer(deps.spawnDefer, `run:${run.id}`);
@@ -16197,10 +16792,32 @@ async function trySpawnQueuedRun(deps, run) {
16197
16792
  return "skipped";
16198
16793
  }
16199
16794
  if (e instanceof AreaBusyError) return deferSpawn(deps, run, e.message);
16200
- deps.db.updateRun(run.id, { status: "failed" });
16201
- deps.db.addEvent(run.id, "failed", String(e));
16795
+ return await failBeforeStart(deps, run, String(e));
16796
+ }
16797
+ }
16798
+ async function failBeforeStart(deps, run, reason) {
16799
+ deps.db.updateRun(run.id, { status: "failed", stop_reason: `${PRE_START_MARK}: ${reason}` });
16800
+ deps.db.addEvent(run.id, "failed", `${PRE_START_MARK} \u2014 ${reason}`);
16801
+ const attempts = preStartAttempts(deps.db.listRuns(), run.task_slug);
16802
+ if (attempts < PRE_START_ATTEMPT_LIMIT) {
16803
+ narrate(deps, `${droneLabel(run)} \u2014 ${PRE_START_MARK} (\u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempts} \u0438\u0437 ${PRE_START_ATTEMPT_LIMIT}): ${reason}`);
16202
16804
  return "failed";
16203
16805
  }
16806
+ const parked = parkReason(attempts, reason);
16807
+ deps.db.updateRun(run.id, { stop_reason: parked });
16808
+ deps.db.addEvent(run.id, "step", parked);
16809
+ narrate(deps, `${run.task_slug}: ${parked}`);
16810
+ if (deps.parkTask) {
16811
+ const ok = await deps.parkTask(run, parked).catch(() => false);
16812
+ if (!ok) {
16813
+ deps.db.addEvent(
16814
+ run.id,
16815
+ "step",
16816
+ `${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`
16817
+ );
16818
+ }
16819
+ }
16820
+ return "failed";
16204
16821
  }
16205
16822
  async function batchSpawnQueue(deps, droneCap) {
16206
16823
  for (const run of orderQueue(deps.db.listRuns("queued"))) {
@@ -16222,7 +16839,7 @@ async function resumeWatchdogParked(deps, droneCap) {
16222
16839
  }
16223
16840
  if (!candidate) return;
16224
16841
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
16225
- const worktreeExists = deps.worktreeExists ?? existsSync16;
16842
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
16226
16843
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
16227
16844
  const wt = candidate.worktree_path;
16228
16845
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -16391,7 +17008,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
16391
17008
  if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
16392
17009
  if (!deps.respawn) return;
16393
17010
  const wt = resumeCandidate.worktree_path;
16394
- const worktreeExists = deps.worktreeExists ?? existsSync16;
17011
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
16395
17012
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
16396
17013
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
16397
17014
  const mode = canContinue ? "continue" : "fresh";
@@ -16660,7 +17277,7 @@ function buildEscalationCandidates(db) {
16660
17277
  }
16661
17278
  async function tickOnce(deps) {
16662
17279
  const now = deps.now ? deps.now() : Date.now();
16663
- const worktreeExists = deps.worktreeExists ?? existsSync16;
17280
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
16664
17281
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
16665
17282
  deps.sampleHost?.();
16666
17283
  if (deps.rehydrate) {
@@ -16685,14 +17302,6 @@ async function tickOnce(deps) {
16685
17302
  }
16686
17303
  }
16687
17304
  }
16688
- if (deps.heartbeat) {
16689
- const hb = deps.heartbeat;
16690
- const sinceLast = hb.lastAt == null ? Infinity : now - hb.lastAt;
16691
- if (sinceLast >= hb.intervalMs) {
16692
- hb.lastAt = now;
16693
- await hb.push();
16694
- }
16695
- }
16696
17305
  if (deps.droneBridge) {
16697
17306
  const br = deps.droneBridge;
16698
17307
  const sinceLast = br.lastAt == null ? Infinity : now - br.lastAt;
@@ -16913,7 +17522,7 @@ async function tickOnce(deps) {
16913
17522
  // не порядком веток в 900-строчной функции: увидели живого — значит закрывать его
16914
17523
  // рано, спавнить второго нельзя.
16915
17524
  liveOrWaitingReviewers(deps.db, run.id).length === 0;
16916
- if (reviewerGatesOk && deps.spawnReviewer && await projectWantsReviewer(deps, run)) {
17525
+ if (reviewerGatesOk && deps.spawnReviewer && await projectWantsChecks(deps, run)) {
16917
17526
  if (run.review_cycle >= REVIEW_CYCLE_LIMIT) {
16918
17527
  if (!deps.db.events(run.id).some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_CYCLE_ESCALATION_MARK))) {
16919
17528
  deps.db.addEvent(run.id, "step", `${REVIEW_CYCLE_ESCALATION_MARK}: ${run.review_cycle} \u0446\u0438\u043A\u043B(\u043E\u0432) \u0440\u0435\u0432\u044C\u044E-\u0444\u0438\u043A\u0441\u043E\u0432 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
@@ -16991,6 +17600,13 @@ async function tickOnce(deps) {
16991
17600
  liveChildPpids = null;
16992
17601
  logTransientSpawnError("ps (livePpids)", e);
16993
17602
  }
17603
+ if (deps.sweepCost) {
17604
+ try {
17605
+ deps.sweepCost();
17606
+ } catch (e) {
17607
+ 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);
17608
+ }
17609
+ }
16994
17610
  for (const run of [...deps.db.listRuns("running"), ...deps.db.listRuns("blocked")]) {
16995
17611
  const alive = run.pid != null && deps.isAlive(run.pid);
16996
17612
  if (!alive && deps.db.takeRun && !await deps.db.takeRun(run.id)) continue;
@@ -17216,6 +17832,16 @@ async function tickOnce(deps) {
17216
17832
  if (deps.respawn && deps.resourcesOk && !stormActive) {
17217
17833
  await resurrectStormDead(deps, worktreeExists, hasTranscript, { gateOpen: deps.resourcesOk(), cap: droneCap });
17218
17834
  }
17835
+ try {
17836
+ const reaped = reapOrphanedQuestions(deps.db, now);
17837
+ if (reaped.withdrawn > 0) {
17838
+ console.log(
17839
+ `[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}`
17840
+ );
17841
+ }
17842
+ } catch (e) {
17843
+ 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);
17844
+ }
17219
17845
  if (deps.anomalies && !deps.control?.paused) {
17220
17846
  const az = deps.anomalies;
17221
17847
  let ready = [];
@@ -17224,7 +17850,7 @@ async function tickOnce(deps) {
17224
17850
  if (deps.autoReviewForRepo && az.autoReviewEnabled) {
17225
17851
  for (const r of deps.db.listRuns("review")) {
17226
17852
  if (!r.pr_url || autoReviewSnapshot.has(r.repo)) continue;
17227
- autoReviewSnapshot.set(r.repo, await deps.autoReviewForRepo(effectiveRepo(deps, r)).catch(() => true));
17853
+ autoReviewSnapshot.set(r.repo, await projectWantsChecks(deps, r));
17228
17854
  }
17229
17855
  }
17230
17856
  const candidates = [
@@ -17367,7 +17993,7 @@ async function resolveConflict(deps, reviewRun) {
17367
17993
  model: reviewRun.model,
17368
17994
  review_model: reviewRun.review_model
17369
17995
  });
17370
- const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
17996
+ const wt = join30(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
17371
17997
  try {
17372
17998
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
17373
17999
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -17424,12 +18050,13 @@ async function spawnReviewer(deps, reviewRun, files) {
17424
18050
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
17425
18051
  workspace_id: reviewRun.workspace_id
17426
18052
  });
17427
- const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
18053
+ const wt = join30(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
17428
18054
  try {
17429
18055
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
17430
18056
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
17431
18057
  const attachments = await deps.taskAttachments?.(reviewRun.task_slug, wt, reviewRun.workspace_id);
17432
- const prompt = buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachments, verifyMode);
18058
+ const pendingRules = await deps.pendingRepoRules?.(repo, reviewRun.task_slug).catch(() => []) ?? [];
18059
+ const prompt = buildReviewerPrompt(reviewRun, reviewer, checklist, branch, attachments, verifyMode, pendingRules);
17433
18060
  const pid = deps.launch({ worktree: wt, run: reviewer, prompt });
17434
18061
  deps.db.updateRun(reviewer.id, { status: "running", worktree_path: wt, branch, pid, pr_url: reviewRun.pr_url });
17435
18062
  deps.db.addEvent(reviewRun.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E: \u043F\u043E\u0434\u043D\u044F\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${reviewer.name} (#${reviewer.id})`);
@@ -17499,7 +18126,7 @@ async function spawnVerifier(deps, verifiedRun, files) {
17499
18126
  // штамп цикла: фикс-итерацию проверяет свежий верификатор
17500
18127
  workspace_id: verifiedRun.workspace_id
17501
18128
  });
17502
- const wt = join29(deps.worktreesDir, basename4(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
18129
+ const wt = join30(deps.worktreesDir, basename4(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
17503
18130
  try {
17504
18131
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
17505
18132
  await deps.git.setWorktreeIdentity(wt, verifier.name);
@@ -17537,7 +18164,7 @@ async function respawnRun(deps, run, mode, nudge) {
17537
18164
  ]);
17538
18165
  const base3 = effectiveBaseBranch(run.base_branch, projectBase);
17539
18166
  const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
17540
- if (!existsSync16(wt)) {
18167
+ if (!existsSync17(wt)) {
17541
18168
  const branch = runToSpawn.branch || runToSpawn.task_slug;
17542
18169
  const source = await addWorktree(repo, wt, branch, {
17543
18170
  ...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
@@ -17564,7 +18191,7 @@ async function respawnRun(deps, run, mode, nudge) {
17564
18191
  deps.db.updateRun(run.id, { status: "running", pid });
17565
18192
  }
17566
18193
  async function syncCrashedOnStartup(deps) {
17567
- const worktreeExists = deps.worktreeExists ?? existsSync16;
18194
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
17568
18195
  let orphaned = 0;
17569
18196
  let merged = 0;
17570
18197
  let requeued = 0;
@@ -17653,10 +18280,12 @@ async function makeStore(repo, worktreesDir, repoFlag) {
17653
18280
  roots: scanRootsList,
17654
18281
  maxDepth: SWARM_SCAN_DEPTH,
17655
18282
  cachePath: REPO_MAP_PATH,
18283
+ // TASWM-418: кэш обязан протухать — иначе новый клон на машине не находится никогда.
18284
+ scanTtlMs: SWARM_REPO_SCAN_TTL_MS,
17656
18285
  state: repoMap,
17657
18286
  log: (m) => console.log(m)
17658
18287
  });
17659
- const repoRegistry = await rebuildRegistry();
18288
+ let repoRegistry = await rebuildRegistry();
17660
18289
  if (repoRegistry.length === 0) {
17661
18290
  console.warn(
17662
18291
  `[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: почему пусто — три РАЗНЫЕ причины, и лечатся они по-разному. Одна
@@ -17686,8 +18315,13 @@ async function makeStore(repo, worktreesDir, repoFlag) {
17686
18315
  // Периодическая пересборка (шаг 0a4 тика): применяем новый реестр к ЖИВОМУ клиенту —
17687
18316
  // и список для claim/heartbeat, и обратную карту owner/repo → путь.
17688
18317
  refreshRepoMap: async () => {
17689
- client.setRepoRegistry(await rebuildRegistry());
17690
- }
18318
+ repoRegistry = await rebuildRegistry();
18319
+ client.setRepoRegistry(repoRegistry);
18320
+ },
18321
+ // TASWM-422: что машина обслуживает ПРЯМО СЕЙЧАС — читается из ЖИВОГО реестра (он
18322
+ // пересобирается тиком выше), чтобы причина «клона нет на этой машине» называла
18323
+ // актуальный список, а не снимок на старте демона.
18324
+ reposServed: () => repoRegistry.map((e) => e.repoFullName)
17691
18325
  };
17692
18326
  }
17693
18327
  var crashGuardsInstalled = false;
@@ -17707,22 +18341,22 @@ async function migrateWorktreesAtStartup() {
17707
18341
  legacyDir: WORKTREES_LEGACY_DIR,
17708
18342
  targetDir: WORKTREES_DIR,
17709
18343
  envOverride: process.env.SWARM_WORKTREES || "",
17710
- exists: (d) => existsSync16(d),
18344
+ exists: (d) => existsSync17(d),
17711
18345
  isNonEmptyDir: (d) => {
17712
18346
  try {
17713
- return readdirSync12(d).length > 0;
18347
+ return readdirSync13(d).length > 0;
17714
18348
  } catch {
17715
18349
  return false;
17716
18350
  }
17717
18351
  },
17718
18352
  liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
17719
- rename: (from, to) => renameSync10(from, to),
18353
+ rename: (from, to) => renameSync11(from, to),
17720
18354
  repairMoved: (t) => repairMovedWorktrees(t),
17721
18355
  log: (m) => console.log(m)
17722
18356
  });
17723
18357
  } catch (e) {
17724
18358
  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);
17725
- return existsSync16(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
18359
+ return existsSync17(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
17726
18360
  }
17727
18361
  }
17728
18362
  async function reportWorkspaceAccess(tl, db) {
@@ -17742,6 +18376,14 @@ async function runDaemon(opts) {
17742
18376
  const daemonStart = captureDaemonStart();
17743
18377
  const latestRelease = fetchLatestVersion();
17744
18378
  const worktreesDir = await migrateWorktreesAtStartup();
18379
+ {
18380
+ const swept = sweepLegacyRunLogs(RUN_LOGS_DIR);
18381
+ if (swept.moved || swept.failed) {
18382
+ console.log(
18383
+ `[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}` : "")
18384
+ );
18385
+ }
18386
+ }
17745
18387
  if (!opts.repo) {
17746
18388
  const remembered = effectiveRepoPaths("", loadRememberedRepos(SWARM_HOME))[0];
17747
18389
  if (remembered) opts.repo = remembered;
@@ -17752,7 +18394,8 @@ async function runDaemon(opts) {
17752
18394
  host,
17753
18395
  repoMap: repoMapState,
17754
18396
  refreshRepoMap: rebuildRepoMap,
17755
- prodMcp
18397
+ prodMcp,
18398
+ reposServed
17756
18399
  } = await makeStore(opts.repo, worktreesDir, opts.repoFlag);
17757
18400
  await preflightAuth();
17758
18401
  const outdated = outdatedLine(PACKAGE_VERSION, await latestRelease);
@@ -17798,6 +18441,8 @@ async function runDaemon(opts) {
17798
18441
  overmindEntry: THIS_FILE,
17799
18442
  dbPath: DB_PATH,
17800
18443
  runLogsDir: RUN_LOGS_DIR,
18444
+ // TASWM-419: чем именуется каталог лога прогона — стабильные ключи из стора, не номер.
18445
+ runLogKeys: (run) => db.runLogKeys(run.id),
17801
18446
  // Кем исполнять (TASWM-349) — настройка SWARM_EXECUTOR, один резолв на весь демон.
17802
18447
  executor: EXECUTOR,
17803
18448
  droneIsolation: DRONE_ISOLATION,
@@ -17852,6 +18497,8 @@ async function runDaemon(opts) {
17852
18497
  }
17853
18498
  let onHostCommands = () => {
17854
18499
  };
18500
+ const tickIntervalMs = opts.intervalMs ?? 4e3;
18501
+ const tickHealth = initialTickHealth(Date.now());
17855
18502
  let heartbeat;
17856
18503
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_PROD_HEARTBEAT_MS > 0) {
17857
18504
  const client = db;
@@ -17865,6 +18512,7 @@ async function runDaemon(opts) {
17865
18512
  lastOwnCapacity,
17866
18513
  hostResourcesForProd(),
17867
18514
  { 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 },
18515
+ worktreesDir,
17868
18516
  // TASWM-393: ВХОД в Taskless — та же природа, что пауза и шторм: состояние
17869
18517
  // машины, о котором знает только она. Пульс идёт и без входа, поэтому «на
17870
18518
  // связи» без этого поля читалось как «возьмёт работу» (12.08 — шесть прогонов
@@ -17874,7 +18522,12 @@ async function runDaemon(opts) {
17874
18522
  // отвергают», а 12.08 входа не делали вовсе, и по отказу машина числилась бы
17875
18523
  // вошедшей. Адрес — WEB_PORT (он же SWARM_WEB_PORT из окружения, на нём и
17876
18524
  // поднимается морда): зашитый 7420 отправил бы человека туда, где ничего нет.
17877
- { signedIn: signedInToTaskless(), webUrl: `http://127.0.0.1:${WEB_PORT}` }
18525
+ { signedIn: signedInToTaskless(), webUrl: `http://127.0.0.1:${WEB_PORT}` },
18526
+ // TASWM-452: и ПРИЗНАК ЖИВОГО ТИКА — номер последнего завершённого прохода
18527
+ // реконсайла и его возраст. Возраст, а не момент: часы машины и прода
18528
+ // расходятся (ноутбук после сна — на минуты), а сверяет признак прод со своим
18529
+ // настенным временем. Не «демон запущен», а «цикл прокрутился».
18530
+ tickPulse(tickHealth, Date.now(), tickIntervalMs)
17878
18531
  );
17879
18532
  if (reply.maxDrones !== void 0) operatorMaxDrones = reply.maxDrones;
17880
18533
  if (reply.commands.length) onHostCommands(reply.commands);
@@ -17883,7 +18536,13 @@ async function runDaemon(opts) {
17883
18536
  {
17884
18537
  runLogsDir: RUN_LOGS_DIR,
17885
18538
  daemonLogPath: DAEMON_LOG_PATH,
17886
- localRunId: (prodRunId) => client.localRunIdForProd(prodRunId),
18539
+ // TASWM-419: каталог лога адресуется стабильными ключами прогона, а не его
18540
+ // локальным номером — номера переиспользуются между жизнями зеркала, и
18541
+ // оператору приезжал хвост чужого прогона.
18542
+ runLogKeys: (prodRunId) => {
18543
+ const local = client.localRunIdForProd(prodRunId);
18544
+ return local == null ? [] : db.runLogKeys(local);
18545
+ },
17887
18546
  deliver: (requestId, payload) => client.deliverLog(requestId, payload)
17888
18547
  },
17889
18548
  reply.logRequests
@@ -17941,6 +18600,7 @@ async function runDaemon(opts) {
17941
18600
  questionUnansweredMs: SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
17942
18601
  outboxStuckMs: SWARM_ANOMALY_OUTBOX_STUCK_MS,
17943
18602
  resourceGateStallMs: SWARM_ANOMALY_RESOURCE_GATE_STALL_MS,
18603
+ depMissingMs: SWARM_ANOMALY_DEP_MISSING_MS,
17944
18604
  runDownWindowMs: SWARM_ANOMALY_RUN_DOWN_WINDOW_MS
17945
18605
  },
17946
18606
  autoReviewEnabled: SWARM_AUTO_REVIEW,
@@ -17992,8 +18652,13 @@ async function runDaemon(opts) {
17992
18652
  let exclusiveProject;
17993
18653
  let projectDroneCap;
17994
18654
  let autoReviewProject;
18655
+ let pendingRepoRules;
17995
18656
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
17996
18657
  const client = db;
18658
+ pendingRepoRules = async (repo, taskSlug) => {
18659
+ const repoFullName = await remoteRepoFullName(repo).catch(() => null);
18660
+ return repoFullName ? client.repoPendingRules(repoFullName, taskSlug) : [];
18661
+ };
17997
18662
  verifyModeReader = makeVerifyModeReader({
17998
18663
  fetch: async (repo) => {
17999
18664
  const repoFullName = await remoteRepoFullName(repo).catch(() => null);
@@ -18065,6 +18730,9 @@ async function runDaemon(opts) {
18065
18730
  // иначе ревьюер судит проект с verifyMode=local по правилам ci и блокирует PR
18066
18731
  // за отсутствие CI, которого у проекта нет и не предполагается.
18067
18732
  ...verifyModeReader ? { verifyMode: verifyModeReader } : {},
18733
+ // TASWM-431: правила о репозитории, принесённые дроном этого прогона, — ревьюер
18734
+ // решает их тем же вердиктом (см. buildReviewerPrompt).
18735
+ ...pendingRepoRules ? { pendingRepoRules } : {},
18068
18736
  // TASWM-283: те же вложения задачи, что у исполнителя — ревьюер без макета
18069
18737
  // не может проверить «сделано ли как просили».
18070
18738
  ...tl?.fetchFile ? {
@@ -18200,6 +18868,30 @@ async function runDaemon(opts) {
18200
18868
  taskClosed: (slug, ws) => tl.forWorkspace(ws).getTask(slug).then((t) => t ? isClosingTaskCategory(t.statusCategory) : null)
18201
18869
  } : {},
18202
18870
  finishTask: (slug, ws) => tlFor(ws)?.finishTask(slug) ?? Promise.resolve(),
18871
+ ...reposServed ? { reposServed } : {},
18872
+ // TASWM-422: ОТЛОЖИТЬ задачу, по которой рой трижды не сумел поднять дрона. Механика —
18873
+ // ровно та же, что у return_task дрона (TASWM-412): `claimableTaskWhere` (api) требует
18874
+ // категорию unstarted, и start_task её ломает — задача перестаёт быть claimable, то есть
18875
+ // авто-интейк больше не берёт её раз в минуту. Успех проверяем ФАКТОМ (перечитываем
18876
+ // задачу), а не кодом возврата: клиент глушит отказ сервера внутрь (safeCall), и по
18877
+ // возврату «отложил» было бы неотличимо от «сказал, но не доехало» — то самое молчание,
18878
+ // из-за которого 17.08 круг и крутился.
18879
+ parkTask: async (run, reason) => {
18880
+ const client = tlFor(run.workspace_id);
18881
+ if (!client) return false;
18882
+ await client.addComment(
18883
+ run.task_slug,
18884
+ `${reason}
18885
+
18886
+ \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.`,
18887
+ { swarmKind: "report", swarmDroneName: run.name }
18888
+ ).catch(() => {
18889
+ });
18890
+ await client.startTask(run.task_slug).catch(() => {
18891
+ });
18892
+ const after = await client.getTask(run.task_slug).catch(() => null);
18893
+ return after != null && after.statusCategory !== "unstarted";
18894
+ },
18203
18895
  // TASWM-272: отчёт/вердикт-компенсация при усыновлении мёртвого дрона — реальный
18204
18896
  // TasklessClient.addComment + gh/git хелперы. Без tl (нет токена) ключи не задаём —
18205
18897
  // postAdoptionReport/postScrapedVerdictReport становятся no-op, как раньше.
@@ -18221,9 +18913,12 @@ async function runDaemon(opts) {
18221
18913
  // TASWM-364: swarmModels — ТОТ ЖЕ снимок, что уехал в launcher: цена токенов считается
18222
18914
  // по модели, на которой прогон реально работал.
18223
18915
  accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR, swarmModels: () => lastSwarmModels }),
18916
+ // TASWM-413: тот же аккаунтант, но поводом служит завершённый ЗАПУСК, а не смерть
18917
+ // прогона в running — иначе сданный через done() прогон не получает цену никогда.
18918
+ sweepCost: makeCostSweeper({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR, swarmModels: () => lastSwarmModels, isAlive: pidAlive }),
18224
18919
  // Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
18225
18920
  // (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
18226
- exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
18921
+ exitOutcome: makeExitClassifier(RUN_LOGS_DIR, (runId) => db.runLogKeys(runId), EXECUTOR),
18227
18922
  // Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
18228
18923
  livePpids,
18229
18924
  // Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
@@ -18370,7 +19065,7 @@ async function runDaemon(opts) {
18370
19065
  mode,
18371
19066
  nudge
18372
19067
  ),
18373
- worktreeExists: (p) => existsSync16(p),
19068
+ worktreeExists: (p) => existsSync17(p),
18374
19069
  hasTranscript: executorHasSession,
18375
19070
  // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
18376
19071
  // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
@@ -18394,8 +19089,6 @@ async function runDaemon(opts) {
18394
19089
  // Периодический re-hydrate зеркала (client-режим): ключ есть только когда сконфигурен.
18395
19090
  ...rehydrate ? { rehydrate } : {},
18396
19091
  ...droneBridge ? { droneBridge } : {},
18397
- // Пульс overmind в прод (client-режим, TASWM-129): ключ есть только когда сконфигурен.
18398
- ...heartbeat ? { heartbeat } : {},
18399
19092
  // Сверка кода демона с main (TASWM-251): ключ есть только когда entryFile в git-репо.
18400
19093
  ...daemonVersion ? { daemonVersion } : {},
18401
19094
  // Карта репозиториев машины (TASWM-336): ключ есть только в client-режиме с непустым TTL.
@@ -18735,8 +19428,30 @@ async function runDaemon(opts) {
18735
19428
  console.error("[overmind] \u0430\u0432\u0442\u043E\u043D\u043E\u043C\u043D\u044B\u0439 \u0440\u0435\u0436\u0438\u043C \u041D\u0415 \u0432\u044B\u0431\u0440\u0430\u043D \u2014 \u043E\u043D \u043D\u0430\u0432\u044F\u0437\u0430\u043D \u043E\u0442\u043A\u0430\u0437\u043E\u043C: \u0440\u043E\u0439 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442 \u0440\u0430\u0431\u043E\u0442\u0443, \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u043A\u043E\u043F\u044F\u0442\u0441\u044F \u0432 outbox.");
18736
19429
  }
18737
19430
  console.log(`\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430: http://127.0.0.1:${web.port} \u2190 \u043E\u0442\u043A\u0440\u043E\u0439 \u0438 \u0432\u043E\u0439\u0434\u0438 \u0447\u0435\u0440\u0435\u0437 Taskless`);
18738
- const interval = opts.intervalMs ?? 4e3;
19431
+ const interval = tickIntervalMs;
18739
19432
  writeHeartbeat(HEARTBEAT_PATH, Date.now());
19433
+ if (heartbeat) {
19434
+ const hb = heartbeat;
19435
+ let inFlight = false;
19436
+ setInterval(() => {
19437
+ if (inFlight) return;
19438
+ inFlight = true;
19439
+ void hb.push().catch((e) => console.error("[overmind] \u043F\u0443\u043B\u044C\u0441 \u0432 \u043F\u0440\u043E\u0434 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e)).finally(() => {
19440
+ inFlight = false;
19441
+ });
19442
+ }, hb.intervalMs);
19443
+ }
19444
+ setInterval(() => {
19445
+ if (restart.phase === "restarting") return;
19446
+ const now = Date.now();
19447
+ if (!tickSelfHealDue(tickHealth, now, interval)) return;
19448
+ const age = Math.round(tickAgeMs(tickHealth, now) / 1e3);
19449
+ console.error(
19450
+ `[overmind] \u0442\u0438\u043A \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B\u0430 \u0432\u0441\u0442\u0430\u043B: \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D\u043D\u044B\u0439 \u043F\u0440\u043E\u0445\u043E\u0434 #${tickHealth.seq} \u0431\u044B\u043B ${age}\u0441 \u043D\u0430\u0437\u0430\u0434${tickHealth.lastError ? ` (\u043F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u043E\u0448\u0438\u0431\u043A\u0430: ${tickHealth.lastError})` : ""} \u2014 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u044E \u0441\u0435\u0431\u044F`
19451
+ );
19452
+ narrator.narrate(`\u{1FA7A} \u0426\u0438\u043A\u043B \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B\u0430 \u0432\u0441\u0442\u0430\u043B (\u043F\u0440\u043E\u0445\u043E\u0434 #${tickHealth.seq}, ${age}\u0441 \u0431\u0435\u0437 \u0434\u0432\u0438\u0436\u0435\u043D\u0438\u044F) \u2014 \u0434\u0435\u043C\u043E\u043D \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0435\u0442 \u0441\u0435\u0431\u044F \u0441\u0430\u043C.`);
19453
+ performRestart(liveSessionIds(db, pidAlive));
19454
+ }, Math.max(1e3, interval));
18740
19455
  if (deps.sweep) {
18741
19456
  deps.sweep.lastAt = Date.now();
18742
19457
  await deps.sweep.run().catch((e) => console.error("[overmind] \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 sweep \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e));
@@ -18745,7 +19460,9 @@ async function runDaemon(opts) {
18745
19460
  deps.daemonVersion.lastAt = Date.now();
18746
19461
  await deps.daemonVersion.run().catch((e) => console.error("[overmind] \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u0430\u044F \u0441\u0432\u0435\u0440\u043A\u0430 \u0432\u0435\u0440\u0441\u0438\u0438 \u0434\u0435\u043C\u043E\u043D\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e));
18747
19462
  }
19463
+ tickHealth.startedAt = Date.now();
18748
19464
  for (; ; ) {
19465
+ let tickError;
18749
19466
  try {
18750
19467
  writeHeartbeat(HEARTBEAT_PATH, Date.now());
18751
19468
  await tickOnce(deps);
@@ -18755,8 +19472,10 @@ async function runDaemon(opts) {
18755
19472
  else onRestartStillWaiting?.(live.length);
18756
19473
  }
18757
19474
  } catch (e) {
18758
- console.error("[overmind] tick error:", e);
19475
+ tickError = e;
19476
+ console.error(`[overmind] \u0442\u0438\u043A \u0443\u043F\u0430\u043B: ${describeTickError(e)} \u2014 \u0446\u0438\u043A\u043B \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0442 \u0440\u0430\u0431\u043E\u0442\u0443`, e);
18759
19477
  }
19478
+ markTickDone(tickHealth, Date.now(), tickError);
18760
19479
  await new Promise((r) => setTimeout(r, interval));
18761
19480
  }
18762
19481
  }