@taskless-app/swarm 0.6.31 → 0.6.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +82 -17
  2. package/dist/overmind.js +1082 -505
  3. package/package.json +2 -2
package/dist/overmind.js CHANGED
@@ -1447,7 +1447,7 @@ function buildZoneModel(entries, opts = {}) {
1447
1447
  if (!m) labelVotes.set(z2, m = /* @__PURE__ */ new Map());
1448
1448
  bump(m, manual);
1449
1449
  }
1450
- const rename = /* @__PURE__ */ new Map();
1450
+ const rename2 = /* @__PURE__ */ new Map();
1451
1451
  const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
1452
1452
  const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
1453
1453
  const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
@@ -1456,11 +1456,11 @@ function buildZoneModel(entries, opts = {}) {
1456
1456
  for (const [zone, votes] of clustersByEvidence) {
1457
1457
  const top = topKey(votes);
1458
1458
  if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
1459
- rename.set(zone, top.key);
1459
+ rename2.set(zone, top.key);
1460
1460
  takenNames.add(top.key);
1461
1461
  }
1462
- const renamed = (z2) => rename.get(z2) ?? z2;
1463
- if (rename.size) {
1462
+ const renamed = (z2) => rename2.get(z2) ?? z2;
1463
+ if (rename2.size) {
1464
1464
  for (const [f, z2] of fileZone) fileZone.set(f, renamed(z2));
1465
1465
  for (const [s, z2] of taskZone) taskZone.set(s, renamed(z2));
1466
1466
  }
@@ -1566,8 +1566,8 @@ function predictFiles(model, opts) {
1566
1566
  }
1567
1567
  const zone = normalizeZoneName(opts.zone) ?? (opts.text ? suggestZonesFromText(model, opts.text)[0]?.zone ?? null : null);
1568
1568
  if (zone) {
1569
- const stat = model.zones.find((z2) => z2.zone === zone);
1570
- if (stat) stat.files.slice(0, limit).forEach((f, i) => put(f, Math.max(0.12, 0.45 - i * 0.02), `\u0437\u043E\u043D\u0430 ${zone}`));
1569
+ const stat2 = model.zones.find((z2) => z2.zone === zone);
1570
+ if (stat2) stat2.files.slice(0, limit).forEach((f, i) => put(f, Math.max(0.12, 0.45 - i * 0.02), `\u0437\u043E\u043D\u0430 ${zone}`));
1571
1571
  }
1572
1572
  return [...best.values()].sort((a, b) => b.weight - a.weight || a.path.localeCompare(b.path)).slice(0, limit);
1573
1573
  }
@@ -2231,10 +2231,10 @@ function ensureCodexHome(worktree, realHome, env = process.env, swarmHome = SWAR
2231
2231
  const real = (env.CODEX_HOME ?? "").trim() || join5(realHome, ".codex");
2232
2232
  for (const file of ["auth.json", "config.toml"]) {
2233
2233
  const target = join5(real, file);
2234
- const link = join5(home, file);
2235
- if (!existsSync4(target) || existsSync4(link)) continue;
2234
+ const link2 = join5(home, file);
2235
+ if (!existsSync4(target) || existsSync4(link2)) continue;
2236
2236
  try {
2237
- symlinkSync(target, link);
2237
+ symlinkSync(target, link2);
2238
2238
  } catch {
2239
2239
  }
2240
2240
  }
@@ -2433,6 +2433,9 @@ __export(config_exports, {
2433
2433
  DRONE_HOME: () => DRONE_HOME,
2434
2434
  DRONE_HOME_ISOLATION: () => DRONE_HOME_ISOLATION,
2435
2435
  DRONE_ISOLATION: () => DRONE_ISOLATION,
2436
+ ENV_CACHE_DIR: () => ENV_CACHE_DIR,
2437
+ ENV_CACHE_ENABLED: () => ENV_CACHE_ENABLED,
2438
+ ENV_CACHE_KEEP: () => ENV_CACHE_KEEP,
2436
2439
  EXECUTOR: () => EXECUTOR,
2437
2440
  HEARTBEAT_PATH: () => HEARTBEAT_PATH,
2438
2441
  HEARTBEAT_STALE_MS: () => HEARTBEAT_STALE_MS,
@@ -2602,7 +2605,7 @@ function reviewCutoffAt() {
2602
2605
  }
2603
2606
  return at;
2604
2607
  }
2605
- 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, SWARM_WORKTREE_REAP_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_REST_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, SWARM_MERGE_QUEUE, 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;
2608
+ 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, ENV_CACHE_DIR, ENV_CACHE_ENABLED, ENV_CACHE_KEEP, 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, SWARM_WORKTREE_REAP_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_REST_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, SWARM_MERGE_QUEUE, 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;
2606
2609
  var init_config = __esm({
2607
2610
  "src/config.ts"() {
2608
2611
  "use strict";
@@ -2628,6 +2631,9 @@ var init_config = __esm({
2628
2631
  SWARM_OUTBOX = process.env.SWARM_OUTBOX || join6(SWARM_HOME, "outbox.jsonl");
2629
2632
  SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
2630
2633
  SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
2634
+ ENV_CACHE_DIR = process.env.SWARM_ENV_CACHE_DIR || join6(SWARM_HOME, "env-cache");
2635
+ ENV_CACHE_ENABLED = process.env.SWARM_ENV_CACHE !== "0";
2636
+ ENV_CACHE_KEEP = Math.trunc(envNum("SWARM_ENV_CACHE_KEEP", 4));
2631
2637
  SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
2632
2638
  SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
2633
2639
  SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
@@ -3327,6 +3333,118 @@ var init_logNoise = __esm({
3327
3333
  }
3328
3334
  });
3329
3335
 
3336
+ // src/netTimeout.ts
3337
+ import { basename as basename2 } from "path";
3338
+ function isTimedOut(outcome) {
3339
+ return typeof outcome === "object" && outcome !== null && outcome.timedOut === true;
3340
+ }
3341
+ function noteTimedOut(scope, call, timeoutMs, outcome, warn = console.warn) {
3342
+ const line = `[${scope}] \u23F1 \u0432\u044B\u0437\u043E\u0432 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0437\u0430 ${humanMs(timeoutMs)} \u0438 \u043F\u0440\u0435\u0440\u0432\u0430\u043D: ${call} \u2014 \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u0438\u0434\u0451\u0442 \u0434\u0430\u043B\u044C\u0448\u0435`;
3343
+ if (!isTimedOut(outcome)) {
3344
+ timeoutNoise.forget(noiseKey(scope, call));
3345
+ return false;
3346
+ }
3347
+ const say = timeoutNoise.say(noiseKey(scope, call), line, Date.now());
3348
+ if (say) warn(say);
3349
+ return true;
3350
+ }
3351
+ function noiseKey(scope, call) {
3352
+ return `timeout:${scope}:${call}`;
3353
+ }
3354
+ function callFailureWord(f) {
3355
+ switch (f.reason) {
3356
+ case "timeout":
3357
+ return "\u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0437\u0430 \u043E\u0442\u0432\u0435\u0434\u0451\u043D\u043D\u043E\u0435 \u0432\u0440\u0435\u043C\u044F (\u043F\u0440\u0435\u0440\u0432\u0430\u043D)";
3358
+ case "unreadable":
3359
+ return "\u043E\u0442\u0432\u0435\u0442\u0438\u043B, \u043D\u043E \u0432\u044B\u0432\u043E\u0434 \u043D\u0435 \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u043D";
3360
+ case "crashed":
3361
+ return "\u043D\u0435 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B\u0441\u044F";
3362
+ default:
3363
+ return "\u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F \u0431\u0435\u0437 \u0432\u044B\u0432\u043E\u0434\u0430";
3364
+ }
3365
+ }
3366
+ function classifyCallFailure(outcome) {
3367
+ if (isTimedOut(outcome)) return { reason: "timeout", detail: firstLineOf(errText(outcome)) };
3368
+ const code = outcome?.code;
3369
+ if (code === "ENOENT") return { reason: "crashed", detail: firstLineOf(errText(outcome)) };
3370
+ return { reason: "empty", detail: firstLineOf(errText(outcome)) };
3371
+ }
3372
+ function errText(outcome) {
3373
+ const o = outcome;
3374
+ for (const v of [o?.stderr, o?.shortMessage, o?.message]) if (typeof v === "string" && v.trim()) return v;
3375
+ return "";
3376
+ }
3377
+ function firstLineOf(s) {
3378
+ return s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "";
3379
+ }
3380
+ function noteCallFailure(scope, call, outcome, warn = console.warn) {
3381
+ const f = classifyCallFailure(outcome);
3382
+ if (f.reason === "timeout") return f;
3383
+ const line = `[${scope}] \u2716 \u0432\u044B\u0437\u043E\u0432 ${callFailureWord(f)}: ${call}${f.detail ? ` \u2014 ${f.detail}` : ""} \u2014 \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u0438\u0434\u0451\u0442 \u0434\u0430\u043B\u044C\u0448\u0435`;
3384
+ const say = timeoutNoise.say(noiseKey(scope, call), line, Date.now());
3385
+ if (say) warn(say);
3386
+ return f;
3387
+ }
3388
+ function humanMs(ms) {
3389
+ return ms >= 1e3 ? `${Math.round(ms / 1e3)}\u0441` : `${ms}\u043C\u0441`;
3390
+ }
3391
+ function destroyPipes(child) {
3392
+ for (const pipe of [child.stdout, child.stderr, child.stdin]) {
3393
+ try {
3394
+ pipe?.destroy?.();
3395
+ } catch {
3396
+ }
3397
+ }
3398
+ }
3399
+ async function withHardTimeout(child, scope, call, timeoutMs, warn = console.warn) {
3400
+ let timer;
3401
+ const capped = new Promise((_, reject) => {
3402
+ timer = setTimeout(() => {
3403
+ child.kill("SIGKILL");
3404
+ destroyPipes(child);
3405
+ reject(new CallTimedOutError(call, timeoutMs));
3406
+ }, timeoutMs + HARD_CAP_GRACE_MS);
3407
+ timer.unref?.();
3408
+ });
3409
+ try {
3410
+ const res = await Promise.race([child, capped]);
3411
+ noteTimedOut(scope, call, timeoutMs, res, warn);
3412
+ return res;
3413
+ } catch (e) {
3414
+ noteTimedOut(scope, call, timeoutMs, e, warn);
3415
+ throw e;
3416
+ } finally {
3417
+ if (timer) clearTimeout(timer);
3418
+ }
3419
+ }
3420
+ function callLabel(cmd, args, head = 4) {
3421
+ const shown = args.slice(0, head).join(" ");
3422
+ return `${cmd} ${shown}${args.length > head ? " \u2026" : ""}`.trim();
3423
+ }
3424
+ function gitCallLabel(repo, args) {
3425
+ return `git -C ${basename2(repo) || repo} ${args.join(" ")}`;
3426
+ }
3427
+ var GH_CALL_TIMEOUT_MS, GIT_NET_TIMEOUT_MS, GIT_LOCAL_TIMEOUT_MS, GIT_HEAVY_NET_TIMEOUT_MS, HARD_CAP_GRACE_MS, CallTimedOutError, timeoutNoise;
3428
+ var init_netTimeout = __esm({
3429
+ "src/netTimeout.ts"() {
3430
+ "use strict";
3431
+ init_logNoise();
3432
+ GH_CALL_TIMEOUT_MS = 15e3;
3433
+ GIT_NET_TIMEOUT_MS = 15e3;
3434
+ GIT_LOCAL_TIMEOUT_MS = 5e3;
3435
+ GIT_HEAVY_NET_TIMEOUT_MS = 6e4;
3436
+ HARD_CAP_GRACE_MS = 2e3;
3437
+ CallTimedOutError = class extends Error {
3438
+ timedOut = true;
3439
+ constructor(call, timeoutMs) {
3440
+ super(`\u0432\u044B\u0437\u043E\u0432 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0437\u0430 ${timeoutMs}\u043C\u0441 \u0438 \u043F\u0440\u0435\u0440\u0432\u0430\u043D: ${call}`);
3441
+ this.name = "CallTimedOutError";
3442
+ }
3443
+ };
3444
+ timeoutNoise = makeRepeatFilter();
3445
+ }
3446
+ });
3447
+
3330
3448
  // src/procList.ts
3331
3449
  function parsePsRows(out) {
3332
3450
  const rows = [];
@@ -3645,88 +3763,10 @@ var init_procCleanup = __esm({
3645
3763
  }
3646
3764
  });
3647
3765
 
3648
- // src/netTimeout.ts
3649
- import { basename as basename2 } from "path";
3650
- function isTimedOut(outcome) {
3651
- return typeof outcome === "object" && outcome !== null && outcome.timedOut === true;
3652
- }
3653
- function noteTimedOut(scope, call, timeoutMs, outcome, warn = console.warn) {
3654
- const line = `[${scope}] \u23F1 \u0432\u044B\u0437\u043E\u0432 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0437\u0430 ${humanMs(timeoutMs)} \u0438 \u043F\u0440\u0435\u0440\u0432\u0430\u043D: ${call} \u2014 \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u0438\u0434\u0451\u0442 \u0434\u0430\u043B\u044C\u0448\u0435`;
3655
- if (!isTimedOut(outcome)) {
3656
- timeoutNoise.forget(noiseKey(scope, call));
3657
- return false;
3658
- }
3659
- const say = timeoutNoise.say(noiseKey(scope, call), line, Date.now());
3660
- if (say) warn(say);
3661
- return true;
3662
- }
3663
- function noiseKey(scope, call) {
3664
- return `timeout:${scope}:${call}`;
3665
- }
3666
- function humanMs(ms) {
3667
- return ms >= 1e3 ? `${Math.round(ms / 1e3)}\u0441` : `${ms}\u043C\u0441`;
3668
- }
3669
- function destroyPipes(child) {
3670
- for (const pipe of [child.stdout, child.stderr, child.stdin]) {
3671
- try {
3672
- pipe?.destroy?.();
3673
- } catch {
3674
- }
3675
- }
3676
- }
3677
- async function withHardTimeout(child, scope, call, timeoutMs, warn = console.warn) {
3678
- let timer;
3679
- const capped = new Promise((_, reject) => {
3680
- timer = setTimeout(() => {
3681
- child.kill("SIGKILL");
3682
- destroyPipes(child);
3683
- reject(new CallTimedOutError(call, timeoutMs));
3684
- }, timeoutMs + HARD_CAP_GRACE_MS);
3685
- timer.unref?.();
3686
- });
3687
- try {
3688
- const res = await Promise.race([child, capped]);
3689
- noteTimedOut(scope, call, timeoutMs, res, warn);
3690
- return res;
3691
- } catch (e) {
3692
- noteTimedOut(scope, call, timeoutMs, e, warn);
3693
- throw e;
3694
- } finally {
3695
- if (timer) clearTimeout(timer);
3696
- }
3697
- }
3698
- function callLabel(cmd, args, head = 4) {
3699
- const shown = args.slice(0, head).join(" ");
3700
- return `${cmd} ${shown}${args.length > head ? " \u2026" : ""}`.trim();
3701
- }
3702
- function gitCallLabel(repo, args) {
3703
- return `git -C ${basename2(repo) || repo} ${args.join(" ")}`;
3704
- }
3705
- var GH_CALL_TIMEOUT_MS, GIT_NET_TIMEOUT_MS, GIT_LOCAL_TIMEOUT_MS, GIT_HEAVY_NET_TIMEOUT_MS, HARD_CAP_GRACE_MS, CallTimedOutError, timeoutNoise;
3706
- var init_netTimeout = __esm({
3707
- "src/netTimeout.ts"() {
3708
- "use strict";
3709
- init_logNoise();
3710
- GH_CALL_TIMEOUT_MS = 15e3;
3711
- GIT_NET_TIMEOUT_MS = 15e3;
3712
- GIT_LOCAL_TIMEOUT_MS = 5e3;
3713
- GIT_HEAVY_NET_TIMEOUT_MS = 6e4;
3714
- HARD_CAP_GRACE_MS = 2e3;
3715
- CallTimedOutError = class extends Error {
3716
- timedOut = true;
3717
- constructor(call, timeoutMs) {
3718
- super(`\u0432\u044B\u0437\u043E\u0432 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0437\u0430 ${timeoutMs}\u043C\u0441 \u0438 \u043F\u0440\u0435\u0440\u0432\u0430\u043D: ${call}`);
3719
- this.name = "CallTimedOutError";
3720
- }
3721
- };
3722
- timeoutNoise = makeRepeatFilter();
3723
- }
3724
- });
3725
-
3726
3766
  // src/git.ts
3727
- import { existsSync as existsSync11, readdirSync as readdirSync11, statSync as statSync8 } from "fs";
3767
+ import { existsSync as existsSync12, readdirSync as readdirSync11, statSync as statSync8 } from "fs";
3728
3768
  import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
3729
- import { isAbsolute as isAbsolute2, join as join18, sep as sep2 } from "path";
3769
+ import { isAbsolute as isAbsolute2, join as join19, sep as sep2 } from "path";
3730
3770
  import { $ as $2 } from "execa";
3731
3771
  function gitNet(repo, args, timeoutMs) {
3732
3772
  return withHardTimeout(
@@ -3749,7 +3789,7 @@ async function reclaimStaleWorktree(repo, worktree) {
3749
3789
  }
3750
3790
  function isLinkedWorktree(dir) {
3751
3791
  try {
3752
- return statSync8(join18(dir, ".git")).isFile();
3792
+ return statSync8(join19(dir, ".git")).isFile();
3753
3793
  } catch {
3754
3794
  return false;
3755
3795
  }
@@ -3761,7 +3801,7 @@ async function detachWorktree(repo, worktree) {
3761
3801
  }
3762
3802
  }
3763
3803
  async function ensureWorktreeDirGone(worktree) {
3764
- if (!existsSync11(worktree)) return false;
3804
+ if (!existsSync12(worktree)) return false;
3765
3805
  if (!isLinkedWorktree(worktree)) {
3766
3806
  console.error(
3767
3807
  `[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.`
@@ -3776,7 +3816,7 @@ async function ensureWorktreeDirGone(worktree) {
3776
3816
  );
3777
3817
  return true;
3778
3818
  }
3779
- if (existsSync11(worktree)) {
3819
+ if (existsSync12(worktree)) {
3780
3820
  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.`);
3781
3821
  }
3782
3822
  return true;
@@ -3824,7 +3864,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
3824
3864
  }
3825
3865
  const busyAt = await worktreeForBranch(repo, branch);
3826
3866
  if (busyAt) {
3827
- const exists = existsSync11(busyAt);
3867
+ const exists = existsSync12(busyAt);
3828
3868
  const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
3829
3869
  if (live) throw new WorktreeBusyError(branch, busyAt);
3830
3870
  if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
@@ -3859,8 +3899,8 @@ async function ensureGitExcluded(worktree, pattern) {
3859
3899
  try {
3860
3900
  const commonDir = await gitPath(worktree, "--git-common-dir");
3861
3901
  if (!commonDir) return false;
3862
- const infoDir = join18(commonDir, "info");
3863
- const excludeFile = join18(infoDir, "exclude");
3902
+ const infoDir = join19(commonDir, "info");
3903
+ const excludeFile = join19(infoDir, "exclude");
3864
3904
  let current = "";
3865
3905
  try {
3866
3906
  current = await readFile(excludeFile, "utf8");
@@ -3880,7 +3920,7 @@ async function gitPath(worktree, flag) {
3880
3920
  const out = await $2({ reject: false })`git -C ${worktree} rev-parse ${flag}`;
3881
3921
  const raw = out.stdout.trim();
3882
3922
  if (out.exitCode !== 0 || !raw) return null;
3883
- return isAbsolute2(raw) ? raw : join18(worktree, raw);
3923
+ return isAbsolute2(raw) ? raw : join19(worktree, raw);
3884
3924
  }
3885
3925
  async function ensureWorktreeHygiene(worktree) {
3886
3926
  for (const pattern of SWARM_EXCLUDE_PATTERNS) await ensureGitExcluded(worktree, pattern);
@@ -3890,12 +3930,12 @@ async function installGuardHooks(worktree) {
3890
3930
  try {
3891
3931
  const gitDir = await gitPath(worktree, "--git-dir");
3892
3932
  if (!gitDir) return false;
3893
- const hooksDir = join18(gitDir, "swarm-hooks");
3933
+ const hooksDir = join19(gitDir, "swarm-hooks");
3894
3934
  const prev = await previousHooksPath(worktree);
3895
3935
  await mkdir(hooksDir, { recursive: true });
3896
3936
  for (const hook of GUARD_HOOKS) {
3897
- await writeFile(join18(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
3898
- await chmod(join18(hooksDir, hook), 493);
3937
+ await writeFile(join19(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
3938
+ await chmod(join19(hooksDir, hook), 493);
3899
3939
  }
3900
3940
  await $2`git -C ${worktree} config extensions.worktreeConfig true`;
3901
3941
  await $2`git -C ${worktree} config --worktree core.hooksPath ${posix(hooksDir)}`;
@@ -3914,7 +3954,7 @@ async function previousHooksPath(worktree) {
3914
3954
  if (out.exitCode === 0 && value) return value;
3915
3955
  }
3916
3956
  const commonDir = await gitPath(worktree, "--git-common-dir");
3917
- return commonDir ? join18(commonDir, "hooks") : null;
3957
+ return commonDir ? join19(commonDir, "hooks") : null;
3918
3958
  }
3919
3959
  async function removeWorktree(repo, path) {
3920
3960
  const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
@@ -3939,7 +3979,7 @@ async function worktreeDirty(path) {
3939
3979
  async function repairMovedWorktrees(targetDir) {
3940
3980
  const subdirs = (dir) => {
3941
3981
  try {
3942
- return readdirSync11(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join18(dir, d.name));
3982
+ return readdirSync11(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join19(dir, d.name));
3943
3983
  } catch {
3944
3984
  return [];
3945
3985
  }
@@ -4056,6 +4096,7 @@ var init_git = __esm({
4056
4096
  var droneChannel_exports = {};
4057
4097
  __export(droneChannel_exports, {
4058
4098
  ChannelUnavailableError: () => ChannelUnavailableError,
4099
+ PATCHABLE: () => PATCHABLE,
4059
4100
  channelDroneStore: () => channelDroneStore,
4060
4101
  closeDroneChannel: () => closeDroneChannel,
4061
4102
  loadChannelSecret: () => loadChannelSecret,
@@ -4068,18 +4109,18 @@ __export(droneChannel_exports, {
4068
4109
  verifyLocalCliToken: () => verifyLocalCliToken
4069
4110
  });
4070
4111
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
4071
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "fs";
4112
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync9, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
4072
4113
  import { dirname as dirname4 } from "path";
4073
4114
  import { serve } from "@hono/node-server";
4074
4115
  function loadChannelSecret(path) {
4075
4116
  try {
4076
- const s = readFileSync15(path, "utf8").trim();
4117
+ const s = readFileSync18(path, "utf8").trim();
4077
4118
  if (s.length >= 32) return s;
4078
4119
  } catch {
4079
4120
  }
4080
4121
  const secret = randomBytes(32).toString("hex");
4081
4122
  mkdirSync9(dirname4(path), { recursive: true });
4082
- writeFileSync11(path, secret, { mode: 384 });
4123
+ writeFileSync12(path, secret, { mode: 384 });
4083
4124
  try {
4084
4125
  chmodSync2(path, 384);
4085
4126
  } catch {
@@ -4139,6 +4180,16 @@ function invalidPatchValue(field, v) {
4139
4180
  return typeof v === "number" && Number.isInteger(v) && v >= 0 ? null : `\u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0439 \u0446\u0438\u043A\u043B \u0440\u0435\u0432\u044C\u044E: ${String(v)}`;
4140
4181
  case "pr_url":
4141
4182
  return v === null || typeof v === "string" ? null : "pr_url \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u043E\u0439";
4183
+ // TASWM-532: имя ветки уходит в аргументы git у ревьюера и резолвера, поэтому проверяем
4184
+ // форму, а не только тип: пустая строка и пробелы внутри — это не ref, а сбой чтения.
4185
+ case "branch":
4186
+ return typeof v === "string" && v.length > 0 && v.length <= BRANCH_LEN_MAX && !BAD_IN_BRANCH.test(v) ? null : `\u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u043E\u0435 \u0438\u043C\u044F \u0432\u0435\u0442\u043A\u0438: ${String(v).slice(0, 80)}`;
4187
+ case "stop_reason":
4188
+ return v === null || typeof v === "string" && v.length <= STOP_REASON_LEN_MAX ? null : "stop_reason \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u043E\u0439 (\u0438\u043B\u0438 null) \u0432 \u043F\u0440\u0435\u0434\u0435\u043B\u0430\u0445 \u043F\u043E\u0442\u043E\u043B\u043A\u0430 \u0434\u043B\u0438\u043D\u044B";
4189
+ // Словарь кодов возврата один на весь продукт (swarm-core) — проверяем его же функцией,
4190
+ // как verify_verdict выше, а не повторяем литералы ещё одним списком.
4191
+ case "return_reason":
4192
+ return v === null || isReturnReason(v) ? null : `\u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0439 \u043A\u043E\u0434 \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u0430: ${String(v)}`;
4142
4193
  case "changed_paths": {
4143
4194
  if (!Array.isArray(v)) return "changed_paths \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043C\u0430\u0441\u0441\u0438\u0432\u043E\u043C \u043F\u0443\u0442\u0435\u0439";
4144
4195
  if (v.length > CHANGED_PATHS_MAX) return `changed_paths: \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u043C\u043D\u043E\u0433\u043E \u043F\u0443\u0442\u0435\u0439 (${v.length} > ${CHANGED_PATHS_MAX})`;
@@ -4202,10 +4253,18 @@ function makeChannelHandler(deps) {
4202
4253
  if (id == null) return bad(403, "\u043F\u0440\u043E\u0433\u043E\u043D \u0432\u043D\u0435 \u0441\u043A\u043E\u0443\u043F\u0430 \u043A\u0430\u043D\u0430\u043B\u0430");
4203
4254
  const raw = p.patch ?? {};
4204
4255
  const patch = {};
4256
+ const refuse = (status, why) => {
4257
+ log(`[\u043A\u0430\u043D\u0430\u043B] \u043F\u0430\u0442\u0447 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${id} \u043E\u0442\u043A\u043B\u043E\u043D\u0451\u043D (${status}): ${why}`);
4258
+ try {
4259
+ deps.store.addEvent(id, "step", `\u043A\u0430\u043D\u0430\u043B \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u043B \u043F\u0430\u0442\u0447 \u0434\u0440\u043E\u043D\u0430 (${status}): ${why}`, null);
4260
+ } catch {
4261
+ }
4262
+ return bad(status, why);
4263
+ };
4205
4264
  for (const [k, v] of Object.entries(raw)) {
4206
- if (!PATCHABLE.has(k)) return bad(403, `\u043F\u043E\u043B\u0435 \xAB${k}\xBB \u043A\u0430\u043D\u0430\u043B\u0443 \u0434\u0440\u043E\u043D\u0430 \u043D\u0435 \u043E\u0442\u0434\u0430\u0451\u0442\u0441\u044F`);
4265
+ if (!PATCHABLE.has(k)) return refuse(403, `\u043F\u043E\u043B\u0435 \xAB${k}\xBB \u043A\u0430\u043D\u0430\u043B\u0443 \u0434\u0440\u043E\u043D\u0430 \u043D\u0435 \u043E\u0442\u0434\u0430\u0451\u0442\u0441\u044F`);
4207
4266
  const why = invalidPatchValue(k, v);
4208
- if (why) return bad(400, why);
4267
+ if (why) return refuse(400, why);
4209
4268
  patch[k] = v;
4210
4269
  }
4211
4270
  deps.store.updateRun(id, patch);
@@ -4398,7 +4457,7 @@ function channelDroneStore(opts) {
4398
4457
  openQuestionsFor: async () => (await call("openQuestionsFor")).questions
4399
4458
  };
4400
4459
  }
4401
- var LOCAL_CLI_KEY, PATCHABLE, CHANGED_PATHS_MAX, CHANGED_PATH_LEN_MAX, EVENT_KIND_SET, ChannelUnavailableError, BACKOFF_MS;
4460
+ var LOCAL_CLI_KEY, PATCHABLE, CHANGED_PATHS_MAX, CHANGED_PATH_LEN_MAX, BRANCH_LEN_MAX, STOP_REASON_LEN_MAX, BAD_IN_BRANCH, EVENT_KIND_SET, ChannelUnavailableError, BACKOFF_MS;
4402
4461
  var init_droneChannel = __esm({
4403
4462
  "src/droneChannel.ts"() {
4404
4463
  "use strict";
@@ -4418,10 +4477,24 @@ var init_droneChannel = __esm({
4418
4477
  // worktree), а не модель — со стороны канала это такой же самоотчёт прогона о себе, как
4419
4478
  // pr_url. Потолки на размер — ниже, в invalidPatchValue: список идёт в гейт раздачи, и
4420
4479
  // сорвавшийся дрон не должен уметь заявить занятым весь репозиторий.
4421
- "changed_paths"
4480
+ "changed_paths",
4481
+ // TASWM-532: ФАКТИЧЕСКАЯ ветка сданного PR. Её подмешивает в патч сдачи не модель, а наш код
4482
+ // (droneServer.done → branchFromPrPatch по ответу `gh pr view --json headRefName`) — тот же
4483
+ // довод, что у changed_paths строкой выше. «Ветка и так известна из прогона» — неправда:
4484
+ // поле пишется на спавне из слага задачи, а дрон мог продолжать ЧУЖОЙ PR, и по этому полю
4485
+ // ревьюер строит worktree `origin/<branch>` (TASWM-356).
4486
+ "branch",
4487
+ // TASWM-532: возврат задачи автору (`return_task`) пишет их одним патчем со статусом
4488
+ // "returned" — проза для ленты задачи и код причины для распределения по бэклогу
4489
+ // (TASWM-505). Без них 403 ложился на возврат ровно так же, как на сдачу.
4490
+ "stop_reason",
4491
+ "return_reason"
4422
4492
  ]);
4423
4493
  CHANGED_PATHS_MAX = 1e3;
4424
4494
  CHANGED_PATH_LEN_MAX = 1024;
4495
+ BRANCH_LEN_MAX = 255;
4496
+ STOP_REASON_LEN_MAX = 2e3;
4497
+ BAD_IN_BRANCH = /[\s~^:?*[\\\p{Cc}]/u;
4425
4498
  EVENT_KIND_SET = new Set(EVENT_KINDS);
4426
4499
  ChannelUnavailableError = class extends Error {
4427
4500
  constructor(downMs, message) {
@@ -4436,8 +4509,8 @@ var init_droneChannel = __esm({
4436
4509
  });
4437
4510
 
4438
4511
  // src/attachments.ts
4439
- import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
4440
- import { join as join21 } from "path";
4512
+ import { mkdir as mkdir3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
4513
+ import { join as join23 } from "path";
4441
4514
  function attachmentsFromDoc(doc, origin) {
4442
4515
  const out = [];
4443
4516
  const walk2 = (node) => {
@@ -4522,13 +4595,13 @@ async function materializeAttachments(opts) {
4522
4595
  const maxTotal = opts.maxTotalBytes ?? ATTACH_MAX_TOTAL_BYTES;
4523
4596
  const maxFile = opts.maxFileBytes ?? ATTACH_MAX_FILE_BYTES;
4524
4597
  const io = opts.io ?? {
4525
- removeDir: async (dir2) => rm2(dir2, { recursive: true, force: true }),
4526
- mkdir: async (dir2) => void await mkdir2(dir2, { recursive: true }),
4527
- writeFile: async (path, bytes) => writeFile2(path, bytes)
4598
+ removeDir: async (dir2) => rm3(dir2, { recursive: true, force: true }),
4599
+ mkdir: async (dir2) => void await mkdir3(dir2, { recursive: true }),
4600
+ writeFile: async (path, bytes) => writeFile3(path, bytes)
4528
4601
  };
4529
4602
  const result = { files: [], skipped: [] };
4530
4603
  if (opts.refs.length === 0) return result;
4531
- const dir = join21(opts.worktree, ATTACHMENTS_DIR);
4604
+ const dir = join23(opts.worktree, ATTACHMENTS_DIR);
4532
4605
  await io.removeDir(dir);
4533
4606
  let dirReady = false;
4534
4607
  let total = 0;
@@ -4553,7 +4626,7 @@ async function materializeAttachments(opts) {
4553
4626
  try {
4554
4627
  file = await opts.fetchFile(ref.url);
4555
4628
  } catch (e) {
4556
- result.skipped.push(`${label2} \u2014 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u043E\u0441\u044C (${errText(e)})`);
4629
+ result.skipped.push(`${label2} \u2014 \u043D\u0435 \u0441\u043A\u0430\u0447\u0430\u043B\u043E\u0441\u044C (${errText2(e)})`);
4557
4630
  continue;
4558
4631
  }
4559
4632
  if (!file) {
@@ -4574,9 +4647,9 @@ async function materializeAttachments(opts) {
4574
4647
  await io.mkdir(dir);
4575
4648
  dirReady = true;
4576
4649
  }
4577
- await io.writeFile(join21(dir, name), file.bytes);
4650
+ await io.writeFile(join23(dir, name), file.bytes);
4578
4651
  } catch (e) {
4579
- result.skipped.push(`${label2} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText(e)})`);
4652
+ result.skipped.push(`${label2} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText2(e)})`);
4580
4653
  continue;
4581
4654
  }
4582
4655
  total += file.bytes.byteLength;
@@ -4630,7 +4703,7 @@ async function prepareTaskAttachments(opts) {
4630
4703
  }
4631
4704
  return renderAttachmentsBlock(result);
4632
4705
  } catch (e) {
4633
- opts.onEvent?.(`\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0437\u0430\u0434\u0430\u0447\u0438 \u0432\u044B\u043B\u043E\u0436\u0438\u0442\u044C \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: ${errText(e)} \u2014 \u0434\u0440\u043E\u043D \u0441\u0442\u0430\u0440\u0442\u0443\u0435\u0442 \u0431\u0435\u0437 \u043D\u0438\u0445`);
4706
+ opts.onEvent?.(`\u0432\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0437\u0430\u0434\u0430\u0447\u0438 \u0432\u044B\u043B\u043E\u0436\u0438\u0442\u044C \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: ${errText2(e)} \u2014 \u0434\u0440\u043E\u043D \u0441\u0442\u0430\u0440\u0442\u0443\u0435\u0442 \u0431\u0435\u0437 \u043D\u0438\u0445`);
4634
4707
  return void 0;
4635
4708
  }
4636
4709
  }
@@ -4653,7 +4726,7 @@ function mb(bytes) {
4653
4726
  if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} \u041C\u0411`;
4654
4727
  return `${bytes === 0 ? 0 : Math.max(1, Math.round(bytes / 1024))} \u041A\u0411`;
4655
4728
  }
4656
- function errText(e) {
4729
+ function errText2(e) {
4657
4730
  return e instanceof Error ? e.message : String(e);
4658
4731
  }
4659
4732
  var ATTACHMENTS_DIR, ATTACH_MAX_FILES, ATTACH_MAX_TOTAL_BYTES, ATTACH_MAX_FILE_BYTES, IMAGE_EXT, DOC_EXT, TEXT_EXT, KNOWN_EXT, TRANSLIT, EXT_BY_MIME;
@@ -4727,7 +4800,7 @@ var init_attachments = __esm({
4727
4800
 
4728
4801
  // src/shotUpload.ts
4729
4802
  import { readFile as fsReadFile } from "fs/promises";
4730
- import { basename as basename4, isAbsolute as isAbsolute3, join as join22 } from "path";
4803
+ import { basename as basename4, isAbsolute as isAbsolute3, join as join24 } from "path";
4731
4804
  function shotMimetype(path) {
4732
4805
  const dot = path.lastIndexOf(".");
4733
4806
  if (dot < 0) return null;
@@ -4753,7 +4826,7 @@ async function readShots(paths, opts) {
4753
4826
  }
4754
4827
  let bytes;
4755
4828
  try {
4756
- bytes = await read(isAbsolute3(path) ? path : join22(opts.worktree, path));
4829
+ bytes = await read(isAbsolute3(path) ? path : join24(opts.worktree, path));
4757
4830
  } catch (e) {
4758
4831
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
4759
4832
  continue;
@@ -4833,12 +4906,12 @@ var init_shotUpload = __esm({
4833
4906
  });
4834
4907
 
4835
4908
  // src/oauth.ts
4836
- import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
4837
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4 } from "fs";
4838
- import { join as join23 } from "path";
4909
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
4910
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, unlinkSync as unlinkSync4 } from "fs";
4911
+ import { join as join25 } from "path";
4839
4912
  function makePkce() {
4840
4913
  const verifier = randomBytes2(48).toString("base64url");
4841
- const challenge = createHash2("sha256").update(verifier).digest("base64url");
4914
+ const challenge = createHash3("sha256").update(verifier).digest("base64url");
4842
4915
  return { verifier, challenge };
4843
4916
  }
4844
4917
  async function registerClient(args, fetchFn = fetch) {
@@ -4919,13 +4992,13 @@ function toStore(clientId, t, now) {
4919
4992
  }
4920
4993
  function loadStore() {
4921
4994
  try {
4922
- return JSON.parse(readFileSync16(STORE_PATH2, "utf8"));
4995
+ return JSON.parse(readFileSync19(STORE_PATH2, "utf8"));
4923
4996
  } catch {
4924
4997
  return null;
4925
4998
  }
4926
4999
  }
4927
5000
  function saveStore(s) {
4928
- writeFileSync12(STORE_PATH2, JSON.stringify(s, null, 2), { mode: 384 });
5001
+ writeFileSync13(STORE_PATH2, JSON.stringify(s, null, 2), { mode: 384 });
4929
5002
  }
4930
5003
  function clearStore() {
4931
5004
  try {
@@ -4935,7 +5008,7 @@ function clearStore() {
4935
5008
  }
4936
5009
  function loadClientId() {
4937
5010
  try {
4938
- return JSON.parse(readFileSync16(CLIENT_PATH, "utf8")).clientId;
5011
+ return JSON.parse(readFileSync19(CLIENT_PATH, "utf8")).clientId;
4939
5012
  } catch {
4940
5013
  return null;
4941
5014
  }
@@ -4944,7 +5017,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
4944
5017
  const existing = loadClientId();
4945
5018
  if (existing) return existing;
4946
5019
  const clientId = await registerClient({ api, redirectUri }, fetchFn);
4947
- writeFileSync12(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
5020
+ writeFileSync13(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
4948
5021
  return clientId;
4949
5022
  }
4950
5023
  async function ensureAccess(store, opts = {}) {
@@ -4970,8 +5043,8 @@ var init_oauth = __esm({
4970
5043
  AUTHORIZE = "/api/oauth/authorize";
4971
5044
  TOKEN = "/api/oauth/token";
4972
5045
  SCOPE = "openid profile email taskless:full";
4973
- STORE_PATH2 = join23(SWARM_HOME, "oauth.json");
4974
- CLIENT_PATH = join23(SWARM_HOME, "oauth-client.json");
5046
+ STORE_PATH2 = join25(SWARM_HOME, "oauth.json");
5047
+ CLIENT_PATH = join25(SWARM_HOME, "oauth-client.json");
4975
5048
  base2 = (api) => api.replace(/\/$/, "");
4976
5049
  }
4977
5050
  });
@@ -5017,11 +5090,11 @@ var init_authState = __esm({
5017
5090
  });
5018
5091
 
5019
5092
  // src/workspace.ts
5020
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync13, unlinkSync as unlinkSync5 } from "fs";
5021
- import { join as join24 } from "path";
5093
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5 } from "fs";
5094
+ import { join as join26 } from "path";
5022
5095
  function loadWorkspace() {
5023
5096
  try {
5024
- const w = JSON.parse(readFileSync17(WORKSPACE_PATH, "utf8"));
5097
+ const w = JSON.parse(readFileSync20(WORKSPACE_PATH, "utf8"));
5025
5098
  return w && typeof w.id === "string" && w.id ? w : null;
5026
5099
  } catch {
5027
5100
  return null;
@@ -5035,7 +5108,7 @@ var init_workspace = __esm({
5035
5108
  "src/workspace.ts"() {
5036
5109
  "use strict";
5037
5110
  init_config();
5038
- WORKSPACE_PATH = join24(SWARM_HOME, "workspace.json");
5111
+ WORKSPACE_PATH = join26(SWARM_HOME, "workspace.json");
5039
5112
  }
5040
5113
  });
5041
5114
 
@@ -5616,7 +5689,7 @@ __export(db_exports, {
5616
5689
  });
5617
5690
  import { createRequire as createRequire2 } from "module";
5618
5691
  import { randomUUID as randomUUID3 } from "crypto";
5619
- import { existsSync as existsSync13 } from "fs";
5692
+ import { existsSync as existsSync15 } from "fs";
5620
5693
  import { resolve as resolve3 } from "path";
5621
5694
  function rowToQuestion(row) {
5622
5695
  return {
@@ -6239,7 +6312,7 @@ CREATE TABLE IF NOT EXISTS imports (
6239
6312
  const repoN = normalizeRepo(repo);
6240
6313
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
6241
6314
  if (already) return { imported: false, runs: already.run_count };
6242
- if (!existsSync13(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
6315
+ if (!existsSync15(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
6243
6316
  const src = new this.databaseCtor(abs);
6244
6317
  try {
6245
6318
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -6608,10 +6681,10 @@ var init_droneStore = __esm({
6608
6681
 
6609
6682
  // src/overmind.ts
6610
6683
  import { homedir as homedir6 } from "os";
6611
- import { existsSync as existsSync18, readFileSync as readFileSync21, readdirSync as readdirSync13, renameSync as renameSync11 } from "fs";
6612
- import { basename as basename6, dirname as dirname5, join as join31 } from "path";
6684
+ import { existsSync as existsSync20, readFileSync as readFileSync24, readdirSync as readdirSync13, renameSync as renameSync12 } from "fs";
6685
+ import { basename as basename6, dirname as dirname5, join as join33 } from "path";
6613
6686
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
6614
- import { spawn as spawn3 } from "child_process";
6687
+ import { spawn as spawn4 } from "child_process";
6615
6688
 
6616
6689
  // src/restart.ts
6617
6690
  init_src();
@@ -6654,11 +6727,11 @@ function savePauseState(home, state, io = {}) {
6654
6727
  remove(path);
6655
6728
  return;
6656
6729
  }
6657
- const writeFile3 = io.writeFile ?? ((p, data) => {
6730
+ const writeFile4 = io.writeFile ?? ((p, data) => {
6658
6731
  writeFileSync(`${p}.tmp`, data);
6659
6732
  renameSync(`${p}.tmp`, p);
6660
6733
  });
6661
- writeFile3(path, JSON.stringify(state, null, 2));
6734
+ writeFile4(path, JSON.stringify(state, null, 2));
6662
6735
  }
6663
6736
  function makePauseState(source, reason, now = Date.now()) {
6664
6737
  return {
@@ -9415,6 +9488,26 @@ function rotateDaemonLog(path, opts, io = realIo) {
9415
9488
  return "rotated";
9416
9489
  }
9417
9490
 
9491
+ // src/daemonLaunch.ts
9492
+ import { openSync as openSync2, closeSync as closeSync2, readFileSync as readFileSync8 } from "fs";
9493
+ import { spawn } from "child_process";
9494
+ var realRespawnIo = {
9495
+ isTty: () => process.stdout.isTTY === true,
9496
+ open: (path) => openSync2(path, "a")
9497
+ };
9498
+ function respawnStdio(logPath, io = realRespawnIo) {
9499
+ if (io.isTty()) return "inherit";
9500
+ try {
9501
+ const fd = io.open(logPath);
9502
+ return ["ignore", fd, fd];
9503
+ } catch {
9504
+ return "inherit";
9505
+ }
9506
+ }
9507
+
9508
+ // src/overmind.ts
9509
+ init_netTimeout();
9510
+
9418
9511
  // src/logStamp.ts
9419
9512
  import { format } from "util";
9420
9513
  function logStamp(at) {
@@ -9496,13 +9589,13 @@ async function buildRepoRegistry(paths) {
9496
9589
  }
9497
9590
 
9498
9591
  // src/repoMemory.ts
9499
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync8 } from "fs";
9592
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
9500
9593
  import { join as join10 } from "path";
9501
9594
  var FILE = "repos.json";
9502
9595
  function loadRememberedRepos(home) {
9503
9596
  let raw;
9504
9597
  try {
9505
- raw = readFileSync8(join10(home, FILE), "utf8");
9598
+ raw = readFileSync9(join10(home, FILE), "utf8");
9506
9599
  } catch {
9507
9600
  return [];
9508
9601
  }
@@ -9532,11 +9625,11 @@ init_src();
9532
9625
  init_config();
9533
9626
  init_linkFailure();
9534
9627
  import { homedir as homedir4 } from "os";
9535
- import { readFileSync as readFileSync10, renameSync as renameSync6, writeFileSync as writeFileSync9 } from "fs";
9628
+ import { readFileSync as readFileSync11, renameSync as renameSync6, writeFileSync as writeFileSync9 } from "fs";
9536
9629
  import { isAbsolute, join as join12, resolve as resolve2 } from "path";
9537
9630
 
9538
9631
  // src/repoScan.ts
9539
- import { readdirSync as readdirSync6, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
9632
+ import { readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
9540
9633
  import { join as join11 } from "path";
9541
9634
  var SKIP = /* @__PURE__ */ new Set([
9542
9635
  "node_modules",
@@ -9567,7 +9660,7 @@ function parseOriginFromGitConfig(text) {
9567
9660
  }
9568
9661
  function originOf(dir) {
9569
9662
  try {
9570
- return parseOriginFromGitConfig(readFileSync9(join11(dir, ".git", "config"), "utf8"));
9663
+ return parseOriginFromGitConfig(readFileSync10(join11(dir, ".git", "config"), "utf8"));
9571
9664
  } catch {
9572
9665
  return null;
9573
9666
  }
@@ -9852,7 +9945,7 @@ function repoMapRows(state) {
9852
9945
  }
9853
9946
  function loadRepoMapCache(path) {
9854
9947
  try {
9855
- const raw = JSON.parse(readFileSync10(path, "utf8"));
9948
+ const raw = JSON.parse(readFileSync11(path, "utf8"));
9856
9949
  if (!Array.isArray(raw?.entries)) return { scannedAt: null, entries: [] };
9857
9950
  return {
9858
9951
  scannedAt: typeof raw.scannedAt === "string" && raw.scannedAt ? raw.scannedAt : null,
@@ -9875,7 +9968,7 @@ function writeRepoMapCache(path, entries, scannedAt = stamp()) {
9875
9968
 
9876
9969
  // src/hostIdentity.ts
9877
9970
  import { hostname } from "os";
9878
- import { existsSync as existsSync9, readFileSync as readFileSync11, renameSync as renameSync7, writeFileSync as writeFileSync10 } from "fs";
9971
+ import { existsSync as existsSync9, readFileSync as readFileSync12, renameSync as renameSync7, writeFileSync as writeFileSync10 } from "fs";
9879
9972
  import { join as join13 } from "path";
9880
9973
  import { randomUUID as randomUUID2 } from "crypto";
9881
9974
  function hostIdentityPath(home) {
@@ -9883,8 +9976,8 @@ function hostIdentityPath(home) {
9883
9976
  }
9884
9977
  function loadHostIdentity(deps) {
9885
9978
  const exists = deps.exists ?? existsSync9;
9886
- const readFile2 = deps.readFile ?? ((p) => readFileSync11(p, "utf8"));
9887
- const writeFile3 = deps.writeFile ?? ((p, data) => {
9979
+ const readFile2 = deps.readFile ?? ((p) => readFileSync12(p, "utf8"));
9980
+ const writeFile4 = deps.writeFile ?? ((p, data) => {
9888
9981
  writeFileSync10(`${p}.tmp`, data);
9889
9982
  renameSync7(`${p}.tmp`, p);
9890
9983
  });
@@ -9904,7 +9997,7 @@ function loadHostIdentity(deps) {
9904
9997
  }
9905
9998
  const hostId = newId();
9906
9999
  try {
9907
- writeFile3(path, JSON.stringify({ hostId, hostName, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
10000
+ writeFile4(path, JSON.stringify({ hostId, hostName, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
9908
10001
  } catch {
9909
10002
  console.warn(`[overmind] \u043D\u0435 \u0441\u043C\u043E\u0433 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C ${path} \u2014 hostId \u0436\u0438\u0432\u0451\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430`);
9910
10003
  }
@@ -9928,7 +10021,13 @@ var DEFAULT_RELIEF_CONFIG = {
9928
10021
  function resolveReliefConfig(cfg) {
9929
10022
  return { ...DEFAULT_RELIEF_CONFIG, ...cfg ?? {} };
9930
10023
  }
9931
- var RELIEF_PAUSE_REASON = `${RELIEF_PAUSE_MARK}: \u0445\u043E\u0441\u0442 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D (SIGTERM, \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0447\u0435\u0440\u0435\u0437 --continue), \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 \u043F\u043E\u0441\u043B\u0435 \u043D\u043E\u0440\u043C\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438 (TASWM-151)`;
10024
+ var RELIEF_PAUSE_REASON = `${RELIEF_PAUSE_MARK}: \u0445\u043E\u0441\u0442 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D (SIGTERM), \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 \u043F\u043E\u0441\u043B\u0435 \u043D\u043E\u0440\u043C\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438 (TASWM-151)`;
10025
+ function reliefPauseReason(canContinue) {
10026
+ return canContinue ? `${RELIEF_PAUSE_MARK}: \u0445\u043E\u0441\u0442 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D (SIGTERM, \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0447\u0435\u0440\u0435\u0437 --continue: worktree \u0438 \u0441\u0435\u0441\u0441\u0438\u044F \u043D\u0430 \u043C\u0435\u0441\u0442\u0435)` : `${RELIEF_PAUSE_MARK}: \u0445\u043E\u0441\u0442 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D (SIGTERM; \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u043C \u2014 worktree/\u0441\u0435\u0441\u0441\u0438\u044F \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B, \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0441\u0442\u0430\u0440\u0442\u043E\u043C \u0437\u0430\u043D\u043E\u0432\u043E)`;
10027
+ }
10028
+ function isReliefPause(stopReason) {
10029
+ return !!stopReason && stopReason.startsWith(RELIEF_PAUSE_MARK);
10030
+ }
9932
10031
  function reliefResumeReason(mode) {
9933
10032
  return mode === "continue" ? `${RELIEF_PAUSE_MARK} \u043E\u0442\u043F\u0443\u0441\u0442\u0438\u043B\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 (--continue: \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 origin + \u0441\u0435\u0441\u0441\u0438\u044F \u0446\u0435\u043B\u044B)` : `${RELIEF_PAUSE_MARK} \u043E\u0442\u043F\u0443\u0441\u0442\u0438\u043B\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443, \u0441\u0442\u0430\u0440\u0442 \u0437\u0430\u043D\u043E\u0432\u043E (\u0441\u0435\u0441\u0441\u0438\u044F/worktree \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B)`;
9934
10033
  }
@@ -9952,6 +10051,115 @@ function pickResumeCandidate(pausedForRelief) {
9952
10051
  return candidates.reduce((oldest, r) => r.updated_at < oldest.updated_at ? r : oldest);
9953
10052
  }
9954
10053
 
10054
+ // src/reliefLoop.ts
10055
+ import { existsSync as existsSync10, readFileSync as readFileSync13, renameSync as renameSync8, writeFileSync as writeFileSync11 } from "fs";
10056
+ import { join as join14 } from "path";
10057
+ var numEnv3 = (name, def) => {
10058
+ const v = Number(process.env[name]);
10059
+ return Number.isFinite(v) && v > 0 ? v : def;
10060
+ };
10061
+ var DEFAULT_RELIEF_LOOP_CONFIG = {
10062
+ limit: numEnv3("SWARM_RELIEF_LOOP_LIMIT", 3),
10063
+ backoffMaxMs: numEnv3("SWARM_RELIEF_BACKOFF_MAX_MS", 10 * 6e4),
10064
+ seriesTtlMs: numEnv3("SWARM_RELIEF_LOOP_TTL_MS", 6 * 60 * 6e4)
10065
+ };
10066
+ function resolveReliefLoopConfig(cfg) {
10067
+ return { ...DEFAULT_RELIEF_LOOP_CONFIG, ...cfg ?? {} };
10068
+ }
10069
+ function reliefLoopKey(repo, slug) {
10070
+ return `${repo}#${slug}`;
10071
+ }
10072
+ function recordReliefLoopPause(state, key, now, delivered, cfg = DEFAULT_RELIEF_LOOP_CONFIG) {
10073
+ const prev = state[key];
10074
+ const continues = !!prev && !delivered && now - prev.lastPauseAt <= cfg.seriesTtlMs;
10075
+ const entry = continues ? { pauses: prev.pauses + 1, lastPauseAt: now, ...prev.escalated ? { escalated: true } : {} } : { pauses: 1, lastPauseAt: now };
10076
+ state[key] = entry;
10077
+ return entry;
10078
+ }
10079
+ function clearReliefLoop(state, key) {
10080
+ delete state[key];
10081
+ }
10082
+ function reliefBackoffMs(pauses, baseMs, cfg = DEFAULT_RELIEF_LOOP_CONFIG) {
10083
+ if (pauses <= 1) return baseMs;
10084
+ const grown = baseMs * 2 ** Math.min(pauses - 1, 30);
10085
+ return Math.min(grown, cfg.backoffMaxMs);
10086
+ }
10087
+ function reliefLoopTripped(entry, cfg = DEFAULT_RELIEF_LOOP_CONFIG) {
10088
+ return !!entry && entry.pauses >= cfg.limit;
10089
+ }
10090
+ function reliefLoopStatePath(home) {
10091
+ return join14(home, "relief-loop.json");
10092
+ }
10093
+ function loadReliefLoopState(home, now = Date.now(), io = {}, cfg = DEFAULT_RELIEF_LOOP_CONFIG) {
10094
+ const exists = io.exists ?? existsSync10;
10095
+ const readFile2 = io.readFile ?? ((p) => readFileSync13(p, "utf8"));
10096
+ const path = reliefLoopStatePath(home);
10097
+ if (!exists(path)) return {};
10098
+ try {
10099
+ const raw = JSON.parse(readFile2(path));
10100
+ const out = {};
10101
+ for (const [key, value] of Object.entries(raw ?? {})) {
10102
+ const v = value;
10103
+ const pauses = Number(v?.pauses);
10104
+ const lastPauseAt = Number(v?.lastPauseAt);
10105
+ if (!Number.isFinite(pauses) || pauses <= 0 || !Number.isFinite(lastPauseAt)) continue;
10106
+ if (now - lastPauseAt > cfg.seriesTtlMs) continue;
10107
+ out[key] = { pauses, lastPauseAt, ...v?.escalated ? { escalated: true } : {} };
10108
+ }
10109
+ return out;
10110
+ } catch {
10111
+ return {};
10112
+ }
10113
+ }
10114
+ function saveReliefLoopState(home, state, io = {}) {
10115
+ const path = reliefLoopStatePath(home);
10116
+ const writeFile4 = io.writeFile ?? ((p, data) => {
10117
+ writeFileSync11(`${p}.tmp`, data);
10118
+ renameSync8(`${p}.tmp`, p);
10119
+ });
10120
+ writeFile4(path, JSON.stringify(state, null, 2));
10121
+ }
10122
+ var ReliefLoopStore = class {
10123
+ constructor(home, now = Date.now(), io = {}, cfg = DEFAULT_RELIEF_LOOP_CONFIG) {
10124
+ this.home = home;
10125
+ this.io = io;
10126
+ this.cfg = cfg;
10127
+ this.state = loadReliefLoopState(home, now, io, cfg);
10128
+ }
10129
+ home;
10130
+ io;
10131
+ cfg;
10132
+ state;
10133
+ get(key) {
10134
+ return this.state[key];
10135
+ }
10136
+ /** Учесть снятие и сразу сохранить. Возвращает запись после учёта. */
10137
+ recordPause(key, now, delivered) {
10138
+ const entry = recordReliefLoopPause(this.state, key, now, delivered, this.cfg);
10139
+ this.flush();
10140
+ return entry;
10141
+ }
10142
+ /** Пометить, что человека уже позвали — чтобы не звать на каждом тике. */
10143
+ markEscalated(key) {
10144
+ const entry = this.state[key];
10145
+ if (!entry || entry.escalated) return;
10146
+ entry.escalated = true;
10147
+ this.flush();
10148
+ }
10149
+ clear(key) {
10150
+ if (!(key in this.state)) return;
10151
+ clearReliefLoop(this.state, key);
10152
+ this.flush();
10153
+ }
10154
+ flush() {
10155
+ try {
10156
+ saveReliefLoopState(this.home, this.state, this.io);
10157
+ } catch (e) {
10158
+ console.warn("[overmind] \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043F\u0435\u0442\u043B\u0438 \u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0438 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C:", e);
10159
+ }
10160
+ }
10161
+ };
10162
+
9955
10163
  // src/reliefCost.ts
9956
10164
  function median(xs) {
9957
10165
  const s = [...xs].sort((a, b) => a - b);
@@ -10067,16 +10275,16 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
10067
10275
  }
10068
10276
 
10069
10277
  // src/launcher.ts
10070
- import { spawn } from "child_process";
10071
- import { closeSync as closeSync2, existsSync as existsSync10, mkdirSync as mkdirSync8, openSync as openSync2, readdirSync as readdirSync8 } from "fs";
10278
+ import { spawn as spawn2 } from "child_process";
10279
+ import { closeSync as closeSync3, existsSync as existsSync11, mkdirSync as mkdirSync8, openSync as openSync3, readdirSync as readdirSync8 } from "fs";
10072
10280
  import { createRequire } from "module";
10073
10281
  import { homedir as homedir5 } from "os";
10074
- import { dirname as dirname3, join as join15 } from "path";
10282
+ import { dirname as dirname3, join as join16 } from "path";
10075
10283
  import { fileURLToPath as fileURLToPath2 } from "url";
10076
10284
 
10077
10285
  // src/droneHome.ts
10078
10286
  import { lstatSync, mkdirSync as mkdirSync7, readdirSync as readdirSync7, rmSync, statSync as statSync7, symlinkSync as symlinkSync2 } from "fs";
10079
- import { join as join14, relative, sep } from "path";
10287
+ import { join as join15, relative, sep } from "path";
10080
10288
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
10081
10289
  function droneHomeDeny(env = process.env) {
10082
10290
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -10100,20 +10308,20 @@ function buildDroneHome(opts) {
10100
10308
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
10101
10309
  for (const existing of readdirSync7(droneHome)) {
10102
10310
  if (wanted.has(existing)) continue;
10103
- const p = join14(droneHome, existing);
10311
+ const p = join15(droneHome, existing);
10104
10312
  try {
10105
10313
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
10106
10314
  } catch {
10107
10315
  }
10108
10316
  }
10109
10317
  for (const name of wanted) {
10110
- const link = join14(droneHome, name);
10318
+ const link2 = join15(droneHome, name);
10111
10319
  try {
10112
- lstatSync(link);
10320
+ lstatSync(link2);
10113
10321
  continue;
10114
10322
  } catch {
10115
10323
  }
10116
- const target = join14(realHome, name);
10324
+ const target = join15(realHome, name);
10117
10325
  let isDir;
10118
10326
  try {
10119
10327
  isDir = statSync7(target).isDirectory();
@@ -10121,7 +10329,7 @@ function buildDroneHome(opts) {
10121
10329
  continue;
10122
10330
  }
10123
10331
  try {
10124
- symlinkSync2(target, link, isDir ? "junction" : "file");
10332
+ symlinkSync2(target, link2, isDir ? "junction" : "file");
10125
10333
  } catch (e) {
10126
10334
  if (e.code !== "EEXIST") throw e;
10127
10335
  }
@@ -10134,7 +10342,7 @@ var REAL_PATH_ENV_PINS = [
10134
10342
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
10135
10343
  ];
10136
10344
  function realPathEnvPins(realHome) {
10137
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join14(realHome, ...segments)]));
10345
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join15(realHome, ...segments)]));
10138
10346
  }
10139
10347
  function dronePrivatePaths(realHome, swarmHome) {
10140
10348
  return [
@@ -10246,7 +10454,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
10246
10454
  const env = { ...base3 };
10247
10455
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
10248
10456
  if (isolate && !base3.TURBO_CACHE_DIR) {
10249
- env.TURBO_CACHE_DIR = join15(worktree, ".turbo", "cache");
10457
+ env.TURBO_CACHE_DIR = join16(worktree, ".turbo", "cache");
10250
10458
  }
10251
10459
  Object.assign(env, droneTestEnv(droneCap, base3));
10252
10460
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -10274,13 +10482,13 @@ var DAEMON_STATE_ENV = [
10274
10482
  "TASKLESS_TOKEN"
10275
10483
  ];
10276
10484
  function bundledRoleDir(kind) {
10277
- return join15(dirname3(fileURLToPath2(import.meta.url)), "..", "roles", kind);
10485
+ return join16(dirname3(fileURLToPath2(import.meta.url)), "..", "roles", kind);
10278
10486
  }
10279
10487
  function roleDir(worktree, kind) {
10280
- return join15(worktree, ".claude", "roles", kind);
10488
+ return join16(worktree, ".claude", "roles", kind);
10281
10489
  }
10282
10490
  function roleDirs(worktree, kind) {
10283
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync10(dir));
10491
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync11(dir));
10284
10492
  }
10285
10493
  function nextRunLogPath(runLogsDir, runLogKeys) {
10286
10494
  const dir = runLogDirForWrite(runLogsDir, runLogKeys);
@@ -10296,21 +10504,21 @@ function nextRunLogPath(runLogsDir, runLogKeys) {
10296
10504
  const m = /^(\d+)\.json/.exec(f);
10297
10505
  if (m) next = Math.max(next, Number(m[1]) + 1);
10298
10506
  }
10299
- return join15(dir, `${next}.json`);
10507
+ return join16(dir, `${next}.json`);
10300
10508
  }
10301
10509
  function openNextRunLog(runLogsDir, runLogKeys) {
10302
10510
  for (let attempt = 0; attempt < 50; attempt++) {
10303
10511
  const path = nextRunLogPath(runLogsDir, runLogKeys);
10304
10512
  let out;
10305
10513
  try {
10306
- out = openSync2(path, "wx");
10514
+ out = openSync3(path, "wx");
10307
10515
  } catch (e) {
10308
10516
  if (e.code !== "EEXIST") throw e;
10309
10517
  continue;
10310
10518
  }
10311
10519
  let err = null;
10312
10520
  try {
10313
- err = openSync2(`${path.slice(0, -".json".length)}.err.log`, "w");
10521
+ err = openSync3(`${path.slice(0, -".json".length)}.err.log`, "w");
10314
10522
  } catch {
10315
10523
  err = null;
10316
10524
  }
@@ -10379,9 +10587,9 @@ function makeLauncher(cfg) {
10379
10587
  }
10380
10588
  const realHome = cfg.droneIsolation?.realHome ?? homedir5();
10381
10589
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
10382
- if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync10(join15(cfg.droneIsolation.swarmHome, "token"))) {
10590
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync11(join16(cfg.droneIsolation.swarmHome, "token"))) {
10383
10591
  console.error(
10384
- `[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 ${join15(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.`
10592
+ `[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 ${join16(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.`
10385
10593
  );
10386
10594
  }
10387
10595
  const executor = cfg.executor ?? resolveExecutor();
@@ -10419,7 +10627,7 @@ function makeLauncher(cfg) {
10419
10627
  errFd = fds.err ?? void 0;
10420
10628
  stdio = ["ignore", outFd, errFd ?? "ignore"];
10421
10629
  }
10422
- const child = spawn(cfg.bin ?? plan.bin, plan.args, {
10630
+ const child = spawn2(cfg.bin ?? plan.bin, plan.args, {
10423
10631
  cwd: worktree,
10424
10632
  detached: true,
10425
10633
  stdio,
@@ -10430,8 +10638,8 @@ function makeLauncher(cfg) {
10430
10638
  // они про его собственное состояние, и перебивать их ройным нечем.
10431
10639
  env: { ...droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null), ...plan.env }
10432
10640
  });
10433
- if (outFd !== void 0) closeSync2(outFd);
10434
- if (errFd !== void 0) closeSync2(errFd);
10641
+ if (outFd !== void 0) closeSync3(outFd);
10642
+ if (errFd !== void 0) closeSync3(errFd);
10435
10643
  child.unref();
10436
10644
  if (child.pid == null) throw new Error("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043D\u0435\u0442 pid)");
10437
10645
  return child.pid;
@@ -10442,8 +10650,8 @@ function makeLauncher(cfg) {
10442
10650
  init_procCleanup();
10443
10651
 
10444
10652
  // src/cost.ts
10445
- import { readFileSync as readFileSync12, readdirSync as readdirSync9, renameSync as renameSync8 } from "fs";
10446
- import { join as join16 } from "path";
10653
+ import { readFileSync as readFileSync14, readdirSync as readdirSync9, renameSync as renameSync9 } from "fs";
10654
+ import { join as join17 } from "path";
10447
10655
  init_executors();
10448
10656
  var UNPROCESSED_LOG_RE = /^\d+\.json$/;
10449
10657
  function unprocessedLogFiles(dir) {
@@ -10465,10 +10673,10 @@ function accountRunCost(deps, run) {
10465
10673
  let durationMs = 0;
10466
10674
  const processed = [];
10467
10675
  for (const f of files) {
10468
- const full = join16(dir, f);
10676
+ const full = join17(dir, f);
10469
10677
  let raw = null;
10470
10678
  try {
10471
- raw = readFileSync12(full, "utf8");
10679
+ raw = readFileSync14(full, "utf8");
10472
10680
  } catch {
10473
10681
  continue;
10474
10682
  }
@@ -10494,7 +10702,7 @@ function accountRunCost(deps, run) {
10494
10702
  }
10495
10703
  for (const full of processed) {
10496
10704
  try {
10497
- renameSync8(full, `${full}.done`);
10705
+ renameSync9(full, `${full}.done`);
10498
10706
  } catch {
10499
10707
  }
10500
10708
  }
@@ -10517,8 +10725,8 @@ function makeCostSweeper(deps) {
10517
10725
  }
10518
10726
 
10519
10727
  // src/runOutcome.ts
10520
- import { readdirSync as readdirSync10, readFileSync as readFileSync13 } from "fs";
10521
- import { join as join17 } from "path";
10728
+ import { readdirSync as readdirSync10, readFileSync as readFileSync15 } from "fs";
10729
+ import { join as join18 } from "path";
10522
10730
  init_executors();
10523
10731
  function endedItself(outcome) {
10524
10732
  return outcome === "graceful" || outcome === "budget" || outcome === "aborted";
@@ -10545,7 +10753,7 @@ function readLatestResultRaw(runLogsDir, runLogKeys) {
10545
10753
  }
10546
10754
  if (!latest) return null;
10547
10755
  try {
10548
- return readFileSync13(join17(dir, latest.name), "utf8");
10756
+ return readFileSync15(join18(dir, latest.name), "utf8");
10549
10757
  } catch {
10550
10758
  return null;
10551
10759
  }
@@ -10578,28 +10786,28 @@ init_src();
10578
10786
  init_git();
10579
10787
 
10580
10788
  // src/deps.ts
10581
- import { existsSync as existsSync12, readdirSync as readdirSync12, readFileSync as readFileSync14 } from "fs";
10582
- import { join as join20 } from "path";
10789
+ import { existsSync as existsSync14, readdirSync as readdirSync12, readFileSync as readFileSync17 } from "fs";
10790
+ import { join as join22 } from "path";
10583
10791
  import { execa } from "execa";
10584
10792
 
10585
10793
  // src/prepareLock.ts
10586
10794
  import { createHash } from "crypto";
10587
- import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as statSync9, writeSync } from "fs";
10588
- import { join as join19 } from "path";
10795
+ import { closeSync as closeSync4, openSync as openSync4, rmSync as rmSync2, statSync as statSync9, writeSync } from "fs";
10796
+ import { join as join20 } from "path";
10589
10797
  import { tmpdir } from "os";
10590
10798
  var WAIT_TIMEOUT_MS = 5 * 6e4;
10591
10799
  var POLL_MS = 300;
10592
10800
  var STALE_MS = 20 * 6e4;
10593
10801
  function machineLockPath(key, dir = tmpdir()) {
10594
- return join19(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
10802
+ return join20(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
10595
10803
  }
10596
10804
  function defaultTryCreate(path, content) {
10597
10805
  try {
10598
- const fd = openSync3(path, "wx");
10806
+ const fd = openSync4(path, "wx");
10599
10807
  try {
10600
10808
  writeSync(fd, content);
10601
10809
  } finally {
10602
- closeSync3(fd);
10810
+ closeSync4(fd);
10603
10811
  }
10604
10812
  return true;
10605
10813
  } catch {
@@ -10645,6 +10853,153 @@ async function withMachineLock(key, fn, io = {}) {
10645
10853
  }
10646
10854
  }
10647
10855
 
10856
+ // src/deps.ts
10857
+ init_config();
10858
+
10859
+ // src/envCache.ts
10860
+ init_config();
10861
+ import { createHash as createHash2 } from "crypto";
10862
+ import { cp, link, lstat, mkdir as mkdir2, readdir, readlink, rename, rm as rm2, stat, symlink, utimes, writeFile as writeFile2 } from "fs/promises";
10863
+ import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
10864
+ import { join as join21 } from "path";
10865
+ function fingerprint(parts) {
10866
+ const h = createHash2("sha256");
10867
+ for (const p of parts) {
10868
+ h.update(p);
10869
+ h.update("\0");
10870
+ }
10871
+ return h.digest("hex").slice(0, 32);
10872
+ }
10873
+ async function cloneTree(src, dst) {
10874
+ await mkdir2(dst, { recursive: true });
10875
+ const entries = await readdir(src, { withFileTypes: true });
10876
+ await Promise.all(
10877
+ entries.map(async (e) => {
10878
+ const from = join21(src, e.name);
10879
+ const to = join21(dst, e.name);
10880
+ if (e.isDirectory()) return cloneTree(from, to);
10881
+ if (e.isSymbolicLink()) {
10882
+ return symlink(await readlink(from), to).catch(() => cp(from, to, { recursive: true, verbatimSymlinks: true }));
10883
+ }
10884
+ return link(from, to).catch(() => cp(from, to));
10885
+ })
10886
+ );
10887
+ }
10888
+ function cacheRoot(io) {
10889
+ return io.root ?? ENV_CACHE_DIR;
10890
+ }
10891
+ function snapshotDir(io, key) {
10892
+ return join21(cacheRoot(io), key);
10893
+ }
10894
+ function readManifest(dir) {
10895
+ try {
10896
+ const parsed = JSON.parse(readFileSync16(join21(dir, "manifest.json"), "utf8"));
10897
+ if (!parsed || typeof parsed !== "object") return null;
10898
+ const m = parsed;
10899
+ if (typeof m.key !== "string" || !Array.isArray(m.dirs)) return null;
10900
+ const dirs = m.dirs.filter((d) => typeof d === "string" && d.length > 0 && !d.startsWith("/") && !d.includes(".."));
10901
+ if (!dirs.length) return null;
10902
+ return { key: m.key, dirs, createdAt: typeof m.createdAt === "string" ? m.createdAt : "" };
10903
+ } catch {
10904
+ return null;
10905
+ }
10906
+ }
10907
+ function hasEnvSnapshot(key, io = {}) {
10908
+ const exists = io.exists ?? existsSync13;
10909
+ return exists(join21(snapshotDir(io, key), "manifest.json"));
10910
+ }
10911
+ async function restoreEnvSnapshot(worktree, key, io = {}) {
10912
+ const exists = io.exists ?? existsSync13;
10913
+ const clone = io.clone ?? cloneTree;
10914
+ const dir = snapshotDir(io, key);
10915
+ const manifest = readManifest(dir);
10916
+ if (!manifest) return null;
10917
+ const made = [];
10918
+ try {
10919
+ for (let i = 0; i < manifest.dirs.length; i++) {
10920
+ const rel = manifest.dirs[i] ?? "";
10921
+ const target = join21(worktree, rel);
10922
+ if (exists(target)) continue;
10923
+ await clone(join21(dir, "trees", String(i)), target);
10924
+ made.push(rel);
10925
+ }
10926
+ } catch {
10927
+ for (const rel of made) await rm2(join21(worktree, rel), { recursive: true, force: true }).catch(() => {
10928
+ });
10929
+ return null;
10930
+ }
10931
+ await touch(dir);
10932
+ return made;
10933
+ }
10934
+ async function touch(dir) {
10935
+ const now = /* @__PURE__ */ new Date();
10936
+ await utimes(dir, now, now).catch(() => {
10937
+ });
10938
+ }
10939
+ async function saveEnvSnapshot(worktree, key, dirs, io = {}) {
10940
+ const exists = io.exists ?? existsSync13;
10941
+ const clone = io.clone ?? cloneTree;
10942
+ if (hasEnvSnapshot(key, io)) return true;
10943
+ const present = dirs.filter((rel) => exists(join21(worktree, rel)));
10944
+ if (!present.length) return false;
10945
+ const root = cacheRoot(io);
10946
+ const tmp = join21(root, `.tmp-${key}-${process.pid}`);
10947
+ try {
10948
+ await rm2(tmp, { recursive: true, force: true });
10949
+ await mkdir2(join21(tmp, "trees"), { recursive: true });
10950
+ for (let i = 0; i < present.length; i++) await clone(join21(worktree, present[i] ?? ""), join21(tmp, "trees", String(i)));
10951
+ const manifest = { key, dirs: present, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
10952
+ await writeFile2(join21(tmp, "manifest.json"), JSON.stringify(manifest, null, 2));
10953
+ await rename(tmp, snapshotDir(io, key));
10954
+ return true;
10955
+ } catch {
10956
+ await rm2(tmp, { recursive: true, force: true }).catch(() => {
10957
+ });
10958
+ return hasEnvSnapshot(key, io);
10959
+ }
10960
+ }
10961
+ async function pruneEnvSnapshots(io = {}) {
10962
+ const root = cacheRoot(io);
10963
+ const keep = io.keep ?? ENV_CACHE_KEEP;
10964
+ let names;
10965
+ try {
10966
+ names = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
10967
+ } catch {
10968
+ return 0;
10969
+ }
10970
+ const withTime = [];
10971
+ for (const name of names) {
10972
+ const s = await stat(join21(root, name)).catch(() => null);
10973
+ if (s) withTime.push({ name, mtime: s.mtimeMs });
10974
+ }
10975
+ withTime.sort((a, b) => b.mtime - a.mtime);
10976
+ let removed = 0;
10977
+ for (const { name } of withTime.slice(Math.max(0, keep))) {
10978
+ await rm2(join21(root, name), { recursive: true, force: true }).catch(() => {
10979
+ });
10980
+ removed++;
10981
+ }
10982
+ return removed;
10983
+ }
10984
+ async function sweepEnvCacheTemp(io = {}) {
10985
+ const root = cacheRoot(io);
10986
+ let names;
10987
+ try {
10988
+ names = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory() && e.name.startsWith(".tmp-")).map((e) => e.name);
10989
+ } catch {
10990
+ return 0;
10991
+ }
10992
+ let removed = 0;
10993
+ for (const name of names) {
10994
+ const s = await lstat(join21(root, name)).catch(() => null);
10995
+ if (!s || Date.now() - s.mtimeMs < 60 * 6e4) continue;
10996
+ await rm2(join21(root, name), { recursive: true, force: true }).catch(() => {
10997
+ });
10998
+ removed++;
10999
+ }
11000
+ return removed;
11001
+ }
11002
+
10648
11003
  // src/deps.ts
10649
11004
  var PREPARE_TIMEOUT_MS = 15 * 6e4;
10650
11005
  var MAX_PLAN_COMMANDS = 12;
@@ -10665,7 +11020,7 @@ async function runCommand(cmd, worktree) {
10665
11020
  }
10666
11021
  function safeReadText(p) {
10667
11022
  try {
10668
- return readFileSync14(p, "utf8");
11023
+ return readFileSync17(p, "utf8");
10669
11024
  } catch {
10670
11025
  return null;
10671
11026
  }
@@ -10705,21 +11060,21 @@ function overridePlan(override) {
10705
11060
  };
10706
11061
  }
10707
11062
  function detectPackageManager(worktree, io = {}) {
10708
- const exists = io.exists ?? existsSync12;
11063
+ const exists = io.exists ?? existsSync14;
10709
11064
  const readText = io.readText ?? safeReadText;
10710
- const hasPkgJson = exists(join20(worktree, "package.json"));
11065
+ const hasPkgJson = exists(join22(worktree, "package.json"));
10711
11066
  if (!hasPkgJson) {
10712
- if (exists(join20(worktree, "deno.json")) || exists(join20(worktree, "deno.jsonc")) || exists(join20(worktree, "deno.lock"))) return "deno";
11067
+ if (exists(join22(worktree, "deno.json")) || exists(join22(worktree, "deno.jsonc")) || exists(join22(worktree, "deno.lock"))) return "deno";
10713
11068
  return null;
10714
11069
  }
10715
- const pkg = readJson(readText, join20(worktree, "package.json"));
11070
+ const pkg = readJson(readText, join22(worktree, "package.json"));
10716
11071
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
10717
11072
  if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
10718
- if (exists(join20(worktree, "bun.lock")) || exists(join20(worktree, "bun.lockb"))) return "bun";
10719
- if (exists(join20(worktree, "pnpm-lock.yaml"))) return "pnpm";
10720
- if (exists(join20(worktree, "yarn.lock"))) return "yarn";
10721
- if (exists(join20(worktree, "package-lock.json"))) return "npm";
10722
- if (exists(join20(worktree, "deno.lock"))) return "deno";
11073
+ if (exists(join22(worktree, "bun.lock")) || exists(join22(worktree, "bun.lockb"))) return "bun";
11074
+ if (exists(join22(worktree, "pnpm-lock.yaml"))) return "pnpm";
11075
+ if (exists(join22(worktree, "yarn.lock"))) return "yarn";
11076
+ if (exists(join22(worktree, "package-lock.json"))) return "npm";
11077
+ if (exists(join22(worktree, "deno.lock"))) return "deno";
10723
11078
  return "npm";
10724
11079
  }
10725
11080
  function jsInstallCommand(pm) {
@@ -10775,7 +11130,7 @@ var ECOSYSTEM_MARKERS = [
10775
11130
  // конфигурация — дело сборки, не подготовки
10776
11131
  ];
10777
11132
  function detectPlan(worktree, io) {
10778
- const exists = io.exists ?? existsSync12;
11133
+ const exists = io.exists ?? existsSync14;
10779
11134
  const listFiles = io.listFiles ?? safeListFiles;
10780
11135
  const pm = detectPackageManager(worktree, io);
10781
11136
  if (pm) {
@@ -10783,7 +11138,7 @@ function detectPlan(worktree, io) {
10783
11138
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
10784
11139
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
10785
11140
  // заново, но он дешёвый: тянет из того же общего кэша.
10786
- { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join20(worktree, "node_modules"), source: "detect" }
11141
+ { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join22(worktree, "node_modules"), source: "detect" }
10787
11142
  ];
10788
11143
  for (const target of workspaceBuildTargets(worktree, io)) {
10789
11144
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
@@ -10792,12 +11147,12 @@ function detectPlan(worktree, io) {
10792
11147
  return { source: "detect", ecosystem: `node/${pm}`, commands };
10793
11148
  }
10794
11149
  for (const marker of ECOSYSTEM_MARKERS) {
10795
- const hit = marker.files.some((f) => exists(join20(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
11150
+ const hit = marker.files.some((f) => exists(join22(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
10796
11151
  if (!hit) continue;
10797
11152
  let ecosystem = marker.ecosystem;
10798
11153
  let command = marker.command;
10799
11154
  for (const refine of marker.refine ?? []) {
10800
- if (!exists(join20(worktree, refine.file))) continue;
11155
+ if (!exists(join22(worktree, refine.file))) continue;
10801
11156
  ecosystem = refine.ecosystem;
10802
11157
  command = refine.command;
10803
11158
  break;
@@ -10854,12 +11209,12 @@ var CI_INSTALL_PATTERNS = [
10854
11209
  function ciWorkflowPlan(worktree, io) {
10855
11210
  const readText = io.readText ?? safeReadText;
10856
11211
  const listFiles = io.listFiles ?? safeListFiles;
10857
- const dir = join20(worktree, ".github", "workflows");
11212
+ const dir = join22(worktree, ".github", "workflows");
10858
11213
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
10859
11214
  const seen = /* @__PURE__ */ new Set();
10860
11215
  const commands = [];
10861
11216
  for (const name of files.sort()) {
10862
- const yaml = readText(join20(dir, name));
11217
+ const yaml = readText(join22(dir, name));
10863
11218
  if (yaml === null) continue;
10864
11219
  for (const raw of workflowRunLines(yaml)) {
10865
11220
  const line = raw.replace(/\s+/g, " ").trim();
@@ -10874,7 +11229,7 @@ function ciWorkflowPlan(worktree, io) {
10874
11229
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
10875
11230
  }
10876
11231
  function workspaceGlobs(worktree, readText) {
10877
- const yaml = readText(join20(worktree, "pnpm-workspace.yaml"));
11232
+ const yaml = readText(join22(worktree, "pnpm-workspace.yaml"));
10878
11233
  if (yaml !== null) {
10879
11234
  const globs = [];
10880
11235
  let inPackages = false;
@@ -10894,7 +11249,7 @@ function workspaceGlobs(worktree, readText) {
10894
11249
  }
10895
11250
  if (globs.length) return globs;
10896
11251
  }
10897
- const pkg = readJson(readText, join20(worktree, "package.json"));
11252
+ const pkg = readJson(readText, join22(worktree, "package.json"));
10898
11253
  const ws = pkg?.workspaces;
10899
11254
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
10900
11255
  return list.filter((g) => typeof g === "string");
@@ -10905,14 +11260,14 @@ function workspaceDirs(worktree, globs, io) {
10905
11260
  if (glob.startsWith("!")) continue;
10906
11261
  const star = glob.indexOf("*");
10907
11262
  if (star === -1) {
10908
- const dir = join20(worktree, glob);
10909
- if (io.exists(join20(dir, "package.json"))) dirs.add(dir);
11263
+ const dir = join22(worktree, glob);
11264
+ if (io.exists(join22(dir, "package.json"))) dirs.add(dir);
10910
11265
  continue;
10911
11266
  }
10912
- const base3 = join20(worktree, glob.slice(0, star).replace(/\/$/, ""));
11267
+ const base3 = join22(worktree, glob.slice(0, star).replace(/\/$/, ""));
10913
11268
  for (const name of io.listDirs(base3)) {
10914
- const dir = join20(base3, name);
10915
- if (io.exists(join20(dir, "package.json"))) dirs.add(dir);
11269
+ const dir = join22(base3, name);
11270
+ if (io.exists(join22(dir, "package.json"))) dirs.add(dir);
10916
11271
  }
10917
11272
  }
10918
11273
  return [...dirs];
@@ -10933,18 +11288,18 @@ function entryPaths(pkg) {
10933
11288
  return out;
10934
11289
  }
10935
11290
  function workspaceBuildTargets(worktree, io = {}) {
10936
- const exists = io.exists ?? existsSync12;
11291
+ const exists = io.exists ?? existsSync14;
10937
11292
  const readText = io.readText ?? safeReadText;
10938
11293
  const listDirs = io.listDirs ?? safeListDirs;
10939
11294
  const globs = workspaceGlobs(worktree, readText);
10940
11295
  if (!globs.length) return [];
10941
11296
  const pkgs = [];
10942
11297
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
10943
- const pkg = readJson(readText, join20(dir, "package.json"));
11298
+ const pkg = readJson(readText, join22(dir, "package.json"));
10944
11299
  if (!pkg || typeof pkg.name !== "string") continue;
10945
11300
  const scripts = pkg.scripts ?? {};
10946
11301
  const entries = entryPaths(pkg);
10947
- const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join20(dir, e)));
11302
+ const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join22(dir, e)));
10948
11303
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
10949
11304
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
10950
11305
  }
@@ -10972,7 +11327,7 @@ function devDatabaseUrl(worktree, io = {}) {
10972
11327
  const fromEnv = (io.env ?? process.env).DATABASE_URL;
10973
11328
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
10974
11329
  for (const file of [".env", ".env.example"]) {
10975
- const raw = readText(join20(worktree, file));
11330
+ const raw = readText(join22(worktree, file));
10976
11331
  if (raw === null) continue;
10977
11332
  const m = /^\s*(?:export\s+)?DATABASE_URL\s*=\s*(.*)$/m.exec(raw);
10978
11333
  const value = (m?.[1] ?? "").trim().replace(/^['"]|['"]$/g, "");
@@ -10988,11 +11343,11 @@ function isLocalDatabase(url) {
10988
11343
  }
10989
11344
  }
10990
11345
  function prismaPackages(worktree, io) {
10991
- const exists = io.exists ?? existsSync12;
11346
+ const exists = io.exists ?? existsSync14;
10992
11347
  const readText = io.readText ?? safeReadText;
10993
11348
  const listDirs = io.listDirs ?? safeListDirs;
10994
11349
  const dirs = [worktree, ...workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs })];
10995
- return dirs.filter((dir) => exists(join20(dir, "prisma", "schema.prisma")) && listDirs(join20(dir, "prisma", "migrations")).length > 0);
11350
+ return dirs.filter((dir) => exists(join22(dir, "prisma", "schema.prisma")) && listDirs(join22(dir, "prisma", "migrations")).length > 0);
10996
11351
  }
10997
11352
  function jsExecBin(pm, command) {
10998
11353
  switch (pm) {
@@ -11019,26 +11374,59 @@ function migrateCommands(worktree, pm, io) {
11019
11374
  source: "detect"
11020
11375
  }));
11021
11376
  }
11377
+ var ENV_DEFINING_FILES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb", "deno.lock", "pnpm-workspace.yaml"];
11378
+ function envCachePlan(worktree, io = {}) {
11379
+ if (!ENV_CACHE_ENABLED) return null;
11380
+ const exists = io.exists ?? existsSync14;
11381
+ const readText = io.readText ?? safeReadText;
11382
+ const listDirs = io.listDirs ?? safeListDirs;
11383
+ const pm = detectPackageManager(worktree, io);
11384
+ if (!pm) return null;
11385
+ const parts = [`pm:${pm}`];
11386
+ for (const name of ENV_DEFINING_FILES) {
11387
+ const raw = readText(join22(worktree, name));
11388
+ if (raw !== null) parts.push(`${name}
11389
+ ${raw}`);
11390
+ }
11391
+ const pkgDirs = workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs });
11392
+ const rels = pkgDirs.map((dir) => dir.slice(worktree.length + 1)).sort();
11393
+ for (const rel of ["", ...rels]) {
11394
+ const raw = readText(join22(worktree, rel, "package.json"));
11395
+ if (raw !== null) parts.push(`${rel}/package.json
11396
+ ${raw}`);
11397
+ }
11398
+ return { key: fingerprint(parts), dirs: ["node_modules", ...rels.map((rel) => join22(rel, "node_modules"))] };
11399
+ }
11022
11400
  function planPrepare(worktree, io = {}) {
11023
11401
  const plan = overridePlan(io.override) ?? detectPlan(worktree, io) ?? ciWorkflowPlan(worktree, io);
11024
11402
  if (!plan) return { source: "none", ecosystem: null, commands: [] };
11025
11403
  return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
11026
11404
  }
11027
11405
  async function ensureWorktreeDeps(worktree, io = {}) {
11028
- const exists = io.exists ?? existsSync12;
11406
+ const exists = io.exists ?? existsSync14;
11029
11407
  const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
11030
11408
  const approve = io.approve ?? (() => true);
11031
- const plan = planPrepare(worktree, io);
11409
+ const cachePlan = envCachePlan(worktree, io);
11410
+ const cacheIO = io.envCache ?? {};
11032
11411
  const report = {
11033
- ecosystem: plan.ecosystem,
11034
- source: plan.source,
11035
- outcome: plan.source === "none" ? "not-detected" : "nothing-to-do",
11412
+ ecosystem: null,
11413
+ source: "none",
11414
+ outcome: "not-detected",
11036
11415
  ran: [],
11037
11416
  failed: [],
11038
11417
  skipped: [],
11039
11418
  blocked: [],
11040
- warned: []
11419
+ warned: [],
11420
+ restored: [],
11421
+ cached: false
11041
11422
  };
11423
+ if (cachePlan && !exists(join22(worktree, "node_modules"))) {
11424
+ report.restored = await restoreEnvSnapshot(worktree, cachePlan.key, cacheIO) ?? [];
11425
+ }
11426
+ const plan = planPrepare(worktree, io);
11427
+ report.ecosystem = plan.ecosystem;
11428
+ report.source = plan.source;
11429
+ report.outcome = plan.source === "none" ? "not-detected" : "nothing-to-do";
11042
11430
  for (const cmd of plan.commands) {
11043
11431
  if (cmd.skipIfExists && exists(cmd.skipIfExists)) {
11044
11432
  report.skipped.push(cmd.label);
@@ -11060,11 +11448,20 @@ async function ensureWorktreeDeps(worktree, io = {}) {
11060
11448
  break;
11061
11449
  }
11062
11450
  }
11451
+ if (cachePlan && !report.failed.length && !report.blocked.length) {
11452
+ report.cached = await saveEnvSnapshot(worktree, cachePlan.key, cachePlan.dirs, cacheIO);
11453
+ if (report.cached) {
11454
+ await pruneEnvSnapshots(cacheIO);
11455
+ await sweepEnvCacheTemp(cacheIO);
11456
+ }
11457
+ }
11063
11458
  return report;
11064
11459
  }
11065
11460
  function depsEvents(report) {
11066
11461
  const events = [];
11067
11462
  const where = report.source === "none" ? "" : ` (${[report.ecosystem, `\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A: ${sourceLabel(report.source)}`].filter(Boolean).join(", ")})`;
11463
+ if (report.restored.length)
11464
+ events.push(`\u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u0432\u0437\u044F\u0442\u043E \u0438\u0437 \u043C\u0430\u0448\u0438\u043D\u043D\u043E\u0433\u043E \u043A\u044D\u0448\u0430, \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043D\u0435 \u043F\u043E\u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043B\u0430\u0441\u044C: ${report.restored.length} \u043A\u0430\u0442. node_modules \u2014 TASWM-519`);
11068
11465
  if (report.ran.length) events.push(`\u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 worktree${where}: ${report.ran.join(", ")} \u2014 TASWM-312`);
11069
11466
  if (report.failed.length)
11070
11467
  events.push(
@@ -11293,20 +11690,256 @@ function preStartAttempts(runs, taskSlug, opts = {}) {
11293
11690
  }
11294
11691
  return n;
11295
11692
  }
11296
- function lastPreStartAttempt(runs, taskSlug) {
11297
- return runs.filter((r) => r.task_slug === taskSlug && isPreStartFailure(r)).sort(newestFirst)[0] ?? null;
11693
+ function lastPreStartAttempt(runs, taskSlug) {
11694
+ return runs.filter((r) => r.task_slug === taskSlug && isPreStartFailure(r)).sort(newestFirst)[0] ?? null;
11695
+ }
11696
+ function unresolvedRepoReason(repo, served) {
11697
+ const raw = repo.trim();
11698
+ if (!raw || raw.startsWith("/") || raw.startsWith("~") || /^[a-z]:[\\/]/i.test(raw)) return null;
11699
+ 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)";
11700
+ 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.`;
11701
+ }
11702
+ function parkReason(attempts, reason) {
11703
+ 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}`;
11704
+ }
11705
+ function parkReasonUnretriable(reason) {
11706
+ return `${PRE_START_MARK}, \u0438 ${UNRETRIABLE_PARK_MARK} \u2014 ${SPAWN_PARK_MARK} \u0441\u0440\u0430\u0437\u0443, \u043D\u0435 \u0442\u0440\u0430\u0442\u044F ${PRE_START_ATTEMPT_LIMIT} \u043F\u043E\u043F\u044B\u0442\u043A\u0438. \u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ${reason}`;
11707
+ }
11708
+
11709
+ // package.json
11710
+ var package_default = {
11711
+ name: "@taskless-app/swarm",
11712
+ version: "0.6.33",
11713
+ type: "module",
11714
+ description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
11715
+ keywords: [
11716
+ "ai-agents",
11717
+ "coding-agents",
11718
+ "swarm",
11719
+ "orchestration",
11720
+ "autonomous-agents",
11721
+ "mcp",
11722
+ "taskless",
11723
+ "cli"
11724
+ ],
11725
+ license: "SEE LICENSE IN LICENSE",
11726
+ author: "Ilya Smurov <ilya@smurov.com>",
11727
+ homepage: "https://github.com/ilyasmurov/taskless/tree/main/packages/swarm-daemon#readme",
11728
+ repository: {
11729
+ type: "git",
11730
+ url: "git+https://github.com/ilyasmurov/taskless.git",
11731
+ directory: "packages/swarm-daemon"
11732
+ },
11733
+ engines: {
11734
+ node: ">=22"
11735
+ },
11736
+ bin: {
11737
+ swarm: "dist/cli.js"
11738
+ },
11739
+ files: [
11740
+ "dist",
11741
+ "docs/HERO.md",
11742
+ "hooks",
11743
+ "roles"
11744
+ ],
11745
+ scripts: {
11746
+ overmind: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/overmind.ts',
11747
+ swarm: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/cli.ts',
11748
+ build: "tsup",
11749
+ start: "node dist/overmind.js",
11750
+ prepublishOnly: "pnpm run build",
11751
+ test: "vitest run && SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
11752
+ "test:client": "SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
11753
+ "test:watch": "vitest",
11754
+ typecheck: "tsc -p tsconfig.json --noEmit",
11755
+ lint: "echo 'no lint in swarm-daemon yet'"
11756
+ },
11757
+ dependencies: {
11758
+ "@hono/node-server": "^2.0.12",
11759
+ "@modelcontextprotocol/sdk": "^1.29.0",
11760
+ execa: "^10.0.0",
11761
+ zod: "3.25.76"
11762
+ },
11763
+ gitHead: "7b17b06b041af953afde46fbd41fc68ebc6dc4e9"
11764
+ };
11765
+
11766
+ // src/version.ts
11767
+ var PACKAGE_VERSION = package_default.version;
11768
+ var MIN_NODE_MAJOR = Number(/(\d+)/.exec(package_default.engines?.node ?? "")?.[1] ?? 0);
11769
+
11770
+ // src/release.ts
11771
+ init_src();
11772
+ init_claudeBin();
11773
+ var SWARM_PACKAGE = SWARM_PACKAGE_NAME;
11774
+ var LATEST_TIMEOUT_MS = 4e3;
11775
+ async function fetchLatestVersion(opts = {}) {
11776
+ const fetchFn = opts.fetchFn ?? fetch;
11777
+ const timeoutMs = opts.timeoutMs ?? LATEST_TIMEOUT_MS;
11778
+ const pkg = opts.pkg ?? SWARM_PACKAGE;
11779
+ const ctrl = new AbortController();
11780
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
11781
+ try {
11782
+ const res = await fetchFn(`https://registry.npmjs.org/${pkg}/latest`, {
11783
+ signal: ctrl.signal,
11784
+ headers: { accept: "application/vnd.npm.install-v1+json, application/json" }
11785
+ });
11786
+ if (!res.ok) return null;
11787
+ const body = await res.json();
11788
+ return typeof body.version === "string" && body.version ? body.version : null;
11789
+ } catch {
11790
+ return null;
11791
+ } finally {
11792
+ clearTimeout(timer);
11793
+ }
11794
+ }
11795
+ function isOutdated(current, latest) {
11796
+ if (!latest || !current) return false;
11797
+ return compareVersionDirs(latest, current) > 0;
11798
+ }
11799
+ function outdatedLine(current, latest, updateCommand = "swarm update") {
11800
+ if (!isOutdated(current, latest)) return null;
11801
+ return `\u26A0 \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u043E\u044F \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0430: \u0443 \u0442\u0435\u0431\u044F v${current}, \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest} \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: ${updateCommand}`;
11802
+ }
11803
+
11804
+ // src/daemonVersion.ts
11805
+ function captureDaemonStart(now = () => /* @__PURE__ */ new Date()) {
11806
+ return { startedAt: now().toISOString(), version: PACKAGE_VERSION };
11807
+ }
11808
+ var DAEMON_CODE_PATHS = ["packages/swarm-daemon", "packages/swarm-core"];
11809
+ async function checkDaemonVersion(start, opts = {}) {
11810
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
11811
+ const fetchLatest = opts.latest ?? (() => fetchLatestVersion());
11812
+ const latestVersion = await fetchLatest().catch(() => null);
11813
+ return {
11814
+ startedAt: start.startedAt,
11815
+ version: start.version,
11816
+ latestVersion,
11817
+ stale: isOutdated(start.version, latestVersion),
11818
+ checkedAt: now().toISOString()
11819
+ };
11820
+ }
11821
+
11822
+ // src/escalations.ts
11823
+ var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
11824
+ var REVIEW_ESCALATION_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F";
11825
+ var REVIEW_UNRECOVERABLE_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u044C";
11826
+ function reviewUnrecoverableMark(branch) {
11827
+ return `${REVIEW_UNRECOVERABLE_MARK} (\u0432\u0435\u0442\u043A\u0430 ${branch})`;
11298
11828
  }
11299
- function unresolvedRepoReason(repo, served) {
11300
- const raw = repo.trim();
11301
- if (!raw || raw.startsWith("/") || raw.startsWith("~") || /^[a-z]:[\\/]/i.test(raw)) return null;
11302
- 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)";
11303
- 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.`;
11829
+ var REVIEW_CYCLE_ESCALATION_MARK = "\u0440\u0435-\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0446\u0438\u043A\u043B\u0438\u043B\u043E\u0441\u044C";
11830
+ var CI_WAKE_ESCALATION_MARK = "CI-\u0430\u0432\u0442\u043E\u043F\u043E\u0447\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043F\u0440\u0430\u0432\u0438\u043B\u0430\u0441\u044C";
11831
+ var CHANGES_WAKE_ESCALATION_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u044F \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0430\u0441\u044C";
11832
+ var REVIEW_BUDGET_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u043E\u0441\u044C \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442";
11833
+ function reviewBudgetMark(cycle) {
11834
+ return `${REVIEW_BUDGET_MARK} (\u043A\u0440\u0443\u0433 ${cycle})`;
11304
11835
  }
11305
- function parkReason(attempts, reason) {
11306
- 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}`;
11836
+ var SELF_REFERENCE_MARK = "PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430";
11837
+ var VERIFY_ESCALATION_MARK = "\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F: \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u044B";
11838
+ var CI_WAIT_ESCALATION_MARK = "\u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 CI \u0437\u0430\u0442\u044F\u043D\u0443\u043B\u043E\u0441\u044C";
11839
+ var RELIEF_LOOP_ESCALATION_MARK = "\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u0437\u0430\u0446\u0438\u043A\u043B\u0438\u043B\u0430\u0441\u044C";
11840
+ var ESCALATIONS = [
11841
+ {
11842
+ mark: SELF_REFERENCE_MARK,
11843
+ // Надзора тут нет намеренно: у self-reference свой канал доставки (watchdog-нотификация
11844
+ // на месте записи), и вход в NEEDS_HUMAN_MARKS задвоил бы сигнал. Ровно на этом маркере
11845
+ // wakeRun и запирал дверь: под self-reference попадает ЛЮБАЯ задача роя, трогающая
11846
+ // packages/swarm-daemon или packages/swarm-core, — вердикта не будет никогда.
11847
+ sides: ["reviewer", "author"],
11848
+ why: "PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430: \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u043D\u043E, \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442",
11849
+ stopReason: () => `PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430 (${DAEMON_CODE_PATHS.join(", ")}) \u2014 \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u043D\u043E (TASWM-270): \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0449\u0438\u0439 \u0434\u0435\u043C\u043E\u043D \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F \u043D\u0430 \u0441\u0442\u0430\u0440\u043E\u043C \u043A\u043E\u0434\u0435 \u0438 \u0441\u0432\u043E\u044E \u0436\u0435 \u043F\u0440\u0430\u0432\u043A\u0443 \u043D\u0435 \u0430\u0442\u0442\u0435\u0441\u0442\u0443\u0435\u0442. \u041D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`
11850
+ },
11851
+ {
11852
+ mark: REVIEW_ESCALATION_MARK,
11853
+ sides: ["reviewer", "author", "supervision"],
11854
+ why: "\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u043F\u043E\u043F\u044B\u0442\u043E\u043A \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u044E\u0449\u0435\u0433\u043E \u0440\u043E\u0439 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442",
11855
+ stopReason: (_run, limits) => `\u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u044B (${limits.reviewSpawn}) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`
11856
+ },
11857
+ {
11858
+ mark: REVIEW_CYCLE_ESCALATION_MARK,
11859
+ sides: ["reviewer", "author", "supervision"],
11860
+ why: "\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0446\u0438\u043A\u043B\u043E\u0432 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u2014 \u0430\u0432\u0442\u043E\u0441\u043F\u0430\u0432\u043D\u043E\u0432 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435\u0442",
11861
+ stopReason: (_run, limits) => `\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0446\u0438\u043A\u043B\u043E\u0432 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E (${limits.reviewCycle}) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`
11862
+ },
11863
+ {
11864
+ mark: REVIEW_BUDGET_MARK,
11865
+ sides: ["reviewer", "author", "supervision"],
11866
+ why: "\u0440\u0435\u0432\u044C\u044E \u044D\u0442\u043E\u0433\u043E \u043A\u0440\u0443\u0433\u0430 \u0441\u0433\u043E\u0440\u0435\u043B\u043E \u043D\u0430 \u043F\u043E\u0442\u043E\u043B\u043A\u0435 \u0442\u0440\u0430\u0442 \u2014 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0430\u044F \u043F\u043E\u043F\u044B\u0442\u043A\u0430 \u0443\u043F\u0440\u0451\u0442\u0441\u044F \u0432 \u0442\u043E\u0442 \u0436\u0435",
11867
+ scoped: (run) => reviewBudgetMark(run.review_cycle ?? 0),
11868
+ stopReason: () => "\u0440\u0435\u0432\u044C\u044E \u044D\u0442\u043E\u0433\u043E \u043A\u0440\u0443\u0433\u0430 \u0441\u0433\u043E\u0440\u0435\u043B\u043E \u043D\u0430 \u043F\u043E\u0442\u043E\u043B\u043A\u0435 \u0442\u0440\u0430\u0442 \u2014 \u043F\u043E\u0432\u0442\u043E\u0440 \u0443\u043F\u0440\u0451\u0442\u0441\u044F \u0432 \u0442\u043E\u0442 \u0436\u0435, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A"
11869
+ },
11870
+ {
11871
+ mark: REVIEW_UNRECOVERABLE_MARK,
11872
+ sides: ["reviewer", "author", "supervision"],
11873
+ why: "\u0432\u0435\u0442\u043A\u0438 PR \u043D\u0435\u0442 \u043D\u0430 origin \u2014 \u043F\u043E\u0432\u0442\u043E\u0440 \u0441\u0434\u0435\u043B\u0430\u0435\u0442 \u0442\u043E\u0442 \u0436\u0435 git-\u0432\u044B\u0437\u043E\u0432 \u0441 \u0442\u0435\u043C \u0436\u0435 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u043C",
11874
+ scoped: (run) => reviewUnrecoverableMark(run.branch || run.task_slug),
11875
+ stopReason: () => "\u0432\u0435\u0442\u043A\u0438 PR \u043D\u0435\u0442 \u043D\u0430 origin \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A"
11876
+ },
11877
+ {
11878
+ mark: CHANGES_WAKE_ESCALATION_MARK,
11879
+ // Проверяющего этот маркер не касается: вердикт-то есть, не поднимается АВТОР.
11880
+ sides: ["author", "supervision"],
11881
+ why: "\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0431\u0435\u0441\u043F\u043B\u043E\u0434\u043D\u044B\u0445 \u0444\u0438\u043A\u0441-\u043F\u043E\u0431\u0443\u0434\u043E\u043A \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u2014 \u0430\u0432\u0442\u043E\u0440\u0430 \u0440\u043E\u0439 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0431\u0443\u0434\u0438\u0442",
11882
+ scoped: (run) => `${CHANGES_WAKE_ESCALATION_MARK} (\u043A\u0440\u0443\u0433 ${run.review_cycle ?? 0})`
11883
+ },
11884
+ {
11885
+ mark: RESOLVE_ESCALATION_MARK,
11886
+ sides: ["author", "supervision"],
11887
+ why: "\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0430\u0432\u0442\u043E-\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440\u043E\u0432 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u2014 \u0434\u0430\u043B\u044C\u0448\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0440\u0430\u0437\u0432\u043E\u0434\u0438\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A"
11888
+ },
11889
+ {
11890
+ mark: CI_WAKE_ESCALATION_MARK,
11891
+ sides: ["author", "supervision"],
11892
+ why: "\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u043F\u043E\u0431\u0443\u0434\u043E\u043A \u043F\u043E \u043A\u0440\u0430\u0441\u043D\u043E\u043C\u0443 CI \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u2014 \u0440\u043E\u0439 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u043B \u0447\u0438\u043D\u0438\u0442\u044C CI \u0441\u0430\u043C"
11893
+ },
11894
+ {
11895
+ mark: VERIFY_ESCALATION_MARK,
11896
+ // Надзора нет: верификация — не ревью, её отсутствие не держит PR (TASWM-344), и в
11897
+ // NEEDS_HUMAN_MARKS этот маркер никогда не входил. Автору дверь открывает: вердикта
11898
+ // верификации у прогона не будет, поднять его может только оператор.
11899
+ sides: ["author"],
11900
+ why: "\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u043F\u043E\u043F\u044B\u0442\u043E\u043A \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u0443 PR \u043D\u0435 \u0431\u0443\u0434\u0435\u0442"
11901
+ },
11902
+ {
11903
+ mark: RELIEF_LOOP_ESCALATION_MARK,
11904
+ // Проверяющего это не касается: работы, которую проверять, ещё нет. Автор — да, и это
11905
+ // главное: сторож прогресса (watchdog.decideFreshAction) обязан УВИДЕТЬ, что рой уже
11906
+ // сдался, иначе он поднимет свежего актора в ту же перегрузку — буквально следующий круг
11907
+ // петли, ровно то, что он и делал 27.08. Ручную побудку `swarm wake` этот маркер, как и
11908
+ // остальные author-маркеры, не запрещает — он открывает ей дверь (см. stoppedForHuman).
11909
+ //
11910
+ // Надзора НЕТ намеренно: маркер ложится на прогон в статусе `stalled`, а надзорная труба
11911
+ // (buildEscalationCandidates) ходит только по `listRuns("review")` — запись там была бы
11912
+ // мёртвой. Человека по такому прогону зовёт детектор run_stalled (anomalies.ts), причём
11913
+ // повторный заход по той же задаче он поднимает до уровня «нужен оператор» сам.
11914
+ sides: ["author"],
11915
+ why: "\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u0441\u043D\u044F\u043B\u0430 \u043F\u0440\u043E\u0433\u043E\u043D N \u0440\u0430\u0437 \u043F\u043E\u0434\u0440\u044F\u0434 \u0431\u0435\u0437 \u0435\u0434\u0438\u043D\u043E\u0439 \u0441\u0434\u0430\u0447\u0438 \u2014 \u0440\u043E\u0439 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u043B \u0435\u0433\u043E \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0442\u044C, \u0437\u043E\u0432\u0451\u0442 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0447\u0435\u0440\u0435\u0437 run_stalled"
11916
+ },
11917
+ {
11918
+ mark: CI_WAIT_ESCALATION_MARK,
11919
+ // Сторон НЕТ — и это решение, а не пропуск. Маркер лежит на прогоне РЕВЬЮЕРА (paused,
11920
+ // AWAITING_CI_REASON), а не на прогоне автора, и человека зовёт сам:
11921
+ // escalateStaleCiWaits пишет аномалию и дёргает watchdog.notify на месте. Вписать его в
11922
+ // author/supervision значило бы задвоить сигнал и поменять поведение — чего задача
11923
+ // прямо просила не делать.
11924
+ sides: [],
11925
+ why: "\u0440\u043E\u0439 \u0417\u041E\u0412\u0401\u0422 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430, \u043D\u043E \u0441\u0430\u043C \u0435\u0449\u0451 \u0436\u0434\u0451\u0442: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043D\u0435 \u0441\u0434\u0430\u043B\u0441\u044F, \u0430 \u043D\u043E\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E \u0448\u043B\u0451\u0442 \u0441\u0432\u043E\u0438\u043C \u043A\u0430\u043D\u0430\u043B\u043E\u043C"
11926
+ }
11927
+ ];
11928
+ function escalationText(entry, run, side) {
11929
+ return side === "supervision" ? entry.mark : entry.scoped?.(run) ?? entry.mark;
11307
11930
  }
11308
- function parkReasonUnretriable(reason) {
11309
- return `${PRE_START_MARK}, \u0438 ${UNRETRIABLE_PARK_MARK} \u2014 ${SPAWN_PARK_MARK} \u0441\u0440\u0430\u0437\u0443, \u043D\u0435 \u0442\u0440\u0430\u0442\u044F ${PRE_START_ATTEMPT_LIMIT} \u043F\u043E\u043F\u044B\u0442\u043A\u0438. \u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ${reason}`;
11931
+ function escalationsFor(side) {
11932
+ return ESCALATIONS.filter((e) => e.sides.includes(side));
11933
+ }
11934
+ function isEscalationText(text, run, side) {
11935
+ return escalationsFor(side).some((e) => text.includes(escalationText(e, run, side)));
11936
+ }
11937
+ function findEscalation(texts, run, side) {
11938
+ for (const entry of escalationsFor(side)) {
11939
+ const needle = escalationText(entry, run, side);
11940
+ if (texts.some((t) => t.includes(needle))) return entry;
11941
+ }
11942
+ return null;
11310
11943
  }
11311
11944
 
11312
11945
  // src/worktreeReap.ts
@@ -11384,7 +12017,12 @@ import { basename as basename3 } from "path";
11384
12017
  async function runGh(args, timeoutMs, cwd) {
11385
12018
  const label2 = callLabel("gh", args) + (cwd ? ` (repo ${basename3(cwd)})` : "");
11386
12019
  const child = cwd ? $3({ cwd, timeout: timeoutMs })`gh ${args}` : $3({ timeout: timeoutMs })`gh ${args}`;
11387
- return (await withHardTimeout(child, "gh", label2, timeoutMs)).stdout;
12020
+ try {
12021
+ return (await withHardTimeout(child, "gh", label2, timeoutMs)).stdout;
12022
+ } catch (e) {
12023
+ noteCallFailure("gh", label2, e);
12024
+ throw e;
12025
+ }
11388
12026
  }
11389
12027
  function ghRunner(timeoutMs = GH_CALL_TIMEOUT_MS) {
11390
12028
  return (args) => runGh(args, timeoutMs);
@@ -11598,36 +12236,59 @@ var DENIED_STDERR = /(?:^|\D)403(?:\D|$)|not accessible by integration|must have
11598
12236
  function firstLine(s) {
11599
12237
  return s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "gh \u0432\u0435\u0440\u043D\u0443\u043B 403";
11600
12238
  }
12239
+ var GhCallFailedError = class extends Error {
12240
+ failure;
12241
+ constructor(call, failure) {
12242
+ super(`${call}: ${failure.reason}${failure.detail ? ` \u2014 ${failure.detail}` : ""}`);
12243
+ this.name = "GhCallFailedError";
12244
+ this.failure = failure;
12245
+ }
12246
+ };
11601
12247
  var realChecksRunner = async (pr) => {
11602
- const r = await withHardTimeout(
11603
- $3({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`,
11604
- "gh",
11605
- callLabel("gh", ["pr", "checks", pr]),
11606
- 15e3
11607
- );
12248
+ const label2 = callLabel("gh", ["pr", "checks", pr]);
12249
+ const r = await withHardTimeout($3({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`, "gh", label2, 15e3);
11608
12250
  const stdout = r.stdout ?? "";
11609
12251
  const stderr = r.stderr ?? "";
12252
+ if (isTimedOut(r)) throw new GhCallFailedError(label2, classifyCallFailure(r));
11610
12253
  if (!stdout.trim() && NO_CHECKS_STDERR.test(stderr)) return "[]";
11611
12254
  if (!stdout.trim() && DENIED_STDERR.test(stderr)) throw new CiAccessDeniedError(firstLine(stderr));
12255
+ if (!stdout.trim()) throw new GhCallFailedError(label2, noteCallFailure("gh", label2, r));
11612
12256
  return stdout;
11613
12257
  };
11614
12258
  async function prCheckRows(pr, run) {
12259
+ let out;
11615
12260
  try {
11616
- const out = await run(pr);
11617
- const rows = JSON.parse(out);
11618
- if (!Array.isArray(rows)) return null;
11619
- return rows.map((r) => ({ name: r.name ?? "?", bucket: (r.bucket ?? "").toLowerCase(), link: r.link ?? "" }));
12261
+ out = await run(pr);
11620
12262
  } catch (e) {
11621
12263
  if (e instanceof CiAccessDeniedError) return { denied: e.detail };
11622
- return null;
12264
+ return { unavailable: e instanceof GhCallFailedError ? e.failure : classifyCallFailure(e) };
12265
+ }
12266
+ let parsed;
12267
+ try {
12268
+ parsed = JSON.parse(out);
12269
+ } catch {
12270
+ return { unavailable: { reason: "unreadable", detail: headOfOutput(out) } };
11623
12271
  }
12272
+ if (!Array.isArray(parsed)) return { unavailable: { reason: "unreadable", detail: "\u043E\u0442\u0432\u0435\u0442 gh \u2014 \u043D\u0435 \u0441\u043F\u0438\u0441\u043E\u043A \u0447\u0435\u043A\u043E\u0432" } };
12273
+ return parsed.map((r) => ({
12274
+ name: r.name ?? "?",
12275
+ bucket: (r.bucket ?? "").toLowerCase(),
12276
+ link: r.link ?? ""
12277
+ }));
12278
+ }
12279
+ function headOfOutput(out) {
12280
+ const head = firstLine(out).slice(0, 120);
12281
+ return head === "gh \u0432\u0435\u0440\u043D\u0443\u043B 403" ? "\u043F\u0443\u0441\u0442\u043E" : head;
11624
12282
  }
11625
12283
  function isDenied(rows) {
11626
- return rows != null && !Array.isArray(rows);
12284
+ return !Array.isArray(rows) && "denied" in rows;
12285
+ }
12286
+ function isUnavailable(rows) {
12287
+ return !Array.isArray(rows) && "unavailable" in rows;
11627
12288
  }
11628
12289
  async function ghChecks(pr, run = realChecksRunner) {
11629
12290
  const rows = await prCheckRows(pr, run);
11630
- if (!rows || isDenied(rows) || rows.length === 0) return null;
12291
+ if (isDenied(rows) || isUnavailable(rows) || rows.length === 0) return null;
11631
12292
  let pass = 0, fail = 0, pending2 = 0;
11632
12293
  for (const r of rows) {
11633
12294
  if (r.bucket === "pass") pass++;
@@ -11651,13 +12312,16 @@ async function ghChecks(pr, run = realChecksRunner) {
11651
12312
  }
11652
12313
  return { bucket, text, tone, total, pass, fail, pending: pending2 };
11653
12314
  }
11654
- function runIdFromLink(link) {
11655
- const m = link.match(/\/actions\/runs\/(\d+)/);
12315
+ function runIdFromLink(link2) {
12316
+ const m = link2.match(/\/actions\/runs\/(\d+)/);
11656
12317
  return m ? m[1] : null;
11657
12318
  }
11658
12319
  async function prCiStatus(pr, run = realChecksRunner) {
11659
12320
  const rows = await prCheckRows(pr, run);
11660
- if (!rows) return null;
12321
+ if (isUnavailable(rows)) {
12322
+ const f = rows.unavailable;
12323
+ return { bucket: "unavailable", reason: f.reason, failing: [], failingRunIds: [], total: 0, pass: 0, pending: 0, ...f.detail ? { detail: f.detail } : {} };
12324
+ }
11661
12325
  if (isDenied(rows)) return { bucket: "denied", failing: [], failingRunIds: [], total: 0, pass: 0, pending: 0, detail: rows.denied };
11662
12326
  if (rows.length === 0) return { bucket: "none", failing: [], failingRunIds: [], total: 0, pass: 0, pending: 0 };
11663
12327
  const total = rows.length;
@@ -11713,7 +12377,7 @@ init_git();
11713
12377
 
11714
12378
  // src/diff.ts
11715
12379
  import { statSync as statSync10 } from "fs";
11716
- import { join as join25 } from "path";
12380
+ import { join as join27 } from "path";
11717
12381
  import { $ as $4 } from "execa";
11718
12382
  var MAX_FILES = 200;
11719
12383
  var MAX_LINES_PER_FILE = 1500;
@@ -11881,7 +12545,7 @@ async function collectRunDiff(worktree, opts = {}) {
11881
12545
  files.push(pf);
11882
12546
  }
11883
12547
  }
11884
- files.sort((a, b) => mtime(join25(worktree, b.path)) - mtime(join25(worktree, a.path)));
12548
+ files.sort((a, b) => mtime(join27(worktree, b.path)) - mtime(join27(worktree, a.path)));
11885
12549
  const truncatedFiles = files.length > MAX_FILES;
11886
12550
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
11887
12551
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -12714,7 +13378,7 @@ ${report}${shots.warning}`,
12714
13378
  const myKind = opts.runKind ?? (await store.myRun().catch(() => null))?.kind;
12715
13379
  if (myKind === "hero") {
12716
13380
  const taskless = opts.taskless ?? null;
12717
- const errText2 = (text) => ({ content: [{ type: "text", text }], isError: true });
13381
+ const errText3 = (text) => ({ content: [{ type: "text", text }], isError: true });
12718
13382
  let heroCache = null;
12719
13383
  const heroRun = async () => heroCache ??= await store.myRun();
12720
13384
  let epicCache;
@@ -12732,14 +13396,14 @@ ${report}${shots.warning}`,
12732
13396
  estimate_hours: z.number().positive().describe("\u043E\u0446\u0435\u043D\u043A\u0430 \u0432 \u0447\u0430\u0441\u0430\u0445 (\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E)")
12733
13397
  },
12734
13398
  guard(async ({ title, description_md, estimate_hours }) => {
12735
- if (!taskless) return errText2("Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u0442\u044C.");
13399
+ if (!taskless) return errText3("Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u0442\u044C.");
12736
13400
  const hero = await heroRun();
12737
- if (!hero) return errText2("\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u0442\u0432\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u0442\u044C, \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.");
13401
+ if (!hero) return errText3("\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u0442\u0432\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u0442\u044C, \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.");
12738
13402
  const ep = await epic(hero.task_slug);
12739
- if (!ep) return errText2(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u044D\u043F\u0438\u043A ${hero.task_slug} \u0438\u0437 Taskless \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430.`);
12740
- if (!ep.projectId) return errText2(`\u044D\u043F\u0438\u043A ${hero.task_slug} \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F (\u043D\u0443\u0436\u0435\u043D projectId).`);
13403
+ if (!ep) return errText3(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u044D\u043F\u0438\u043A ${hero.task_slug} \u0438\u0437 Taskless \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430.`);
13404
+ if (!ep.projectId) return errText3(`\u044D\u043F\u0438\u043A ${hero.task_slug} \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F (\u043D\u0443\u0436\u0435\u043D projectId).`);
12741
13405
  const statusId = await taskless.backlogStatusId();
12742
- if (!statusId) return errText2("\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0441\u0442\u0430\u0442\u0443\u0441 \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u0438 backlog \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430 \u2014 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u044E (\u0440\u0438\u0441\u043A \u043F\u043E\u043F\u0430\u0441\u0442\u044C \u0441\u0440\u0430\u0437\u0443 \u0432 \xAB\u0421\u0434\u0435\u043B\u0430\u0442\u044C\xBB).");
13406
+ if (!statusId) return errText3("\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0441\u0442\u0430\u0442\u0443\u0441 \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u0438 backlog \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430 \u2014 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u044E (\u0440\u0438\u0441\u043A \u043F\u043E\u043F\u0430\u0441\u0442\u044C \u0441\u0440\u0430\u0437\u0443 \u0432 \xAB\u0421\u0434\u0435\u043B\u0430\u0442\u044C\xBB).");
12743
13407
  const footer = `
12744
13408
 
12745
13409
  ---
@@ -12753,7 +13417,7 @@ ${report}${shots.warning}`,
12753
13417
  projectId: ep.projectId
12754
13418
  // TASWM-138: наследуем проект эпика — иначе сирота не claimable
12755
13419
  });
12756
- if (!created) return errText2("Taskless \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043B \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 (\u0441\u043C. \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430).");
13420
+ if (!created) return errText3("Taskless \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043B \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 (\u0441\u043C. \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430).");
12757
13421
  await store.addEvent(null, "step", `\u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0430: ${created.slug}`);
12758
13422
  return { content: [{ type: "text", text: created.slug }] };
12759
13423
  })
@@ -12767,18 +13431,18 @@ ${report}${shots.warning}`,
12767
13431
  after: z.array(z.string()).optional().describe("\u0441\u043B\u0430\u0433\u0438 \u0437\u0430\u0434\u0430\u0447, \u043F\u043E\u0441\u043B\u0435 done \u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C (\u043E\u043F\u0446\u0438\u043E\u043D\u0430\u043B\u044C\u043D\u043E)")
12768
13432
  },
12769
13433
  guard(async ({ slug, area, after }) => {
12770
- if (!taskless) return errText2("Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u0434\u0438\u0441\u043F\u0430\u0442\u0447 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u0435\u043D.");
13434
+ if (!taskless) return errText3("Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u0434\u0438\u0441\u043F\u0430\u0442\u0447 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u0435\u043D.");
12771
13435
  const hero = await heroRun();
12772
- if (!hero) return errText2("\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u0442\u0432\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u2014 \u0434\u0438\u0441\u043F\u0430\u0442\u0447 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u0435\u043D, \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.");
13436
+ if (!hero) return errText3("\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u0442\u0432\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u2014 \u0434\u0438\u0441\u043F\u0430\u0442\u0447 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u0435\u043D, \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.");
12773
13437
  const read = await taskless.getTaskDetailed(slug);
12774
- if (!read.ok) return errText2(`\u043D\u0435 \u0441\u043C\u043E\u0433 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 ${slug}: ${describeToolFailure(read.failure)}.`);
13438
+ if (!read.ok) return errText3(`\u043D\u0435 \u0441\u043C\u043E\u0433 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 ${slug}: ${describeToolFailure(read.failure)}.`);
12775
13439
  const task = read.task;
12776
13440
  const ep = await epic(hero.task_slug);
12777
13441
  if (!ep || task.parentId !== ep.id) {
12778
- return errText2(`${slug} \u2014 \u043D\u0435 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 \u0442\u0432\u043E\u0435\u0433\u043E \u044D\u043F\u0438\u043A\u0430 ${hero.task_slug}. \u0414\u0438\u0441\u043F\u0430\u0442\u0447\u0438\u0442\u044C \u043C\u043E\u0436\u043D\u043E \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u0432\u043E\u0438 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0438.`);
13442
+ return errText3(`${slug} \u2014 \u043D\u0435 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 \u0442\u0432\u043E\u0435\u0433\u043E \u044D\u043F\u0438\u043A\u0430 ${hero.task_slug}. \u0414\u0438\u0441\u043F\u0430\u0442\u0447\u0438\u0442\u044C \u043C\u043E\u0436\u043D\u043E \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u0432\u043E\u0438 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0438.`);
12779
13443
  }
12780
13444
  if (task.statusCategory !== "unstarted") {
12781
- return errText2(
13445
+ return errText3(
12782
13446
  `\u0436\u0434\u0438 \u0430\u043F\u043F\u0440\u0443\u0432\u0430 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430: \u043F\u0435\u0440\u0435\u043D\u043E\u0441 \u0432 \xAB\u0421\u0434\u0435\u043B\u0430\u0442\u044C\xBB. ${slug} \u0441\u0435\u0439\u0447\u0430\u0441 \u0432 \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u0438 \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \xAB${task.statusCategory ?? "?"}\xBB, \u0434\u0438\u0441\u043F\u0430\u0442\u0447 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F unstarted (\u043E\u0434\u043E\u0431\u0440\u0435\u043D\u043D\u044B\u0445).`
12783
13447
  );
12784
13448
  }
@@ -12800,7 +13464,7 @@ ${report}${shots.warning}`,
12800
13464
  {},
12801
13465
  guard(async () => {
12802
13466
  const hero = await heroRun();
12803
- if (!hero) return errText2("\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u0442\u0432\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u2014 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043E\u0442\u0440\u044F\u0434\u0430 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435 \u0441\u043E\u0431\u0440\u0430\u0442\u044C, \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.");
13467
+ if (!hero) return errText3("\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u0442\u0432\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u2014 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043E\u0442\u0440\u044F\u0434\u0430 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435 \u0441\u043E\u0431\u0440\u0430\u0442\u044C, \u043F\u043E\u0432\u0442\u043E\u0440\u0438 \u043F\u043E\u0437\u0436\u0435.");
12804
13468
  const squad = await store.squadRuns();
12805
13469
  const qByRun = /* @__PURE__ */ new Map();
12806
13470
  for (const q of await store.openQuestionsFor()) {
@@ -12830,9 +13494,9 @@ ${report}${shots.warning}`,
12830
13494
  },
12831
13495
  guard(async ({ question_id, text }) => {
12832
13496
  const mine = (await store.openQuestionsFor()).some((q) => q.id === question_id);
12833
- if (!mine) return errText2(`\u0432\u043E\u043F\u0440\u043E\u0441 #${question_id} \u043D\u0435 \u0438\u0437 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0442\u0432\u043E\u0435\u0433\u043E \u043E\u0442\u0440\u044F\u0434\u0430 \u2014 \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F.`);
13497
+ if (!mine) return errText3(`\u0432\u043E\u043F\u0440\u043E\u0441 #${question_id} \u043D\u0435 \u0438\u0437 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0442\u0432\u043E\u0435\u0433\u043E \u043E\u0442\u0440\u044F\u0434\u0430 \u2014 \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F.`);
12834
13498
  const answered = await store.answer(question_id, text, "hero");
12835
- if (!answered || answered.status !== "answered") return errText2(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0430 \u0432\u043E\u043F\u0440\u043E\u0441 #${question_id}.`);
13499
+ if (!answered || answered.status !== "answered") return errText3(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0430 \u0432\u043E\u043F\u0440\u043E\u0441 #${question_id}.`);
12836
13500
  return { content: [{ type: "text", text: `\u043E\u0442\u0432\u0435\u0442 \u0434\u0440\u043E\u043D\u0443 (\u0432\u043E\u043F\u0440\u043E\u0441 #${question_id}) \u0437\u0430\u043F\u0438\u0441\u0430\u043D, \u0434\u0440\u043E\u043D \u0440\u0430\u0437\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D` }] };
12837
13501
  })
12838
13502
  );
@@ -12906,7 +13570,7 @@ async function runDroneServer(runId) {
12906
13570
 
12907
13571
  // src/droneBridge.ts
12908
13572
  init_src();
12909
- import { createHash as createHash3 } from "crypto";
13573
+ import { createHash as createHash4 } from "crypto";
12910
13574
 
12911
13575
  // src/anomalies.ts
12912
13576
  init_src();
@@ -13437,7 +14101,7 @@ function questionKey(runId, text) {
13437
14101
  return `${runId} ${text.trim().replace(/\s+/g, " ")}`;
13438
14102
  }
13439
14103
  function noteKey(runId, text) {
13440
- return `note:${runId}:${createHash3("sha1").update(questionKey(runId, text)).digest("hex").slice(0, 16)}`;
14104
+ return `note:${runId}:${createHash4("sha1").update(questionKey(runId, text)).digest("hex").slice(0, 16)}`;
13441
14105
  }
13442
14106
  async function syncDroneChannel(opts) {
13443
14107
  const result = { imported: [], answered: [], notes: [] };
@@ -13663,42 +14327,6 @@ init_authState();
13663
14327
 
13664
14328
  // src/swarmStatus.ts
13665
14329
  init_linkFailure();
13666
-
13667
- // src/release.ts
13668
- init_src();
13669
- init_claudeBin();
13670
- var SWARM_PACKAGE = SWARM_PACKAGE_NAME;
13671
- var LATEST_TIMEOUT_MS = 4e3;
13672
- async function fetchLatestVersion(opts = {}) {
13673
- const fetchFn = opts.fetchFn ?? fetch;
13674
- const timeoutMs = opts.timeoutMs ?? LATEST_TIMEOUT_MS;
13675
- const pkg = opts.pkg ?? SWARM_PACKAGE;
13676
- const ctrl = new AbortController();
13677
- const timer = setTimeout(() => ctrl.abort(), timeoutMs);
13678
- try {
13679
- const res = await fetchFn(`https://registry.npmjs.org/${pkg}/latest`, {
13680
- signal: ctrl.signal,
13681
- headers: { accept: "application/vnd.npm.install-v1+json, application/json" }
13682
- });
13683
- if (!res.ok) return null;
13684
- const body = await res.json();
13685
- return typeof body.version === "string" && body.version ? body.version : null;
13686
- } catch {
13687
- return null;
13688
- } finally {
13689
- clearTimeout(timer);
13690
- }
13691
- }
13692
- function isOutdated(current, latest) {
13693
- if (!latest || !current) return false;
13694
- return compareVersionDirs(latest, current) > 0;
13695
- }
13696
- function outdatedLine(current, latest, updateCommand = "swarm update") {
13697
- if (!isOutdated(current, latest)) return null;
13698
- return `\u26A0 \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u043E\u044F \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0430: \u0443 \u0442\u0435\u0431\u044F v${current}, \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest} \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: ${updateCommand}`;
13699
- }
13700
-
13701
- // src/swarmStatus.ts
13702
14330
  init_src();
13703
14331
  function classifyLinkError(e) {
13704
14332
  const { kind, detail } = explainLinkFailure(e);
@@ -14000,22 +14628,22 @@ function makeMergeGateParts(deps) {
14000
14628
  }
14001
14629
 
14002
14630
  // src/envGate.ts
14003
- import { existsSync as existsSync15 } from "fs";
14004
- import { join as join27 } from "path";
14631
+ import { existsSync as existsSync17 } from "fs";
14632
+ import { join as join29 } from "path";
14005
14633
  import { createConnection } from "net";
14006
14634
  import { execa as execa2 } from "execa";
14007
14635
 
14008
14636
  // src/platform.ts
14009
- import { existsSync as existsSync14, readFileSync as readFileSync18 } from "fs";
14010
- import { delimiter, join as join26 } from "path";
14637
+ import { existsSync as existsSync16, readFileSync as readFileSync21 } from "fs";
14638
+ import { delimiter, join as join28 } from "path";
14011
14639
  function resolveCommandPath(name, env = process.env, platform = process.platform) {
14012
- if (name.includes("/") || name.includes("\\")) return existsSync14(name) ? name : null;
14640
+ if (name.includes("/") || name.includes("\\")) return existsSync16(name) ? name : null;
14013
14641
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
14014
14642
  const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
14015
14643
  for (const dir of dirs) {
14016
14644
  for (const ext of exts) {
14017
- const candidate = join26(dir, name + ext);
14018
- if (existsSync14(candidate)) return candidate;
14645
+ const candidate = join28(dir, name + ext);
14646
+ if (existsSync16(candidate)) return candidate;
14019
14647
  }
14020
14648
  }
14021
14649
  return null;
@@ -14098,8 +14726,8 @@ var COMPOSE_MARKERS = [
14098
14726
  "compose.yaml"
14099
14727
  ];
14100
14728
  function detectRequirements(repo, io = {}) {
14101
- const exists = io.exists ?? existsSync15;
14102
- const marker = COMPOSE_MARKERS.find((f) => exists(join27(repo, f)));
14729
+ const exists = io.exists ?? existsSync17;
14730
+ const marker = COMPOSE_MARKERS.find((f) => exists(join29(repo, f)));
14103
14731
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
14104
14732
  }
14105
14733
  function resolveRequirements(repo, declared, io = {}) {
@@ -14233,69 +14861,6 @@ init_src();
14233
14861
  import { randomBytes as randomBytes3 } from "crypto";
14234
14862
  import { basename as basename5 } from "path";
14235
14863
  import { serve as serve2 } from "@hono/node-server";
14236
-
14237
- // package.json
14238
- var package_default = {
14239
- name: "@taskless-app/swarm",
14240
- version: "0.6.31",
14241
- type: "module",
14242
- description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
14243
- keywords: [
14244
- "ai-agents",
14245
- "coding-agents",
14246
- "swarm",
14247
- "orchestration",
14248
- "autonomous-agents",
14249
- "mcp",
14250
- "taskless",
14251
- "cli"
14252
- ],
14253
- license: "SEE LICENSE IN LICENSE",
14254
- author: "Ilya Smurov <ilya@smurov.com>",
14255
- homepage: "https://github.com/ilyasmurov/taskless/tree/main/packages/swarm-daemon#readme",
14256
- repository: {
14257
- type: "git",
14258
- url: "git+https://github.com/ilyasmurov/taskless.git",
14259
- directory: "packages/swarm-daemon"
14260
- },
14261
- engines: {
14262
- node: ">=22"
14263
- },
14264
- bin: {
14265
- swarm: "dist/cli.js"
14266
- },
14267
- files: [
14268
- "dist",
14269
- "docs/HERO.md",
14270
- "hooks",
14271
- "roles"
14272
- ],
14273
- scripts: {
14274
- overmind: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/overmind.ts',
14275
- swarm: 'NODE_OPTIONS="${NODE_OPTIONS:-} --conditions=development" tsx src/cli.ts',
14276
- build: "tsup",
14277
- start: "node dist/overmind.js",
14278
- prepublishOnly: "pnpm run build",
14279
- test: "vitest run && SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
14280
- "test:client": "SWARM_TEST_BACKEND=client vitest run src/overmind.test.ts src/overmind.storm.test.ts src/db.storm.test.ts",
14281
- "test:watch": "vitest",
14282
- typecheck: "tsc -p tsconfig.json --noEmit",
14283
- lint: "echo 'no lint in swarm-daemon yet'"
14284
- },
14285
- dependencies: {
14286
- "@hono/node-server": "^2.0.12",
14287
- "@modelcontextprotocol/sdk": "^1.29.0",
14288
- execa: "^10.0.0",
14289
- zod: "3.25.76"
14290
- },
14291
- gitHead: "82950eae7a6ad09be4193708257798c811bb6fae"
14292
- };
14293
-
14294
- // src/version.ts
14295
- var PACKAGE_VERSION = package_default.version;
14296
- var MIN_NODE_MAJOR = Number(/(\d+)/.exec(package_default.engines?.node ?? "")?.[1] ?? 0);
14297
-
14298
- // src/web.ts
14299
14864
  init_storm();
14300
14865
  init_resources();
14301
14866
  init_config();
@@ -15903,42 +16468,24 @@ function closeWebServer(server) {
15903
16468
  });
15904
16469
  }
15905
16470
 
15906
- // src/daemonVersion.ts
15907
- function captureDaemonStart(now = () => /* @__PURE__ */ new Date()) {
15908
- return { startedAt: now().toISOString(), version: PACKAGE_VERSION };
15909
- }
15910
- var DAEMON_CODE_PATHS = ["packages/swarm-daemon", "packages/swarm-core"];
15911
- async function checkDaemonVersion(start, opts = {}) {
15912
- const now = opts.now ?? (() => /* @__PURE__ */ new Date());
15913
- const fetchLatest = opts.latest ?? (() => fetchLatestVersion());
15914
- const latestVersion = await fetchLatest().catch(() => null);
15915
- return {
15916
- startedAt: start.startedAt,
15917
- version: start.version,
15918
- latestVersion,
15919
- stale: isOutdated(start.version, latestVersion),
15920
- checkedAt: now().toISOString()
15921
- };
15922
- }
15923
-
15924
16471
  // src/hostCommands.ts
15925
16472
  init_src();
15926
- import { readFileSync as readFileSync19, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
15927
- import { join as join28 } from "path";
16473
+ import { readFileSync as readFileSync22, renameSync as renameSync10, writeFileSync as writeFileSync15 } from "fs";
16474
+ import { join as join30 } from "path";
15928
16475
  function hostCommandStatePath(home) {
15929
- return join28(home, "host-command.json");
16476
+ return join30(home, "host-command.json");
15930
16477
  }
15931
16478
  function readState(path) {
15932
16479
  try {
15933
- return JSON.parse(readFileSync19(path, "utf8"));
16480
+ return JSON.parse(readFileSync22(path, "utf8"));
15934
16481
  } catch {
15935
16482
  return {};
15936
16483
  }
15937
16484
  }
15938
16485
  function writeState(path, state) {
15939
16486
  try {
15940
- writeFileSync14(`${path}.tmp`, JSON.stringify(state));
15941
- renameSync9(`${path}.tmp`, path);
16487
+ writeFileSync15(`${path}.tmp`, JSON.stringify(state));
16488
+ renameSync10(`${path}.tmp`, path);
15942
16489
  } catch {
15943
16490
  }
15944
16491
  }
@@ -16100,8 +16647,8 @@ async function doRestart(cmd, opts, note) {
16100
16647
 
16101
16648
  // src/selfUpdate.ts
16102
16649
  init_git();
16103
- import { existsSync as existsSync16 } from "fs";
16104
- import { join as join29 } from "path";
16650
+ import { existsSync as existsSync18 } from "fs";
16651
+ import { join as join31 } from "path";
16105
16652
  import { $ as $5 } from "execa";
16106
16653
  var DAEMON_PACKAGE = "@taskless-app/swarm";
16107
16654
  var defaultRun = async (cmd, args, cwd) => {
@@ -16114,7 +16661,7 @@ function tail(text, max = 400) {
16114
16661
  }
16115
16662
  async function runSelfUpdate(io) {
16116
16663
  const run = io.run ?? defaultRun;
16117
- const exists = io.exists ?? existsSync16;
16664
+ const exists = io.exists ?? existsSync18;
16118
16665
  const repo = io.repo;
16119
16666
  await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
16120
16667
  const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
@@ -16143,7 +16690,7 @@ async function runSelfUpdate(io) {
16143
16690
  };
16144
16691
  }
16145
16692
  await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
16146
- if (!exists(join29(repo, "pnpm-lock.yaml"))) {
16693
+ if (!exists(join31(repo, "pnpm-lock.yaml"))) {
16147
16694
  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" };
16148
16695
  }
16149
16696
  const installed2 = await run("pnpm", ["install", "--frozen-lockfile"], repo);
@@ -16244,7 +16791,7 @@ function orphanNoteText(run, q) {
16244
16791
 
16245
16792
  // src/probe.ts
16246
16793
  init_executors();
16247
- import { spawn as spawn2 } from "child_process";
16794
+ import { spawn as spawn3 } from "child_process";
16248
16795
  var RESET_RE = /resets[^\n]*/i;
16249
16796
  function executorHealthy(opts = {}) {
16250
16797
  const executor = opts.executor ?? resolveExecutor();
@@ -16263,7 +16810,7 @@ function executorHealthy(opts = {}) {
16263
16810
  }
16264
16811
  resolve4(r);
16265
16812
  };
16266
- const child = spawn2(bin, [...executor.probe.args], { stdio: ["ignore", "pipe", "pipe"] });
16813
+ const child = spawn3(bin, [...executor.probe.args], { stdio: ["ignore", "pipe", "pipe"] });
16267
16814
  child.stdout?.on("data", (d) => {
16268
16815
  out += d.toString();
16269
16816
  });
@@ -16302,19 +16849,19 @@ async function fetchExternalStorm(url, opts = {}) {
16302
16849
  }
16303
16850
 
16304
16851
  // src/anomalyState.ts
16305
- import { existsSync as existsSync17, readFileSync as readFileSync20, renameSync as renameSync10, writeFileSync as writeFileSync15 } from "fs";
16306
- import { join as join30 } from "path";
16852
+ import { existsSync as existsSync19, readFileSync as readFileSync23, renameSync as renameSync11, writeFileSync as writeFileSync16 } from "fs";
16853
+ import { join as join32 } from "path";
16307
16854
  var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
16308
16855
  var MAX_RESOLVED = 200;
16309
16856
  function emptyState() {
16310
16857
  return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
16311
16858
  }
16312
16859
  function anomalyStatePath(home) {
16313
- return join30(home, "anomaly-state.json");
16860
+ return join32(home, "anomaly-state.json");
16314
16861
  }
16315
16862
  function loadAnomalyState(home, now = Date.now(), io = {}) {
16316
- const exists = io.exists ?? existsSync17;
16317
- const readFile2 = io.readFile ?? ((p) => readFileSync20(p, "utf8"));
16863
+ const exists = io.exists ?? existsSync19;
16864
+ const readFile2 = io.readFile ?? ((p) => readFileSync23(p, "utf8"));
16318
16865
  const path = anomalyStatePath(home);
16319
16866
  if (!exists(path)) return emptyState();
16320
16867
  try {
@@ -16343,11 +16890,11 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
16343
16890
  }
16344
16891
  function saveAnomalyState(home, state, io = {}) {
16345
16892
  const path = anomalyStatePath(home);
16346
- const writeFile3 = io.writeFile ?? ((p, data) => {
16347
- writeFileSync15(`${p}.tmp`, data);
16348
- renameSync10(`${p}.tmp`, p);
16893
+ const writeFile4 = io.writeFile ?? ((p, data) => {
16894
+ writeFileSync16(`${p}.tmp`, data);
16895
+ renameSync11(`${p}.tmp`, p);
16349
16896
  });
16350
- writeFile3(path, JSON.stringify(state, null, 2));
16897
+ writeFile4(path, JSON.stringify(state, null, 2));
16351
16898
  }
16352
16899
  var AnomalyStateStore = class {
16353
16900
  constructor(home, now = Date.now(), io = {}) {
@@ -16499,6 +17046,7 @@ function decideParkAction(db, droneCap, exclusivityBlocked) {
16499
17046
  function decideFreshAction(db, wt) {
16500
17047
  const stalled = db.listRuns("stalled").filter((r) => r.resolves_run_id == null && r.reviews_run_id == null);
16501
17048
  for (const r of stalled) {
17049
+ if (findEscalation(db.events(r.id).map((e) => e.text ?? ""), r, "author")) continue;
16502
17050
  const path = r.worktree_path;
16503
17051
  const recoverable = !!path && wt.exists(path) && wt.hasTranscript(path);
16504
17052
  if (recoverable) continue;
@@ -16814,19 +17362,11 @@ var EXITED_CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u043E\u0448\u
16814
17362
  var STORM_DEAD_LIMIT = Number(process.env.SWARM_STORM_DEAD_LIMIT) || 3;
16815
17363
  var CONFLICT_RESOLVE_COOLDOWN_MS = Number(process.env.SWARM_CONFLICT_COOLDOWN_MS) || 3 * 60 * 1e3;
16816
17364
  var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
16817
- var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
16818
17365
  var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
16819
- var REVIEW_ESCALATION_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F";
16820
- var REVIEW_UNRECOVERABLE_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u044C";
16821
- function reviewUnrecoverableMark(branch) {
16822
- return `${REVIEW_UNRECOVERABLE_MARK} (\u0432\u0435\u0442\u043A\u0430 ${branch})`;
16823
- }
16824
17366
  var REVIEW_CYCLE_LIMIT = Number(process.env.SWARM_REVIEW_CYCLE_LIMIT) || 3;
16825
- var REVIEW_CYCLE_ESCALATION_MARK = "\u0440\u0435-\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0446\u0438\u043A\u043B\u0438\u043B\u043E\u0441\u044C";
16826
17367
  var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
16827
17368
  var CI_WAKE_COOLDOWN_MS = Number(process.env.SWARM_CI_WAKE_COOLDOWN_MS) || 3 * 60 * 1e3;
16828
17369
  var CI_CHECK_COOLDOWN_MS = Number(process.env.SWARM_CI_CHECK_COOLDOWN_MS) || 60 * 1e3;
16829
- var CI_WAKE_ESCALATION_MARK = "CI-\u0430\u0432\u0442\u043E\u043F\u043E\u0447\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043F\u0440\u0430\u0432\u0438\u043B\u0430\u0441\u044C";
16830
17370
  var CI_NO_CHECKS_MARK = "\u0443 PR \u043D\u0435\u0442 \u0447\u0435\u043A\u043E\u0432 \u0438 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442";
16831
17371
  var CI_WAIT_POLL_MARK = "\u0436\u0434\u0443 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A";
16832
17372
  var CI_POLL_LIMIT_MARK = "\u043E\u043F\u0440\u043E\u0441 CI \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043F\u043E \u043F\u043E\u0442\u043E\u043B\u043A\u0443";
@@ -16842,34 +17382,19 @@ var CI_POLL_WINDOW_MS = Math.max(
16842
17382
  Number(process.env.SWARM_CI_POLL_WINDOW_MS) || 90 * 60 * 1e3,
16843
17383
  CI_WAIT_LIMIT_MS + CI_CHECK_COOLDOWN_MS
16844
17384
  );
16845
- var CI_WAIT_ESCALATION_MARK = "\u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 CI \u0437\u0430\u0442\u044F\u043D\u0443\u043B\u043E\u0441\u044C";
16846
17385
  var CHANGES_WAKE_COOLDOWN_MS = Number(process.env.SWARM_CHANGES_WAKE_COOLDOWN_MS) || 3 * 60 * 1e3;
16847
17386
  var CHANGES_WAKE_LIMIT = Number(process.env.SWARM_CHANGES_WAKE_LIMIT) || 2;
16848
17387
  var CHANGES_FRESH_WAKE_AFTER_MS = Number(process.env.SWARM_CHANGES_FRESH_WAKE_AFTER_MS) || SWARM_ANOMALY_PR_NO_REVIEWER_MS;
16849
17388
  var CHANGES_WAKE_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u044F \u043F\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0443 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB";
16850
- var CHANGES_WAKE_ESCALATION_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u044F \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0430\u0441\u044C";
16851
17389
  var CHANGES_WAKE_FAILED_MARK = "\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C";
16852
17390
  var BUDGET_STOP_REASON = "\u043D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442: \u0441\u0435\u0441\u0441\u0438\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B \u0431\u044E\u0434\u0436\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430";
16853
17391
  var NO_AUTO_WAKE_REASONS = [EXITED_NO_DONE_REASON, BUDGET_STOP_REASON, ABORTED_STOP_REASON];
16854
- var REVIEW_BUDGET_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u043E\u0441\u044C \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442";
16855
- function reviewBudgetMark(cycle) {
16856
- return `${REVIEW_BUDGET_MARK} (\u043A\u0440\u0443\u0433 ${cycle})`;
16857
- }
16858
- var NEEDS_HUMAN_MARKS = [
16859
- RESOLVE_ESCALATION_MARK,
16860
- REVIEW_ESCALATION_MARK,
16861
- REVIEW_UNRECOVERABLE_MARK,
16862
- REVIEW_CYCLE_ESCALATION_MARK,
16863
- CI_WAKE_ESCALATION_MARK,
16864
- CHANGES_WAKE_ESCALATION_MARK,
16865
- REVIEW_BUDGET_MARK
16866
- ];
16867
- var SELF_REFERENCE_MARK = "PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430";
16868
17392
  var REVIEW_NO_VERDICT_REASON = "\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u043E\u0441\u044C \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430: \u043D\u0438 approve, \u043D\u0438 changes \u2014 \u0437\u0430\u0441\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E, \u043D\u0443\u0436\u0435\u043D \u043D\u043E\u0432\u044B\u0439 \u0437\u0430\u0445\u043E\u0434 (TASWM-375)";
16869
17393
  function verdictCommentMark(createdAt) {
16870
17394
  return `\u043A\u043E\u043C\u043C\u0435\u043D\u0442-\u0432\u0435\u0440\u0434\u0438\u043A\u0442@${createdAt}`;
16871
17395
  }
16872
17396
  var WATCHDOG_PARK_REASON = "\u0441\u0442\u043E\u0440\u043E\u0436 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430: \u0441\u043B\u043E\u0442 \u0437\u0430\u043D\u044F\u0442 \u043D\u0438\u0437\u043A\u043E\u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442\u043D\u044B\u043C \u2014 \u043F\u0430\u0440\u043A\u0443\u044E, \u0432\u043F\u0443\u0441\u043A\u0430\u044E \u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442\u043D\u0435\u0435 \u0436\u0434\u0443\u0449\u0435\u0433\u043E (TASWM-270)";
17397
+ var RELIEF_LOOP_STOP_REASON = "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u0437\u0430\u0446\u0438\u043A\u043B\u0438\u043B\u0430\u0441\u044C: \u0441\u043D\u044F\u0442\u0438\u0439 \u043F\u043E\u0434\u0440\u044F\u0434 \u0431\u0435\u0437 \u0441\u0434\u0430\u0447\u0438 \u0431\u043E\u043B\u044C\u0448\u0435 \u043F\u043E\u0442\u043E\u043B\u043A\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A (TASWM-531)";
16873
17398
  function livePpids() {
16874
17399
  return livePpidsFrom(listProcs());
16875
17400
  }
@@ -17120,11 +17645,17 @@ async function prGateForReviewer(deps, run, prUrl, when) {
17120
17645
  await finalizeClosedPrReview(deps, run, `PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 (${when}) \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E`);
17121
17646
  narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, ${when} \u2014 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E, \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442.`);
17122
17647
  return { kind: "finalized" };
17123
- case "unknown":
17648
+ case "unknown": {
17649
+ const notPr = !isGithubPrUrl(prUrl);
17124
17650
  if (!deps.db.events(run.id).some((e) => (e.text ?? "").includes(REVIEW_PR_UNKNOWN_MARK))) {
17125
- deps.db.addEvent(run.id, "step", `${REVIEW_PR_UNKNOWN_MARK} (gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0438\u043B\u0438 \u0432 pr_url \u043D\u0435 PR) \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u043C, \u0441\u043F\u0440\u043E\u0441\u0438\u043C \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C \u0442\u0438\u043A\u0435`);
17651
+ deps.db.addEvent(
17652
+ run.id,
17653
+ "step",
17654
+ notPr ? `${REVIEW_PR_UNKNOWN_MARK}: \u0432 pr_url \u043D\u0435 GitHub-PR (${prUrl}) \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u043C. \u0421\u0430\u043C\u043E \u043D\u0435 \u043F\u043E\u0447\u0438\u043D\u0438\u0442\u0441\u044F: \u0434\u0440\u043E\u043D \u0441\u0434\u0430\u043B \u0441\u0441\u044B\u043B\u043A\u043E\u0439 \u043D\u0435 \u043D\u0430 PR.` : `${REVIEW_PR_UNKNOWN_MARK}: gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043F\u0440\u043E PR (\u0441\u0435\u0442\u044C/\u043B\u0438\u043C\u0438\u0442) \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u043C, \u0441\u043F\u0440\u043E\u0441\u0438\u043C \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C \u0442\u0438\u043A\u0435`
17655
+ );
17126
17656
  }
17127
17657
  return { kind: "wait" };
17658
+ }
17128
17659
  default:
17129
17660
  if (st.conflicting || st.isDraft) {
17130
17661
  deps.db.addEvent(run.id, "step", `PR \u0441\u0442\u0430\u043B \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u044E\u0449\u0438\u043C/\u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u043E\u043C, ${when} \u2014 \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D \u0434\u043E \u0441\u043B\u0435\u0434. \u0442\u0438\u043A\u0430`);
@@ -17238,17 +17769,11 @@ function relinkUnboundReviewers(deps) {
17238
17769
  narrate(deps, `${droneLabel(rv)} \u0440\u0430\u0431\u043E\u0442\u0430\u043B \u0431\u0435\u0437 \u043F\u0440\u0438\u0432\u044F\u0437\u043A\u0438 \u043A \u043F\u0440\u043E\u0433\u043E\u043D\u0443 \u2014 \u0441\u0432\u044F\u0437\u0430\u043B \u043F\u043E PR-\u0441\u0441\u044B\u043B\u043A\u0435, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0442\u0435\u043F\u0435\u0440\u044C \u043B\u044F\u0436\u0435\u0442 \u0432 ${parent.task_slug}, \u0430 \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442 GitHub.`);
17239
17770
  }
17240
17771
  }
17772
+ var ESCALATION_LIMITS = { reviewSpawn: REVIEW_SPAWN_LIMIT, reviewCycle: REVIEW_CYCLE_LIMIT };
17241
17773
  function deliberateReviewStop(db, run) {
17242
17774
  const texts = db.events(run.id).map((e) => e.text ?? "");
17243
- const has = (mark) => texts.some((t) => t.includes(mark));
17244
- if (has(SELF_REFERENCE_MARK)) {
17245
- return `PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441\u0430\u043C\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430 (${DAEMON_CODE_PATHS.join(", ")}) \u2014 \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u043D\u043E (TASWM-270): \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0449\u0438\u0439 \u0434\u0435\u043C\u043E\u043D \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F \u043D\u0430 \u0441\u0442\u0430\u0440\u043E\u043C \u043A\u043E\u0434\u0435 \u0438 \u0441\u0432\u043E\u044E \u0436\u0435 \u043F\u0440\u0430\u0432\u043A\u0443 \u043D\u0435 \u0430\u0442\u0442\u0435\u0441\u0442\u0443\u0435\u0442. \u041D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
17246
- }
17247
- if (has(REVIEW_ESCALATION_MARK)) return `\u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u044B (${REVIEW_SPAWN_LIMIT}) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
17248
- if (has(REVIEW_CYCLE_ESCALATION_MARK)) return `\u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0446\u0438\u043A\u043B\u043E\u0432 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E (${REVIEW_CYCLE_LIMIT}) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
17249
- if (has(reviewBudgetMark(run.review_cycle ?? 0))) return "\u0440\u0435\u0432\u044C\u044E \u044D\u0442\u043E\u0433\u043E \u043A\u0440\u0443\u0433\u0430 \u0441\u0433\u043E\u0440\u0435\u043B\u043E \u043D\u0430 \u043F\u043E\u0442\u043E\u043B\u043A\u0435 \u0442\u0440\u0430\u0442 \u2014 \u043F\u043E\u0432\u0442\u043E\u0440 \u0443\u043F\u0440\u0451\u0442\u0441\u044F \u0432 \u0442\u043E\u0442 \u0436\u0435, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A";
17250
- if (has(reviewUnrecoverableMark(run.branch || run.task_slug))) return "\u0432\u0435\u0442\u043A\u0438 PR \u043D\u0435\u0442 \u043D\u0430 origin \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A";
17251
- return null;
17775
+ const hit = findEscalation(texts, run, "reviewer");
17776
+ return hit?.stopReason?.(run, ESCALATION_LIMITS) ?? null;
17252
17777
  }
17253
17778
  function noteReviewBlocked(deps, run, reason) {
17254
17779
  if (!shouldNoteReason(deps.db, run.id, reason)) return;
@@ -17322,7 +17847,6 @@ async function closeOrphanedReviewers(deps) {
17322
17847
  }
17323
17848
  }
17324
17849
  var VERIFY_SPAWN_LIMIT = Number(process.env.SWARM_VERIFY_SPAWN_LIMIT) || 2;
17325
- var VERIFY_ESCALATION_MARK = "\u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F: \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u044B";
17326
17850
  function countActiveVerifiers(db) {
17327
17851
  return db.listRuns().filter((r) => r.kind === "verifier" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
17328
17852
  }
@@ -17496,10 +18020,10 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
17496
18020
  }
17497
18021
  return parts.join("\n");
17498
18022
  }
17499
- var HERO_CHARTER_PATH = join31(dirname5(THIS_FILE), "..", "docs", "HERO.md");
18023
+ var HERO_CHARTER_PATH = join33(dirname5(THIS_FILE), "..", "docs", "HERO.md");
17500
18024
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
17501
18025
  const name = run.name || `#${run.id}`;
17502
- const charter = readFileSync21(HERO_CHARTER_PATH, "utf8");
18026
+ const charter = readFileSync24(HERO_CHARTER_PATH, "utf8");
17503
18027
  const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
17504
18028
 
17505
18029
  ${task.description}` : `\u042D\u041F\u0418\u041A ${run.task_slug} (\u0442\u0435\u043B\u043E \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E slug \u0438 \u0437\u0434\u0440\u0430\u0432\u043E\u043C\u0443 \u0441\u043C\u044B\u0441\u043B\u0443).`;
@@ -17803,17 +18327,11 @@ async function wakeRun(deps, runId) {
17803
18327
  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` };
17804
18328
  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` };
17805
18329
  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` };
17806
- const authorSideStopped = deps.db.events(run.id).some(
17807
- (e) => [RESOLVE_ESCALATION_MARK, CI_WAKE_ESCALATION_MARK, VERIFY_ESCALATION_MARK].some(
17808
- (m) => (e.text ?? "").includes(m)
17809
- )
17810
- );
17811
- const changesEscalated = deps.db.events(run.id).some((e) => (e.text ?? "").includes(`${CHANGES_WAKE_ESCALATION_MARK} (\u043A\u0440\u0443\u0433 ${run.review_cycle ?? 0})`));
17812
- const stoppedForHuman = deliberateReviewStop(deps.db, run) != null || changesEscalated || authorSideStopped;
18330
+ const stoppedForHuman = deps.db.events(run.id).some((e) => isEscalationText(e.text ?? "", run, "author"));
17813
18331
  if (run.status === "review" && run.pr_url && !needsFixIteration(run) && !stoppedForHuman) {
17814
18332
  return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0441\u0434\u0430\u043D \u043D\u0430 \u0440\u0435\u0432\u044C\u044E (${run.pr_url}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E; \u043F\u0440\u0430\u0432\u043A\u0438 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442 \u0440\u043E\u0439 \u043F\u043E \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0443` };
17815
18333
  }
17816
- const worktreeExists = deps.worktreeExists ?? existsSync18;
18334
+ const worktreeExists = deps.worktreeExists ?? existsSync20;
17817
18335
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
17818
18336
  const wt = run.worktree_path;
17819
18337
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -17907,11 +18425,25 @@ async function handleCi(deps, run, now, worktreeExists, hasTranscript, droneCap)
17907
18425
  }
17908
18426
  if (ci?.bucket === "pass" && await wakeReviewerOnCiReady(deps, run, { kind: "pass", ci })) return;
17909
18427
  const counts = ci && ci.total > 0 ? ` (${ci.pass}/${ci.total})` : "";
17910
- const note = ci == null ? "\u0436\u0434\u0443 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043E\u043F\u0440\u043E\u0441\u0430" : ci.bucket === "pending" ? "\u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0435\u0449\u0451 \u0438\u0434\u0443\u0442" : ci.bucket === "none" ? "\u0447\u0435\u043A-\u0440\u0430\u043D\u043E\u0432 \u0435\u0449\u0451 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E, workflow \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0435\u0441\u0442\u044C \u2014 \u0436\u0434\u0443" : "\u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0433\u043E\u0442\u043E\u0432\u044B, \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u043A\u043E\u0433\u043E";
17911
- const waiting = ci == null || ci.bucket === "pending" || ci.bucket === "none";
18428
+ const why = ci == null ? "\u043E\u043F\u0440\u043E\u0441 CI \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F \u0446\u0435\u043B\u0438\u043A\u043E\u043C (checks \u0431\u0440\u043E\u0441\u0438\u043B) \u2014 \u0436\u0434\u0443 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043E\u043F\u0440\u043E\u0441\u0430" : ciUnavailableNote(ci);
18429
+ const note = ci == null || ci.bucket === "unavailable" ? why : ci.bucket === "pending" ? "\u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0435\u0449\u0451 \u0438\u0434\u0443\u0442" : ci.bucket === "none" ? "\u0447\u0435\u043A-\u0440\u0430\u043D\u043E\u0432 \u0435\u0449\u0451 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E, workflow \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0435\u0441\u0442\u044C \u2014 \u0436\u0434\u0443" : "\u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0433\u043E\u0442\u043E\u0432\u044B, \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u043A\u043E\u0433\u043E";
18430
+ const waiting = ci == null || ci.bucket === "unavailable" || ci.bucket === "pending" || ci.bucket === "none";
17912
18431
  const text = `CI ${ci?.bucket ?? "\u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D"}${counts} \u2014 ${note}`;
17913
18432
  deps.db.addEvent(run.id, "ci_checked", waiting ? `${text} [${CI_WAIT_POLL_MARK}]` : text);
17914
18433
  }
18434
+ function ciUnavailableNote(ci) {
18435
+ const detail = ci.detail ? ` \u2014 ${ci.detail}` : "";
18436
+ switch (ci.reason) {
18437
+ case "timeout":
18438
+ return `\u0422\u0410\u0419\u041C\u0410\u0423\u0422: gh pr checks \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0437\u0430 ${Math.round(GH_CALL_TIMEOUT_MS / 1e3)} \u0441 \u0438 \u043F\u0440\u0435\u0440\u0432\u0430\u043D${detail}. \u0421\u0435\u0442\u044C \u0443 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u043C\u0435\u0434\u043B\u0435\u043D\u043D\u0435\u0435, \u0447\u0435\u043C \u0443 \u0448\u0435\u043B\u043B\u0430 (\u043F\u0440\u043E\u043A\u0441\u0438/DNS) \u2014 \u0436\u0434\u0443 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043E\u043F\u0440\u043E\u0441\u0430`;
18439
+ case "crashed":
18440
+ return `gh \u041D\u0415 \u0417\u0410\u041F\u0423\u0421\u0422\u0418\u041B\u0421\u042F${detail}. \u042D\u0442\u043E \u043D\u0435 \u0441\u0435\u0442\u044C: \u0443 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442 gh \u0432 PATH \u2014 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`;
18441
+ case "unreadable":
18442
+ return `\u041D\u0415\u0427\u0418\u0422\u0410\u0415\u041C\u042B\u0419 \u041E\u0422\u0412\u0415\u0422 gh${detail}. \u0412\u044B\u0432\u043E\u0434 \u0435\u0441\u0442\u044C, \u043D\u043E \u044D\u0442\u043E \u043D\u0435 \u0441\u043F\u0438\u0441\u043E\u043A \u0447\u0435\u043A\u043E\u0432 \u2014 \u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C, \u0447\u0442\u043E \u0438\u043C\u0435\u043D\u043D\u043E \u043F\u0435\u0447\u0430\u0442\u0430\u0435\u0442 gh, \u0430 \u043D\u0435 \u0436\u0434\u0430\u0442\u044C`;
18443
+ default:
18444
+ return `\u041F\u0423\u0421\u0422\u041E\u0419 \u041E\u0422\u0412\u0415\u0422: gh \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0441\u044F, \u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u0432 \u043D\u0438\u0447\u0435\u0433\u043E${detail}. \u0421\u043C\u043E\u0442\u0440\u0435\u0442\u044C stderr \u0432\u044B\u0437\u043E\u0432\u0430 \u0432 \u043B\u043E\u0433\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0442\u0430\u043C DNS/\u043F\u0440\u043E\u043A\u0441\u0438/\u043B\u0438\u043C\u0438\u0442`;
18445
+ }
18446
+ }
17915
18447
  function repoHasCi(deps, run) {
17916
18448
  if (deps.repoHasCi) return deps.repoHasCi(run);
17917
18449
  return repoHasCiWorkflows(run.worktree_path, run.repo);
@@ -17919,7 +18451,7 @@ function repoHasCi(deps, run) {
17919
18451
  function repoHasCiWorkflows(worktreePath, repo) {
17920
18452
  for (const base3 of [worktreePath, repo]) {
17921
18453
  if (!base3) continue;
17922
- const dir = join31(base3, ".github", "workflows");
18454
+ const dir = join33(base3, ".github", "workflows");
17923
18455
  try {
17924
18456
  if (readdirSync13(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
17925
18457
  } catch {
@@ -18105,7 +18637,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
18105
18637
  return true;
18106
18638
  }
18107
18639
  function worktreeAndTranscriptAlive(deps, worktree) {
18108
- const exists = deps.worktreeExists ?? existsSync18;
18640
+ const exists = deps.worktreeExists ?? existsSync20;
18109
18641
  const transcript = deps.hasTranscript ?? executorHasSession;
18110
18642
  return exists(worktree) && transcript(worktree);
18111
18643
  }
@@ -18614,7 +19146,7 @@ async function resumeWatchdogParked(deps, droneCap) {
18614
19146
  }
18615
19147
  if (!candidate) return;
18616
19148
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
18617
- const worktreeExists = deps.worktreeExists ?? existsSync18;
19149
+ const worktreeExists = deps.worktreeExists ?? existsSync20;
18618
19150
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
18619
19151
  const wt = candidate.worktree_path;
18620
19152
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -18729,17 +19261,31 @@ async function manageRelief(deps, now, droneCap, stormActive) {
18729
19261
  console.error(`[overmind] \u0440\u0435\u043A\u043B\u0435\u0439\u043C worktree \u043F\u0430\u0443\u0437\u043D\u0443\u0442\u043E\u0433\u043E ${droneLabel(candidate)} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
18730
19262
  }
18731
19263
  }
19264
+ let saved = false;
18732
19265
  if (candidate.worktree_path && deps.pauseSnapshot) {
18733
19266
  try {
18734
- const saved = await deps.pauseSnapshot(candidate.worktree_path);
19267
+ saved = await deps.pauseSnapshot(candidate.worktree_path);
18735
19268
  if (saved) deps.db.addEvent(candidate.id, "step", "\u043D\u0435\u0434\u043E\u0434\u0435\u043B\u0430\u043D\u043D\u043E\u0435 \u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043E+\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043E \u043F\u0435\u0440\u0435\u0434 \u043F\u0430\u0443\u0437\u043E\u0439 (TASWM-229) \u2014 --continue \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442 \u0441 \u044D\u0442\u043E\u0439 \u0442\u043E\u0447\u043A\u0438");
18736
19269
  } catch (e) {
18737
19270
  console.error(`[overmind] \u0441\u043D\u0430\u043F\u0448\u043E\u0442 worktree \u043F\u0430\u0443\u0437\u043D\u0443\u0442\u043E\u0433\u043E ${droneLabel(candidate)} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
18738
19271
  }
18739
19272
  }
19273
+ const canContinue = !!candidate.worktree_path && worktreeAndTranscriptAlive(deps, candidate.worktree_path);
18740
19274
  deps.db.updateRun(candidate.id, { status: "paused", stop_reason: RELIEF_PAUSE_REASON });
18741
- deps.db.addEvent(candidate.id, "paused", RELIEF_PAUSE_REASON);
19275
+ deps.db.addEvent(candidate.id, "paused", reliefPauseReason(canContinue));
18742
19276
  state.lastPauseAt = now;
19277
+ const loopEntry = deps.reliefLoop?.recordPause(
19278
+ reliefLoopKey(normalizeRepo(candidate.repo || deps.repoFallback), candidate.task_slug),
19279
+ now,
19280
+ candidate.pr_url != null || saved
19281
+ );
19282
+ if (loopEntry && loopEntry.pauses > 1) {
19283
+ deps.db.addEvent(
19284
+ candidate.id,
19285
+ "step",
19286
+ `${RELIEF_PAUSE_MARK}: \u0441\u043D\u044F\u0442\u0438\u0435 ${loopEntry.pauses}-\u0435 \u043F\u043E\u0434\u0440\u044F\u0434 \u0431\u0435\u0437 \u0441\u0434\u0430\u0447\u0438 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043E\u0442\u043B\u043E\u0436\u0435\u043D \u043D\u0430 ${Math.round(reliefBackoffMs(loopEntry.pauses, cfg.rampWindowMs) / 1e3)}\u0441 (TASWM-531)`
19287
+ );
19288
+ }
18743
19289
  recordReliefPause(
18744
19290
  state.cost ??= { pauses: [] },
18745
19291
  { runId: candidate.id, label: droneLabel(candidate), at: now, resumedAt: null, stepsSinceResume },
@@ -18771,7 +19317,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
18771
19317
  state.lastLoggedGateReason = null;
18772
19318
  }
18773
19319
  if (countActiveDrones(deps.db) >= droneCap) return;
18774
- const pausedForRelief = deps.db.listRuns("paused").filter((r) => r.stop_reason === RELIEF_PAUSE_REASON);
19320
+ const pausedForRelief = deps.db.listRuns("paused").filter((r) => isReliefPause(r.stop_reason));
18775
19321
  const recovering = pausedForRelief.length > 0 || state.overloadedAt != null && now - state.overloadedAt < cfg.rampWindowMs;
18776
19322
  if (!recovering) {
18777
19323
  await batchSpawnQueue(deps, droneCap);
@@ -18782,10 +19328,27 @@ async function manageRelief(deps, now, droneCap, stormActive) {
18782
19328
  if (resumeCandidate) {
18783
19329
  if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
18784
19330
  if (!deps.respawn) return;
19331
+ const loopKey = reliefLoopKey(normalizeRepo(resumeCandidate.repo || deps.repoFallback), resumeCandidate.task_slug);
19332
+ const loopEntry = deps.reliefLoop?.get(loopKey);
19333
+ if (loopEntry) {
19334
+ const loopCfg = resolveReliefLoopConfig(deps.reliefLoopConfig);
19335
+ if (reliefLoopTripped(loopEntry, loopCfg)) {
19336
+ if (!loopEntry.escalated) {
19337
+ deps.db.updateRun(resumeCandidate.id, { status: "stalled", stop_reason: RELIEF_LOOP_STOP_REASON });
19338
+ deps.db.addEvent(
19339
+ resumeCandidate.id,
19340
+ "step",
19341
+ `${RELIEF_LOOP_ESCALATION_MARK}: \u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u0441\u043D\u0438\u043C\u0430\u043B\u0430 \u043F\u0440\u043E\u0433\u043E\u043D ${loopEntry.pauses} \u0440\u0430\u0437(\u0430) \u043F\u043E\u0434\u0440\u044F\u0434 \u0438 \u043D\u0438 \u043E\u0434\u0438\u043D \u043A\u0440\u0443\u0433 \u043D\u0435 \u043F\u0440\u0438\u043D\u0451\u0441 \u0441\u0434\u0430\u0447\u0438 (\u043D\u0438 PR, \u043D\u0438 \u043A\u043E\u043C\u043C\u0438\u0442\u0430) \u2014 \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A (\u043F\u043E\u0442\u043E\u043B\u043E\u043A ${loopCfg.limit}, TASWM-531)`
19342
+ );
19343
+ deps.reliefLoop?.markEscalated(loopKey);
19344
+ narrate(deps, `${droneLabel(resumeCandidate)} \u0441\u043D\u0438\u043C\u0430\u043B\u0441\u044F \u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u043E\u0439 ${loopEntry.pauses} \u0440\u0430\u0437(\u0430) \u043F\u043E\u0434\u0440\u044F\u0434 \u0431\u0435\u0437 \u0435\u0434\u0438\u043D\u043E\u0433\u043E \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u2014 \u044D\u0442\u043E \u043F\u0435\u0442\u043B\u044F, \u0434\u0430\u043B\u044C\u0448\u0435 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440.`);
19345
+ }
19346
+ return;
19347
+ }
19348
+ if (now - loopEntry.lastPauseAt < reliefBackoffMs(loopEntry.pauses, cfg.rampWindowMs, loopCfg)) return;
19349
+ }
18785
19350
  const wt = resumeCandidate.worktree_path;
18786
- const worktreeExists = deps.worktreeExists ?? existsSync18;
18787
- const hasTranscript = deps.hasTranscript ?? executorHasSession;
18788
- const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
19351
+ const canContinue = !!wt && worktreeAndTranscriptAlive(deps, wt);
18789
19352
  const mode = canContinue ? "continue" : "fresh";
18790
19353
  if (mode === "continue" && wt && deps.reclaimWorktree) {
18791
19354
  try {
@@ -18824,7 +19387,7 @@ async function buildReliefCostCandidatesFor(deps, now, droneCap) {
18824
19387
  (runId) => {
18825
19388
  const run = runById.get(runId);
18826
19389
  if (!run) return now;
18827
- if (run.status === "paused" && run.stop_reason === RELIEF_PAUSE_REASON) return null;
19390
+ if (run.status === "paused" && isReliefPause(run.stop_reason)) return null;
18828
19391
  return sqliteMs(run.updated_at);
18829
19392
  },
18830
19393
  now
@@ -18840,7 +19403,7 @@ async function buildReliefCostCandidatesFor(deps, now, droneCap) {
18840
19403
  if (!reliefAlertDue(cost, thresholds)) return [];
18841
19404
  const pressure = deps.hostPressure?.() ?? { culprits: [], readings: [], totalGb: 0 };
18842
19405
  const occupying = allRuns.filter(
18843
- (r) => r.kind !== "hero" && (ACTIVE_DRONE_STATUSES.has(r.status) || r.status === "paused" && r.stop_reason === RELIEF_PAUSE_REASON)
19406
+ (r) => r.kind !== "hero" && (ACTIVE_DRONE_STATUSES.has(r.status) || r.status === "paused" && isReliefPause(r.stop_reason))
18844
19407
  );
18845
19408
  const repoOf = (r) => normalizeRepo(r.repo || deps.repoFallback || "");
18846
19409
  const busyRepos = [...new Set(occupying.map(repoOf).filter(Boolean))].filter(
@@ -19043,7 +19606,7 @@ async function handleBudgetExhausted(deps, run, ctx) {
19043
19606
  function buildEscalationCandidates(db) {
19044
19607
  const candidates = [];
19045
19608
  for (const run of db.listRuns("review")) {
19046
- const hit = db.events(run.id).findLast((e) => NEEDS_HUMAN_MARKS.some((mark) => (e.text ?? "").includes(mark)));
19609
+ const hit = db.events(run.id).findLast((e) => isEscalationText(e.text ?? "", run, "supervision"));
19047
19610
  if (!hit) continue;
19048
19611
  candidates.push({
19049
19612
  key: `needs_human:${run.id}`,
@@ -19061,7 +19624,7 @@ function buildEscalationCandidates(db) {
19061
19624
  }
19062
19625
  async function tickOnce(deps) {
19063
19626
  const now = deps.now ? deps.now() : Date.now();
19064
- const worktreeExists = deps.worktreeExists ?? existsSync18;
19627
+ const worktreeExists = deps.worktreeExists ?? existsSync20;
19065
19628
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
19066
19629
  deps.sampleHost?.();
19067
19630
  if (deps.rehydrate) {
@@ -19107,12 +19670,12 @@ async function tickOnce(deps) {
19107
19670
  }
19108
19671
  }
19109
19672
  if (deps.repoMap) {
19110
- const rm3 = deps.repoMap;
19111
- const sinceLast = rm3.lastAt == null ? Infinity : now - rm3.lastAt;
19112
- if (sinceLast >= rm3.intervalMs) {
19113
- rm3.lastAt = now;
19673
+ const rm4 = deps.repoMap;
19674
+ const sinceLast = rm4.lastAt == null ? Infinity : now - rm4.lastAt;
19675
+ if (sinceLast >= rm4.intervalMs) {
19676
+ rm4.lastAt = now;
19114
19677
  try {
19115
- await rm3.run();
19678
+ await rm4.run();
19116
19679
  } catch (e) {
19117
19680
  console.error("[overmind] \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u043A\u0430\u0440\u0442\u044B \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:", e);
19118
19681
  }
@@ -19578,8 +20141,13 @@ async function tickOnce(deps) {
19578
20141
  const ciWatchOff = parent != null && ciWatchClosed(deps.db, parent);
19579
20142
  const ci = ciWatchOff ? null : await deps.ci.checks(run.pr_url).catch(() => null);
19580
20143
  const noCiRepo = ci?.bucket === "none" && !repoHasCi(deps, run);
19581
- if (!ciWatchOff && !noCiRepo && (ci == null || ci.bucket === "pending" || ci.bucket === "none" || ci.bucket === "denied")) {
19582
- const what = ci == null ? "gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043F\u0440\u043E \u0447\u0435\u043A\u0438 \u2014 \u0436\u0434\u0443, \u043F\u043E\u043A\u0430 \u043F\u0440\u043E\u044F\u0441\u043D\u0438\u0442\u0441\u044F" : ci.bucket === "none" ? "\u0447\u0435\u043A\u043E\u0432 \u043D\u0430 PR \u043F\u043E\u043A\u0430 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E \u2014 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0447\u0435\u043A-\u0440\u0430\u043D\u0430, \u043D\u043E workflow \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0435\u0441\u0442\u044C" : ci.bucket === "denied" ? `\u0447\u0435\u043A\u0438 \u0440\u043E\u044E \u043D\u0435 \u0432\u0438\u0434\u043D\u044B: GitHub \u043E\u0442\u043A\u0430\u0437\u0430\u043B \u043F\u043E \u043F\u0440\u0430\u0432\u0430\u043C (403)${ci.detail ? ` \u2014 ${ci.detail}` : ""}` : `CI ${ci.pass}/${ci.total}`;
20144
+ if (!ciWatchOff && !noCiRepo && (ci == null || ci.bucket === "pending" || ci.bucket === "none" || ci.bucket === "denied" || ci.bucket === "unavailable")) {
20145
+ const what = ci == null ? "gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043F\u0440\u043E \u0447\u0435\u043A\u0438 \u2014 \u0436\u0434\u0443, \u043F\u043E\u043A\u0430 \u043F\u0440\u043E\u044F\u0441\u043D\u0438\u0442\u0441\u044F" : ci.bucket === "none" ? "\u0447\u0435\u043A\u043E\u0432 \u043D\u0430 PR \u043F\u043E\u043A\u0430 \u043D\u0435 \u0432\u0438\u0434\u043D\u043E \u2014 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0447\u0435\u043A-\u0440\u0430\u043D\u0430, \u043D\u043E workflow \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0435\u0441\u0442\u044C" : ci.bucket === "denied" ? `\u0447\u0435\u043A\u0438 \u0440\u043E\u044E \u043D\u0435 \u0432\u0438\u0434\u043D\u044B: GitHub \u043E\u0442\u043A\u0430\u0437\u0430\u043B \u043F\u043E \u043F\u0440\u0430\u0432\u0430\u043C (403)${ci.detail ? ` \u2014 ${ci.detail}` : ""}` : (
20146
+ // TASWM-497: и здесь причина СЛОВАМИ, а не общее «gh не ответил». Тот же словарь,
20147
+ // что в ленте опроса (callFailureWord), чтобы одно и то же состояние в журнале
20148
+ // прогона и в событии парковки не называлось двумя разными способами.
20149
+ ci.bucket === "unavailable" ? `\u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C \u0447\u0435\u043A\u0438 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C: gh ${callFailureWord({ reason: ci.reason ?? "empty", detail: "" })}${ci.detail ? ` \u2014 ${ci.detail}` : ""}` : `CI ${ci.pass}/${ci.total}`
20150
+ );
19583
20151
  deps.db.updateRun(run.id, { status: "paused", stop_reason: AWAITING_CI_REASON });
19584
20152
  deps.db.addEvent(run.id, "paused", `${AWAITING_CI_REASON} (${what}) \u2014 \u043D\u0435 \u0441\u043C\u0435\u0440\u0442\u044C: \u0440\u0430\u0437\u0431\u0443\u0436\u0443 \u0441\u0430\u043C, \u043A\u043E\u0433\u0434\u0430 \u0447\u0435\u043A\u0438 \u0434\u043E\u0439\u0434\u0443\u0442 \u0434\u043E \u043A\u043E\u043D\u0446\u0430`);
19585
20153
  narrate(deps, `${droneLabel(run)} \u0443\u0448\u0451\u043B \u0436\u0434\u0430\u0442\u044C CI (${what}) \u2014 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u044E, \u0440\u0430\u0437\u0431\u0443\u0436\u0443 \u043F\u043E \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A.`);
@@ -19642,7 +20210,7 @@ async function tickOnce(deps) {
19642
20210
  }
19643
20211
  for (const run of deps.db.listRuns("paused")) {
19644
20212
  if (run.kind === "hero") continue;
19645
- if (run.stop_reason === AWAITING_CI_REASON || run.stop_reason === RELIEF_PAUSE_REASON) continue;
20213
+ if (run.stop_reason === AWAITING_CI_REASON || isReliefPause(run.stop_reason)) continue;
19646
20214
  const last = lastActivityMs(deps.db, run);
19647
20215
  if (now - last <= STALL_FLOOR_MS) continue;
19648
20216
  const evs = deps.db.events(run.id);
@@ -19658,7 +20226,7 @@ async function tickOnce(deps) {
19658
20226
  for (const run of deps.db.listRuns("paused")) {
19659
20227
  if (run.kind === "hero") continue;
19660
20228
  if (run.reviews_run_id != null) continue;
19661
- if (run.stop_reason === RELIEF_PAUSE_REASON) continue;
20229
+ if (isReliefPause(run.stop_reason)) continue;
19662
20230
  if (NO_AUTO_WAKE_REASONS.some((r) => run.stop_reason?.startsWith(r))) continue;
19663
20231
  if (run.pid != null && deps.isAlive(run.pid)) continue;
19664
20232
  if (!await projectQuotaGateAllows(deps, run)) continue;
@@ -19852,7 +20420,7 @@ async function resolveConflict(deps, reviewRun) {
19852
20420
  model: reviewRun.model,
19853
20421
  review_model: reviewRun.review_model
19854
20422
  });
19855
- const wt = join31(deps.worktreesDir, basename6(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
20423
+ const wt = join33(deps.worktreesDir, basename6(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
19856
20424
  try {
19857
20425
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
19858
20426
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -19931,7 +20499,7 @@ async function spawnReviewer(deps, reviewRun, files) {
19931
20499
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
19932
20500
  workspace_id: reviewRun.workspace_id
19933
20501
  });
19934
- const wt = join31(deps.worktreesDir, basename6(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
20502
+ const wt = join33(deps.worktreesDir, basename6(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
19935
20503
  try {
19936
20504
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
19937
20505
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -20012,7 +20580,7 @@ async function spawnVerifier(deps, verifiedRun, files) {
20012
20580
  // штамп цикла: фикс-итерацию проверяет свежий верификатор
20013
20581
  workspace_id: verifiedRun.workspace_id
20014
20582
  });
20015
- const wt = join31(deps.worktreesDir, basename6(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
20583
+ const wt = join33(deps.worktreesDir, basename6(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
20016
20584
  try {
20017
20585
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
20018
20586
  await deps.git.setWorktreeIdentity(wt, verifier.name);
@@ -20045,7 +20613,7 @@ async function respawnRun(deps, run, mode, nudge) {
20045
20613
  if (mode === "continue") {
20046
20614
  const prompt2 = nudge ?? (run.kind === "hero" ? HERO_CONTINUE_NUDGE : CONTINUE_NUDGE);
20047
20615
  const pid2 = deps.launch({ worktree: wt, run, prompt: prompt2, continueSession: true });
20048
- deps.db.updateRun(run.id, { status: "running", pid: pid2 });
20616
+ deps.db.updateRun(run.id, { status: "running", pid: pid2, ...worktreePathPatch(run, wt) });
20049
20617
  return;
20050
20618
  }
20051
20619
  const [task, verifyMode, projectBase, zoneNotes] = await Promise.all([
@@ -20058,7 +20626,7 @@ async function respawnRun(deps, run, mode, nudge) {
20058
20626
  ]);
20059
20627
  const base3 = effectiveBaseBranch(run.base_branch, projectBase);
20060
20628
  const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
20061
- if (!existsSync18(wt)) {
20629
+ if (!existsSync20(wt)) {
20062
20630
  const branch = runToSpawn.branch || runToSpawn.task_slug;
20063
20631
  const source = await addWorktree(repo, wt, branch, {
20064
20632
  ...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
@@ -20082,10 +20650,13 @@ async function respawnRun(deps, run, mode, nudge) {
20082
20650
  const notice = depsPromptNotice(depsReport);
20083
20651
  const prompt = [freshPrompt, notice, nudge].filter(Boolean).join("\n\n");
20084
20652
  const pid = deps.launch({ worktree: wt, run: runToSpawn, prompt });
20085
- deps.db.updateRun(run.id, { status: "running", pid });
20653
+ deps.db.updateRun(run.id, { status: "running", pid, ...worktreePathPatch(run, wt) });
20654
+ }
20655
+ function worktreePathPatch(run, wt) {
20656
+ return run.worktree_path === wt ? {} : { worktree_path: wt };
20086
20657
  }
20087
20658
  async function syncCrashedOnStartup(deps) {
20088
- const worktreeExists = deps.worktreeExists ?? existsSync18;
20659
+ const worktreeExists = deps.worktreeExists ?? existsSync20;
20089
20660
  let orphaned = 0;
20090
20661
  let merged = 0;
20091
20662
  let requeued = 0;
@@ -20239,7 +20810,7 @@ async function migrateWorktreesAtStartup() {
20239
20810
  legacyDir: WORKTREES_LEGACY_DIR,
20240
20811
  targetDir: WORKTREES_DIR,
20241
20812
  envOverride: process.env.SWARM_WORKTREES || "",
20242
- exists: (d) => existsSync18(d),
20813
+ exists: (d) => existsSync20(d),
20243
20814
  isNonEmptyDir: (d) => {
20244
20815
  try {
20245
20816
  return readdirSync13(d).length > 0;
@@ -20248,13 +20819,13 @@ async function migrateWorktreesAtStartup() {
20248
20819
  }
20249
20820
  },
20250
20821
  liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
20251
- rename: (from, to) => renameSync11(from, to),
20822
+ rename: (from, to) => renameSync12(from, to),
20252
20823
  repairMoved: (t) => repairMovedWorktrees(t),
20253
20824
  log: (m) => console.log(m)
20254
20825
  });
20255
20826
  } catch (e) {
20256
20827
  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);
20257
- return existsSync18(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
20828
+ return existsSync20(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
20258
20829
  }
20259
20830
  }
20260
20831
  async function reportWorkspaceAccess(tl, db) {
@@ -20727,7 +21298,7 @@ async function runDaemon(opts) {
20727
21298
  baseSync: (url) => prBaseSync(url),
20728
21299
  checks: async (url) => {
20729
21300
  const ci = await prCiStatus(url);
20730
- if (ci == null || ci.bucket === "denied") return null;
21301
+ if (ci.bucket === "denied" || ci.bucket === "unavailable") return null;
20731
21302
  return { bucket: ci.bucket };
20732
21303
  }
20733
21304
  } : {}
@@ -20907,7 +21478,7 @@ async function runDaemon(opts) {
20907
21478
  isDirty: (p) => worktreeDirty(p),
20908
21479
  releaseWorktree: (repo, p) => releaseWorktree(repo || opts.repo, p),
20909
21480
  pruneWorktrees: (repo) => pruneWorktrees(repo || opts.repo),
20910
- worktreeExists: existsSync18,
21481
+ worktreeExists: existsSync20,
20911
21482
  liveWorktrees: () => new Set(listDroneProcesses().map((p) => p.worktree)),
20912
21483
  narrate: narrator.narrate
20913
21484
  });
@@ -20947,6 +21518,9 @@ async function runDaemon(opts) {
20947
21518
  // разгрузки (TASWM-316) — заводим ОДИН раз на цикл демона, tickOnce (manageRelief)
20948
21519
  // мутирует эти же поля каждый тик.
20949
21520
  relief: { cost: { pauses: [] } },
21521
+ // Счёт снятий разгрузкой подряд без сдачи (TASWM-531) — durable, в отличие от relief-состояния
21522
+ // выше: он обязан пережить и рестарт демона, и подмену прогона сторожем прогресса.
21523
+ reliefLoop: new ReliefLoopStore(SWARM_HOME),
20950
21524
  // Гейт окружения машины (TASWM-313): не спавним дрона, если проекту не хватает сервисов
20951
21525
  // (лежащий докер = заведомо красные api-тесты). Только client-режим — требования живут в
20952
21526
  // настройках проекта; без него гейта нет и рой работает как раньше. Дедуп событий —
@@ -21030,7 +21604,7 @@ async function runDaemon(opts) {
21030
21604
  mode,
21031
21605
  nudge
21032
21606
  ),
21033
- worktreeExists: (p) => existsSync18(p),
21607
+ worktreeExists: (p) => existsSync20(p),
21034
21608
  // TASWM-514: с этого момента считаем попытки спавна — счётчик живёт ровно одну жизнь
21035
21609
  // процесса демона и рестарт не переживает (см. preStartAttempts).
21036
21610
  startedAt: Date.now(),
@@ -21105,9 +21679,11 @@ async function runDaemon(opts) {
21105
21679
  setTimeout(() => {
21106
21680
  Promise.all([closeWebServer(web.server), droneChannel ? closeDroneChannel(droneChannel.server) : Promise.resolve()]).then(() => {
21107
21681
  if (!stopping) {
21108
- spawn3(process.execPath, respawnArgs, {
21682
+ spawn4(process.execPath, respawnArgs, {
21109
21683
  detached: true,
21110
- stdio: "ignore",
21684
+ // TASWM-497: в ЛОГ, а не в /dev/null. Со `stdio: "ignore"` демон после каждого
21685
+ // `update`/`restart` замолкал навсегда, и разбирать инциденты по логу было нечем.
21686
+ stdio: respawnStdio(DAEMON_LOG_PATH),
21111
21687
  cwd: process.cwd(),
21112
21688
  env: { ...process.env, SWARM_REPO: opts.repo }
21113
21689
  }).unref();
@@ -21497,6 +22073,7 @@ export {
21497
22073
  HERO_CONTINUE_NUDGE,
21498
22074
  OPEN_PR_HOLD_MARK,
21499
22075
  OVERMIND_ENTRY,
22076
+ RELIEF_LOOP_STOP_REASON,
21500
22077
  RESOLVE_ESCALATION_MARK,
21501
22078
  RESOLVE_LIMIT,
21502
22079
  RESPAWN_LIMIT,