@taskless-app/swarm 0.6.32 → 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.
package/dist/overmind.js CHANGED
@@ -3764,9 +3764,9 @@ var init_procCleanup = __esm({
3764
3764
  });
3765
3765
 
3766
3766
  // src/git.ts
3767
- 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";
3768
3768
  import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
3769
- 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";
3770
3770
  import { $ as $2 } from "execa";
3771
3771
  function gitNet(repo, args, timeoutMs) {
3772
3772
  return withHardTimeout(
@@ -3789,7 +3789,7 @@ async function reclaimStaleWorktree(repo, worktree) {
3789
3789
  }
3790
3790
  function isLinkedWorktree(dir) {
3791
3791
  try {
3792
- return statSync8(join18(dir, ".git")).isFile();
3792
+ return statSync8(join19(dir, ".git")).isFile();
3793
3793
  } catch {
3794
3794
  return false;
3795
3795
  }
@@ -3801,7 +3801,7 @@ async function detachWorktree(repo, worktree) {
3801
3801
  }
3802
3802
  }
3803
3803
  async function ensureWorktreeDirGone(worktree) {
3804
- if (!existsSync11(worktree)) return false;
3804
+ if (!existsSync12(worktree)) return false;
3805
3805
  if (!isLinkedWorktree(worktree)) {
3806
3806
  console.error(
3807
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.`
@@ -3816,7 +3816,7 @@ async function ensureWorktreeDirGone(worktree) {
3816
3816
  );
3817
3817
  return true;
3818
3818
  }
3819
- if (existsSync11(worktree)) {
3819
+ if (existsSync12(worktree)) {
3820
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.`);
3821
3821
  }
3822
3822
  return true;
@@ -3864,7 +3864,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
3864
3864
  }
3865
3865
  const busyAt = await worktreeForBranch(repo, branch);
3866
3866
  if (busyAt) {
3867
- const exists = existsSync11(busyAt);
3867
+ const exists = existsSync12(busyAt);
3868
3868
  const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
3869
3869
  if (live) throw new WorktreeBusyError(branch, busyAt);
3870
3870
  if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
@@ -3899,8 +3899,8 @@ async function ensureGitExcluded(worktree, pattern) {
3899
3899
  try {
3900
3900
  const commonDir = await gitPath(worktree, "--git-common-dir");
3901
3901
  if (!commonDir) return false;
3902
- const infoDir = join18(commonDir, "info");
3903
- const excludeFile = join18(infoDir, "exclude");
3902
+ const infoDir = join19(commonDir, "info");
3903
+ const excludeFile = join19(infoDir, "exclude");
3904
3904
  let current = "";
3905
3905
  try {
3906
3906
  current = await readFile(excludeFile, "utf8");
@@ -3920,7 +3920,7 @@ async function gitPath(worktree, flag) {
3920
3920
  const out = await $2({ reject: false })`git -C ${worktree} rev-parse ${flag}`;
3921
3921
  const raw = out.stdout.trim();
3922
3922
  if (out.exitCode !== 0 || !raw) return null;
3923
- return isAbsolute2(raw) ? raw : join18(worktree, raw);
3923
+ return isAbsolute2(raw) ? raw : join19(worktree, raw);
3924
3924
  }
3925
3925
  async function ensureWorktreeHygiene(worktree) {
3926
3926
  for (const pattern of SWARM_EXCLUDE_PATTERNS) await ensureGitExcluded(worktree, pattern);
@@ -3930,12 +3930,12 @@ async function installGuardHooks(worktree) {
3930
3930
  try {
3931
3931
  const gitDir = await gitPath(worktree, "--git-dir");
3932
3932
  if (!gitDir) return false;
3933
- const hooksDir = join18(gitDir, "swarm-hooks");
3933
+ const hooksDir = join19(gitDir, "swarm-hooks");
3934
3934
  const prev = await previousHooksPath(worktree);
3935
3935
  await mkdir(hooksDir, { recursive: true });
3936
3936
  for (const hook of GUARD_HOOKS) {
3937
- await writeFile(join18(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
3938
- 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);
3939
3939
  }
3940
3940
  await $2`git -C ${worktree} config extensions.worktreeConfig true`;
3941
3941
  await $2`git -C ${worktree} config --worktree core.hooksPath ${posix(hooksDir)}`;
@@ -3954,7 +3954,7 @@ async function previousHooksPath(worktree) {
3954
3954
  if (out.exitCode === 0 && value) return value;
3955
3955
  }
3956
3956
  const commonDir = await gitPath(worktree, "--git-common-dir");
3957
- return commonDir ? join18(commonDir, "hooks") : null;
3957
+ return commonDir ? join19(commonDir, "hooks") : null;
3958
3958
  }
3959
3959
  async function removeWorktree(repo, path) {
3960
3960
  const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
@@ -3979,7 +3979,7 @@ async function worktreeDirty(path) {
3979
3979
  async function repairMovedWorktrees(targetDir) {
3980
3980
  const subdirs = (dir) => {
3981
3981
  try {
3982
- 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));
3983
3983
  } catch {
3984
3984
  return [];
3985
3985
  }
@@ -4096,6 +4096,7 @@ var init_git = __esm({
4096
4096
  var droneChannel_exports = {};
4097
4097
  __export(droneChannel_exports, {
4098
4098
  ChannelUnavailableError: () => ChannelUnavailableError,
4099
+ PATCHABLE: () => PATCHABLE,
4099
4100
  channelDroneStore: () => channelDroneStore,
4100
4101
  closeDroneChannel: () => closeDroneChannel,
4101
4102
  loadChannelSecret: () => loadChannelSecret,
@@ -4108,18 +4109,18 @@ __export(droneChannel_exports, {
4108
4109
  verifyLocalCliToken: () => verifyLocalCliToken
4109
4110
  });
4110
4111
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
4111
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync9, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
4112
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync9, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
4112
4113
  import { dirname as dirname4 } from "path";
4113
4114
  import { serve } from "@hono/node-server";
4114
4115
  function loadChannelSecret(path) {
4115
4116
  try {
4116
- const s = readFileSync17(path, "utf8").trim();
4117
+ const s = readFileSync18(path, "utf8").trim();
4117
4118
  if (s.length >= 32) return s;
4118
4119
  } catch {
4119
4120
  }
4120
4121
  const secret = randomBytes(32).toString("hex");
4121
4122
  mkdirSync9(dirname4(path), { recursive: true });
4122
- writeFileSync11(path, secret, { mode: 384 });
4123
+ writeFileSync12(path, secret, { mode: 384 });
4123
4124
  try {
4124
4125
  chmodSync2(path, 384);
4125
4126
  } catch {
@@ -4179,6 +4180,16 @@ function invalidPatchValue(field, v) {
4179
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)}`;
4180
4181
  case "pr_url":
4181
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)}`;
4182
4193
  case "changed_paths": {
4183
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";
4184
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})`;
@@ -4242,10 +4253,18 @@ function makeChannelHandler(deps) {
4242
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");
4243
4254
  const raw = p.patch ?? {};
4244
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
+ };
4245
4264
  for (const [k, v] of Object.entries(raw)) {
4246
- 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`);
4247
4266
  const why = invalidPatchValue(k, v);
4248
- if (why) return bad(400, why);
4267
+ if (why) return refuse(400, why);
4249
4268
  patch[k] = v;
4250
4269
  }
4251
4270
  deps.store.updateRun(id, patch);
@@ -4438,7 +4457,7 @@ function channelDroneStore(opts) {
4438
4457
  openQuestionsFor: async () => (await call("openQuestionsFor")).questions
4439
4458
  };
4440
4459
  }
4441
- 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;
4442
4461
  var init_droneChannel = __esm({
4443
4462
  "src/droneChannel.ts"() {
4444
4463
  "use strict";
@@ -4458,10 +4477,24 @@ var init_droneChannel = __esm({
4458
4477
  // worktree), а не модель — со стороны канала это такой же самоотчёт прогона о себе, как
4459
4478
  // pr_url. Потолки на размер — ниже, в invalidPatchValue: список идёт в гейт раздачи, и
4460
4479
  // сорвавшийся дрон не должен уметь заявить занятым весь репозиторий.
4461
- "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"
4462
4492
  ]);
4463
4493
  CHANGED_PATHS_MAX = 1e3;
4464
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;
4465
4498
  EVENT_KIND_SET = new Set(EVENT_KINDS);
4466
4499
  ChannelUnavailableError = class extends Error {
4467
4500
  constructor(downMs, message) {
@@ -4477,7 +4510,7 @@ var init_droneChannel = __esm({
4477
4510
 
4478
4511
  // src/attachments.ts
4479
4512
  import { mkdir as mkdir3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
4480
- import { join as join22 } from "path";
4513
+ import { join as join23 } from "path";
4481
4514
  function attachmentsFromDoc(doc, origin) {
4482
4515
  const out = [];
4483
4516
  const walk2 = (node) => {
@@ -4568,7 +4601,7 @@ async function materializeAttachments(opts) {
4568
4601
  };
4569
4602
  const result = { files: [], skipped: [] };
4570
4603
  if (opts.refs.length === 0) return result;
4571
- const dir = join22(opts.worktree, ATTACHMENTS_DIR);
4604
+ const dir = join23(opts.worktree, ATTACHMENTS_DIR);
4572
4605
  await io.removeDir(dir);
4573
4606
  let dirReady = false;
4574
4607
  let total = 0;
@@ -4614,7 +4647,7 @@ async function materializeAttachments(opts) {
4614
4647
  await io.mkdir(dir);
4615
4648
  dirReady = true;
4616
4649
  }
4617
- await io.writeFile(join22(dir, name), file.bytes);
4650
+ await io.writeFile(join23(dir, name), file.bytes);
4618
4651
  } catch (e) {
4619
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)})`);
4620
4653
  continue;
@@ -4767,7 +4800,7 @@ var init_attachments = __esm({
4767
4800
 
4768
4801
  // src/shotUpload.ts
4769
4802
  import { readFile as fsReadFile } from "fs/promises";
4770
- import { basename as basename4, isAbsolute as isAbsolute3, join as join23 } from "path";
4803
+ import { basename as basename4, isAbsolute as isAbsolute3, join as join24 } from "path";
4771
4804
  function shotMimetype(path) {
4772
4805
  const dot = path.lastIndexOf(".");
4773
4806
  if (dot < 0) return null;
@@ -4793,7 +4826,7 @@ async function readShots(paths, opts) {
4793
4826
  }
4794
4827
  let bytes;
4795
4828
  try {
4796
- bytes = await read(isAbsolute3(path) ? path : join23(opts.worktree, path));
4829
+ bytes = await read(isAbsolute3(path) ? path : join24(opts.worktree, path));
4797
4830
  } catch (e) {
4798
4831
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
4799
4832
  continue;
@@ -4874,8 +4907,8 @@ var init_shotUpload = __esm({
4874
4907
 
4875
4908
  // src/oauth.ts
4876
4909
  import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
4877
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4 } from "fs";
4878
- import { join as join24 } from "path";
4910
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, unlinkSync as unlinkSync4 } from "fs";
4911
+ import { join as join25 } from "path";
4879
4912
  function makePkce() {
4880
4913
  const verifier = randomBytes2(48).toString("base64url");
4881
4914
  const challenge = createHash3("sha256").update(verifier).digest("base64url");
@@ -4959,13 +4992,13 @@ function toStore(clientId, t, now) {
4959
4992
  }
4960
4993
  function loadStore() {
4961
4994
  try {
4962
- return JSON.parse(readFileSync18(STORE_PATH2, "utf8"));
4995
+ return JSON.parse(readFileSync19(STORE_PATH2, "utf8"));
4963
4996
  } catch {
4964
4997
  return null;
4965
4998
  }
4966
4999
  }
4967
5000
  function saveStore(s) {
4968
- writeFileSync12(STORE_PATH2, JSON.stringify(s, null, 2), { mode: 384 });
5001
+ writeFileSync13(STORE_PATH2, JSON.stringify(s, null, 2), { mode: 384 });
4969
5002
  }
4970
5003
  function clearStore() {
4971
5004
  try {
@@ -4975,7 +5008,7 @@ function clearStore() {
4975
5008
  }
4976
5009
  function loadClientId() {
4977
5010
  try {
4978
- return JSON.parse(readFileSync18(CLIENT_PATH, "utf8")).clientId;
5011
+ return JSON.parse(readFileSync19(CLIENT_PATH, "utf8")).clientId;
4979
5012
  } catch {
4980
5013
  return null;
4981
5014
  }
@@ -4984,7 +5017,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
4984
5017
  const existing = loadClientId();
4985
5018
  if (existing) return existing;
4986
5019
  const clientId = await registerClient({ api, redirectUri }, fetchFn);
4987
- writeFileSync12(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
5020
+ writeFileSync13(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
4988
5021
  return clientId;
4989
5022
  }
4990
5023
  async function ensureAccess(store, opts = {}) {
@@ -5010,8 +5043,8 @@ var init_oauth = __esm({
5010
5043
  AUTHORIZE = "/api/oauth/authorize";
5011
5044
  TOKEN = "/api/oauth/token";
5012
5045
  SCOPE = "openid profile email taskless:full";
5013
- STORE_PATH2 = join24(SWARM_HOME, "oauth.json");
5014
- CLIENT_PATH = join24(SWARM_HOME, "oauth-client.json");
5046
+ STORE_PATH2 = join25(SWARM_HOME, "oauth.json");
5047
+ CLIENT_PATH = join25(SWARM_HOME, "oauth-client.json");
5015
5048
  base2 = (api) => api.replace(/\/$/, "");
5016
5049
  }
5017
5050
  });
@@ -5057,11 +5090,11 @@ var init_authState = __esm({
5057
5090
  });
5058
5091
 
5059
5092
  // src/workspace.ts
5060
- import { readFileSync as readFileSync19, writeFileSync as writeFileSync13, unlinkSync as unlinkSync5 } from "fs";
5061
- import { join as join25 } from "path";
5093
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5 } from "fs";
5094
+ import { join as join26 } from "path";
5062
5095
  function loadWorkspace() {
5063
5096
  try {
5064
- const w = JSON.parse(readFileSync19(WORKSPACE_PATH, "utf8"));
5097
+ const w = JSON.parse(readFileSync20(WORKSPACE_PATH, "utf8"));
5065
5098
  return w && typeof w.id === "string" && w.id ? w : null;
5066
5099
  } catch {
5067
5100
  return null;
@@ -5075,7 +5108,7 @@ var init_workspace = __esm({
5075
5108
  "src/workspace.ts"() {
5076
5109
  "use strict";
5077
5110
  init_config();
5078
- WORKSPACE_PATH = join25(SWARM_HOME, "workspace.json");
5111
+ WORKSPACE_PATH = join26(SWARM_HOME, "workspace.json");
5079
5112
  }
5080
5113
  });
5081
5114
 
@@ -5656,7 +5689,7 @@ __export(db_exports, {
5656
5689
  });
5657
5690
  import { createRequire as createRequire2 } from "module";
5658
5691
  import { randomUUID as randomUUID3 } from "crypto";
5659
- import { existsSync as existsSync14 } from "fs";
5692
+ import { existsSync as existsSync15 } from "fs";
5660
5693
  import { resolve as resolve3 } from "path";
5661
5694
  function rowToQuestion(row) {
5662
5695
  return {
@@ -6279,7 +6312,7 @@ CREATE TABLE IF NOT EXISTS imports (
6279
6312
  const repoN = normalizeRepo(repo);
6280
6313
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
6281
6314
  if (already) return { imported: false, runs: already.run_count };
6282
- if (!existsSync14(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}`);
6283
6316
  const src = new this.databaseCtor(abs);
6284
6317
  try {
6285
6318
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -6648,8 +6681,8 @@ var init_droneStore = __esm({
6648
6681
 
6649
6682
  // src/overmind.ts
6650
6683
  import { homedir as homedir6 } from "os";
6651
- import { existsSync as existsSync19, readFileSync as readFileSync23, readdirSync as readdirSync13, renameSync as renameSync11 } from "fs";
6652
- import { basename as basename6, dirname as dirname5, join as join32 } 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";
6653
6686
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
6654
6687
  import { spawn as spawn4 } from "child_process";
6655
6688
 
@@ -9988,7 +10021,13 @@ var DEFAULT_RELIEF_CONFIG = {
9988
10021
  function resolveReliefConfig(cfg) {
9989
10022
  return { ...DEFAULT_RELIEF_CONFIG, ...cfg ?? {} };
9990
10023
  }
9991
- 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
+ }
9992
10031
  function reliefResumeReason(mode) {
9993
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)`;
9994
10033
  }
@@ -10012,6 +10051,115 @@ function pickResumeCandidate(pausedForRelief) {
10012
10051
  return candidates.reduce((oldest, r) => r.updated_at < oldest.updated_at ? r : oldest);
10013
10052
  }
10014
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
+
10015
10163
  // src/reliefCost.ts
10016
10164
  function median(xs) {
10017
10165
  const s = [...xs].sort((a, b) => a - b);
@@ -10128,15 +10276,15 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
10128
10276
 
10129
10277
  // src/launcher.ts
10130
10278
  import { spawn as spawn2 } from "child_process";
10131
- import { closeSync as closeSync3, existsSync as existsSync10, mkdirSync as mkdirSync8, openSync as openSync3, readdirSync as readdirSync8 } from "fs";
10279
+ import { closeSync as closeSync3, existsSync as existsSync11, mkdirSync as mkdirSync8, openSync as openSync3, readdirSync as readdirSync8 } from "fs";
10132
10280
  import { createRequire } from "module";
10133
10281
  import { homedir as homedir5 } from "os";
10134
- import { dirname as dirname3, join as join15 } from "path";
10282
+ import { dirname as dirname3, join as join16 } from "path";
10135
10283
  import { fileURLToPath as fileURLToPath2 } from "url";
10136
10284
 
10137
10285
  // src/droneHome.ts
10138
10286
  import { lstatSync, mkdirSync as mkdirSync7, readdirSync as readdirSync7, rmSync, statSync as statSync7, symlinkSync as symlinkSync2 } from "fs";
10139
- import { join as join14, relative, sep } from "path";
10287
+ import { join as join15, relative, sep } from "path";
10140
10288
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
10141
10289
  function droneHomeDeny(env = process.env) {
10142
10290
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -10160,20 +10308,20 @@ function buildDroneHome(opts) {
10160
10308
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
10161
10309
  for (const existing of readdirSync7(droneHome)) {
10162
10310
  if (wanted.has(existing)) continue;
10163
- const p = join14(droneHome, existing);
10311
+ const p = join15(droneHome, existing);
10164
10312
  try {
10165
10313
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
10166
10314
  } catch {
10167
10315
  }
10168
10316
  }
10169
10317
  for (const name of wanted) {
10170
- const link2 = join14(droneHome, name);
10318
+ const link2 = join15(droneHome, name);
10171
10319
  try {
10172
10320
  lstatSync(link2);
10173
10321
  continue;
10174
10322
  } catch {
10175
10323
  }
10176
- const target = join14(realHome, name);
10324
+ const target = join15(realHome, name);
10177
10325
  let isDir;
10178
10326
  try {
10179
10327
  isDir = statSync7(target).isDirectory();
@@ -10194,7 +10342,7 @@ var REAL_PATH_ENV_PINS = [
10194
10342
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
10195
10343
  ];
10196
10344
  function realPathEnvPins(realHome) {
10197
- 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)]));
10198
10346
  }
10199
10347
  function dronePrivatePaths(realHome, swarmHome) {
10200
10348
  return [
@@ -10306,7 +10454,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
10306
10454
  const env = { ...base3 };
10307
10455
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
10308
10456
  if (isolate && !base3.TURBO_CACHE_DIR) {
10309
- env.TURBO_CACHE_DIR = join15(worktree, ".turbo", "cache");
10457
+ env.TURBO_CACHE_DIR = join16(worktree, ".turbo", "cache");
10310
10458
  }
10311
10459
  Object.assign(env, droneTestEnv(droneCap, base3));
10312
10460
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -10334,13 +10482,13 @@ var DAEMON_STATE_ENV = [
10334
10482
  "TASKLESS_TOKEN"
10335
10483
  ];
10336
10484
  function bundledRoleDir(kind) {
10337
- return join15(dirname3(fileURLToPath2(import.meta.url)), "..", "roles", kind);
10485
+ return join16(dirname3(fileURLToPath2(import.meta.url)), "..", "roles", kind);
10338
10486
  }
10339
10487
  function roleDir(worktree, kind) {
10340
- return join15(worktree, ".claude", "roles", kind);
10488
+ return join16(worktree, ".claude", "roles", kind);
10341
10489
  }
10342
10490
  function roleDirs(worktree, kind) {
10343
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync10(dir));
10491
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync11(dir));
10344
10492
  }
10345
10493
  function nextRunLogPath(runLogsDir, runLogKeys) {
10346
10494
  const dir = runLogDirForWrite(runLogsDir, runLogKeys);
@@ -10356,7 +10504,7 @@ function nextRunLogPath(runLogsDir, runLogKeys) {
10356
10504
  const m = /^(\d+)\.json/.exec(f);
10357
10505
  if (m) next = Math.max(next, Number(m[1]) + 1);
10358
10506
  }
10359
- return join15(dir, `${next}.json`);
10507
+ return join16(dir, `${next}.json`);
10360
10508
  }
10361
10509
  function openNextRunLog(runLogsDir, runLogKeys) {
10362
10510
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -10439,9 +10587,9 @@ function makeLauncher(cfg) {
10439
10587
  }
10440
10588
  const realHome = cfg.droneIsolation?.realHome ?? homedir5();
10441
10589
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
10442
- 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"))) {
10443
10591
  console.error(
10444
- `[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.`
10445
10593
  );
10446
10594
  }
10447
10595
  const executor = cfg.executor ?? resolveExecutor();
@@ -10502,8 +10650,8 @@ function makeLauncher(cfg) {
10502
10650
  init_procCleanup();
10503
10651
 
10504
10652
  // src/cost.ts
10505
- import { readFileSync as readFileSync13, readdirSync as readdirSync9, renameSync as renameSync8 } from "fs";
10506
- 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";
10507
10655
  init_executors();
10508
10656
  var UNPROCESSED_LOG_RE = /^\d+\.json$/;
10509
10657
  function unprocessedLogFiles(dir) {
@@ -10525,10 +10673,10 @@ function accountRunCost(deps, run) {
10525
10673
  let durationMs = 0;
10526
10674
  const processed = [];
10527
10675
  for (const f of files) {
10528
- const full = join16(dir, f);
10676
+ const full = join17(dir, f);
10529
10677
  let raw = null;
10530
10678
  try {
10531
- raw = readFileSync13(full, "utf8");
10679
+ raw = readFileSync14(full, "utf8");
10532
10680
  } catch {
10533
10681
  continue;
10534
10682
  }
@@ -10554,7 +10702,7 @@ function accountRunCost(deps, run) {
10554
10702
  }
10555
10703
  for (const full of processed) {
10556
10704
  try {
10557
- renameSync8(full, `${full}.done`);
10705
+ renameSync9(full, `${full}.done`);
10558
10706
  } catch {
10559
10707
  }
10560
10708
  }
@@ -10577,8 +10725,8 @@ function makeCostSweeper(deps) {
10577
10725
  }
10578
10726
 
10579
10727
  // src/runOutcome.ts
10580
- import { readdirSync as readdirSync10, readFileSync as readFileSync14 } from "fs";
10581
- import { join as join17 } from "path";
10728
+ import { readdirSync as readdirSync10, readFileSync as readFileSync15 } from "fs";
10729
+ import { join as join18 } from "path";
10582
10730
  init_executors();
10583
10731
  function endedItself(outcome) {
10584
10732
  return outcome === "graceful" || outcome === "budget" || outcome === "aborted";
@@ -10605,7 +10753,7 @@ function readLatestResultRaw(runLogsDir, runLogKeys) {
10605
10753
  }
10606
10754
  if (!latest) return null;
10607
10755
  try {
10608
- return readFileSync14(join17(dir, latest.name), "utf8");
10756
+ return readFileSync15(join18(dir, latest.name), "utf8");
10609
10757
  } catch {
10610
10758
  return null;
10611
10759
  }
@@ -10638,20 +10786,20 @@ init_src();
10638
10786
  init_git();
10639
10787
 
10640
10788
  // src/deps.ts
10641
- import { existsSync as existsSync13, readdirSync as readdirSync12, readFileSync as readFileSync16 } from "fs";
10642
- import { join as join21 } from "path";
10789
+ import { existsSync as existsSync14, readdirSync as readdirSync12, readFileSync as readFileSync17 } from "fs";
10790
+ import { join as join22 } from "path";
10643
10791
  import { execa } from "execa";
10644
10792
 
10645
10793
  // src/prepareLock.ts
10646
10794
  import { createHash } from "crypto";
10647
10795
  import { closeSync as closeSync4, openSync as openSync4, rmSync as rmSync2, statSync as statSync9, writeSync } from "fs";
10648
- import { join as join19 } from "path";
10796
+ import { join as join20 } from "path";
10649
10797
  import { tmpdir } from "os";
10650
10798
  var WAIT_TIMEOUT_MS = 5 * 6e4;
10651
10799
  var POLL_MS = 300;
10652
10800
  var STALE_MS = 20 * 6e4;
10653
10801
  function machineLockPath(key, dir = tmpdir()) {
10654
- 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`);
10655
10803
  }
10656
10804
  function defaultTryCreate(path, content) {
10657
10805
  try {
@@ -10712,8 +10860,8 @@ init_config();
10712
10860
  init_config();
10713
10861
  import { createHash as createHash2 } from "crypto";
10714
10862
  import { cp, link, lstat, mkdir as mkdir2, readdir, readlink, rename, rm as rm2, stat, symlink, utimes, writeFile as writeFile2 } from "fs/promises";
10715
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
10716
- import { join as join20 } from "path";
10863
+ import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
10864
+ import { join as join21 } from "path";
10717
10865
  function fingerprint(parts) {
10718
10866
  const h = createHash2("sha256");
10719
10867
  for (const p of parts) {
@@ -10727,8 +10875,8 @@ async function cloneTree(src, dst) {
10727
10875
  const entries = await readdir(src, { withFileTypes: true });
10728
10876
  await Promise.all(
10729
10877
  entries.map(async (e) => {
10730
- const from = join20(src, e.name);
10731
- const to = join20(dst, e.name);
10878
+ const from = join21(src, e.name);
10879
+ const to = join21(dst, e.name);
10732
10880
  if (e.isDirectory()) return cloneTree(from, to);
10733
10881
  if (e.isSymbolicLink()) {
10734
10882
  return symlink(await readlink(from), to).catch(() => cp(from, to, { recursive: true, verbatimSymlinks: true }));
@@ -10741,11 +10889,11 @@ function cacheRoot(io) {
10741
10889
  return io.root ?? ENV_CACHE_DIR;
10742
10890
  }
10743
10891
  function snapshotDir(io, key) {
10744
- return join20(cacheRoot(io), key);
10892
+ return join21(cacheRoot(io), key);
10745
10893
  }
10746
10894
  function readManifest(dir) {
10747
10895
  try {
10748
- const parsed = JSON.parse(readFileSync15(join20(dir, "manifest.json"), "utf8"));
10896
+ const parsed = JSON.parse(readFileSync16(join21(dir, "manifest.json"), "utf8"));
10749
10897
  if (!parsed || typeof parsed !== "object") return null;
10750
10898
  const m = parsed;
10751
10899
  if (typeof m.key !== "string" || !Array.isArray(m.dirs)) return null;
@@ -10757,11 +10905,11 @@ function readManifest(dir) {
10757
10905
  }
10758
10906
  }
10759
10907
  function hasEnvSnapshot(key, io = {}) {
10760
- const exists = io.exists ?? existsSync12;
10761
- return exists(join20(snapshotDir(io, key), "manifest.json"));
10908
+ const exists = io.exists ?? existsSync13;
10909
+ return exists(join21(snapshotDir(io, key), "manifest.json"));
10762
10910
  }
10763
10911
  async function restoreEnvSnapshot(worktree, key, io = {}) {
10764
- const exists = io.exists ?? existsSync12;
10912
+ const exists = io.exists ?? existsSync13;
10765
10913
  const clone = io.clone ?? cloneTree;
10766
10914
  const dir = snapshotDir(io, key);
10767
10915
  const manifest = readManifest(dir);
@@ -10770,13 +10918,13 @@ async function restoreEnvSnapshot(worktree, key, io = {}) {
10770
10918
  try {
10771
10919
  for (let i = 0; i < manifest.dirs.length; i++) {
10772
10920
  const rel = manifest.dirs[i] ?? "";
10773
- const target = join20(worktree, rel);
10921
+ const target = join21(worktree, rel);
10774
10922
  if (exists(target)) continue;
10775
- await clone(join20(dir, "trees", String(i)), target);
10923
+ await clone(join21(dir, "trees", String(i)), target);
10776
10924
  made.push(rel);
10777
10925
  }
10778
10926
  } catch {
10779
- for (const rel of made) await rm2(join20(worktree, rel), { recursive: true, force: true }).catch(() => {
10927
+ for (const rel of made) await rm2(join21(worktree, rel), { recursive: true, force: true }).catch(() => {
10780
10928
  });
10781
10929
  return null;
10782
10930
  }
@@ -10789,19 +10937,19 @@ async function touch(dir) {
10789
10937
  });
10790
10938
  }
10791
10939
  async function saveEnvSnapshot(worktree, key, dirs, io = {}) {
10792
- const exists = io.exists ?? existsSync12;
10940
+ const exists = io.exists ?? existsSync13;
10793
10941
  const clone = io.clone ?? cloneTree;
10794
10942
  if (hasEnvSnapshot(key, io)) return true;
10795
- const present = dirs.filter((rel) => exists(join20(worktree, rel)));
10943
+ const present = dirs.filter((rel) => exists(join21(worktree, rel)));
10796
10944
  if (!present.length) return false;
10797
10945
  const root = cacheRoot(io);
10798
- const tmp = join20(root, `.tmp-${key}-${process.pid}`);
10946
+ const tmp = join21(root, `.tmp-${key}-${process.pid}`);
10799
10947
  try {
10800
10948
  await rm2(tmp, { recursive: true, force: true });
10801
- await mkdir2(join20(tmp, "trees"), { recursive: true });
10802
- for (let i = 0; i < present.length; i++) await clone(join20(worktree, present[i] ?? ""), join20(tmp, "trees", String(i)));
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)));
10803
10951
  const manifest = { key, dirs: present, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
10804
- await writeFile2(join20(tmp, "manifest.json"), JSON.stringify(manifest, null, 2));
10952
+ await writeFile2(join21(tmp, "manifest.json"), JSON.stringify(manifest, null, 2));
10805
10953
  await rename(tmp, snapshotDir(io, key));
10806
10954
  return true;
10807
10955
  } catch {
@@ -10821,13 +10969,13 @@ async function pruneEnvSnapshots(io = {}) {
10821
10969
  }
10822
10970
  const withTime = [];
10823
10971
  for (const name of names) {
10824
- const s = await stat(join20(root, name)).catch(() => null);
10972
+ const s = await stat(join21(root, name)).catch(() => null);
10825
10973
  if (s) withTime.push({ name, mtime: s.mtimeMs });
10826
10974
  }
10827
10975
  withTime.sort((a, b) => b.mtime - a.mtime);
10828
10976
  let removed = 0;
10829
10977
  for (const { name } of withTime.slice(Math.max(0, keep))) {
10830
- await rm2(join20(root, name), { recursive: true, force: true }).catch(() => {
10978
+ await rm2(join21(root, name), { recursive: true, force: true }).catch(() => {
10831
10979
  });
10832
10980
  removed++;
10833
10981
  }
@@ -10843,9 +10991,9 @@ async function sweepEnvCacheTemp(io = {}) {
10843
10991
  }
10844
10992
  let removed = 0;
10845
10993
  for (const name of names) {
10846
- const s = await lstat(join20(root, name)).catch(() => null);
10994
+ const s = await lstat(join21(root, name)).catch(() => null);
10847
10995
  if (!s || Date.now() - s.mtimeMs < 60 * 6e4) continue;
10848
- await rm2(join20(root, name), { recursive: true, force: true }).catch(() => {
10996
+ await rm2(join21(root, name), { recursive: true, force: true }).catch(() => {
10849
10997
  });
10850
10998
  removed++;
10851
10999
  }
@@ -10872,7 +11020,7 @@ async function runCommand(cmd, worktree) {
10872
11020
  }
10873
11021
  function safeReadText(p) {
10874
11022
  try {
10875
- return readFileSync16(p, "utf8");
11023
+ return readFileSync17(p, "utf8");
10876
11024
  } catch {
10877
11025
  return null;
10878
11026
  }
@@ -10912,21 +11060,21 @@ function overridePlan(override) {
10912
11060
  };
10913
11061
  }
10914
11062
  function detectPackageManager(worktree, io = {}) {
10915
- const exists = io.exists ?? existsSync13;
11063
+ const exists = io.exists ?? existsSync14;
10916
11064
  const readText = io.readText ?? safeReadText;
10917
- const hasPkgJson = exists(join21(worktree, "package.json"));
11065
+ const hasPkgJson = exists(join22(worktree, "package.json"));
10918
11066
  if (!hasPkgJson) {
10919
- if (exists(join21(worktree, "deno.json")) || exists(join21(worktree, "deno.jsonc")) || exists(join21(worktree, "deno.lock"))) return "deno";
11067
+ if (exists(join22(worktree, "deno.json")) || exists(join22(worktree, "deno.jsonc")) || exists(join22(worktree, "deno.lock"))) return "deno";
10920
11068
  return null;
10921
11069
  }
10922
- const pkg = readJson(readText, join21(worktree, "package.json"));
11070
+ const pkg = readJson(readText, join22(worktree, "package.json"));
10923
11071
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
10924
11072
  if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
10925
- if (exists(join21(worktree, "bun.lock")) || exists(join21(worktree, "bun.lockb"))) return "bun";
10926
- if (exists(join21(worktree, "pnpm-lock.yaml"))) return "pnpm";
10927
- if (exists(join21(worktree, "yarn.lock"))) return "yarn";
10928
- if (exists(join21(worktree, "package-lock.json"))) return "npm";
10929
- if (exists(join21(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";
10930
11078
  return "npm";
10931
11079
  }
10932
11080
  function jsInstallCommand(pm) {
@@ -10982,7 +11130,7 @@ var ECOSYSTEM_MARKERS = [
10982
11130
  // конфигурация — дело сборки, не подготовки
10983
11131
  ];
10984
11132
  function detectPlan(worktree, io) {
10985
- const exists = io.exists ?? existsSync13;
11133
+ const exists = io.exists ?? existsSync14;
10986
11134
  const listFiles = io.listFiles ?? safeListFiles;
10987
11135
  const pm = detectPackageManager(worktree, io);
10988
11136
  if (pm) {
@@ -10990,7 +11138,7 @@ function detectPlan(worktree, io) {
10990
11138
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
10991
11139
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
10992
11140
  // заново, но он дешёвый: тянет из того же общего кэша.
10993
- { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join21(worktree, "node_modules"), source: "detect" }
11141
+ { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join22(worktree, "node_modules"), source: "detect" }
10994
11142
  ];
10995
11143
  for (const target of workspaceBuildTargets(worktree, io)) {
10996
11144
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
@@ -10999,12 +11147,12 @@ function detectPlan(worktree, io) {
10999
11147
  return { source: "detect", ecosystem: `node/${pm}`, commands };
11000
11148
  }
11001
11149
  for (const marker of ECOSYSTEM_MARKERS) {
11002
- const hit = marker.files.some((f) => exists(join21(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));
11003
11151
  if (!hit) continue;
11004
11152
  let ecosystem = marker.ecosystem;
11005
11153
  let command = marker.command;
11006
11154
  for (const refine of marker.refine ?? []) {
11007
- if (!exists(join21(worktree, refine.file))) continue;
11155
+ if (!exists(join22(worktree, refine.file))) continue;
11008
11156
  ecosystem = refine.ecosystem;
11009
11157
  command = refine.command;
11010
11158
  break;
@@ -11061,12 +11209,12 @@ var CI_INSTALL_PATTERNS = [
11061
11209
  function ciWorkflowPlan(worktree, io) {
11062
11210
  const readText = io.readText ?? safeReadText;
11063
11211
  const listFiles = io.listFiles ?? safeListFiles;
11064
- const dir = join21(worktree, ".github", "workflows");
11212
+ const dir = join22(worktree, ".github", "workflows");
11065
11213
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
11066
11214
  const seen = /* @__PURE__ */ new Set();
11067
11215
  const commands = [];
11068
11216
  for (const name of files.sort()) {
11069
- const yaml = readText(join21(dir, name));
11217
+ const yaml = readText(join22(dir, name));
11070
11218
  if (yaml === null) continue;
11071
11219
  for (const raw of workflowRunLines(yaml)) {
11072
11220
  const line = raw.replace(/\s+/g, " ").trim();
@@ -11081,7 +11229,7 @@ function ciWorkflowPlan(worktree, io) {
11081
11229
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
11082
11230
  }
11083
11231
  function workspaceGlobs(worktree, readText) {
11084
- const yaml = readText(join21(worktree, "pnpm-workspace.yaml"));
11232
+ const yaml = readText(join22(worktree, "pnpm-workspace.yaml"));
11085
11233
  if (yaml !== null) {
11086
11234
  const globs = [];
11087
11235
  let inPackages = false;
@@ -11101,7 +11249,7 @@ function workspaceGlobs(worktree, readText) {
11101
11249
  }
11102
11250
  if (globs.length) return globs;
11103
11251
  }
11104
- const pkg = readJson(readText, join21(worktree, "package.json"));
11252
+ const pkg = readJson(readText, join22(worktree, "package.json"));
11105
11253
  const ws = pkg?.workspaces;
11106
11254
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
11107
11255
  return list.filter((g) => typeof g === "string");
@@ -11112,14 +11260,14 @@ function workspaceDirs(worktree, globs, io) {
11112
11260
  if (glob.startsWith("!")) continue;
11113
11261
  const star = glob.indexOf("*");
11114
11262
  if (star === -1) {
11115
- const dir = join21(worktree, glob);
11116
- if (io.exists(join21(dir, "package.json"))) dirs.add(dir);
11263
+ const dir = join22(worktree, glob);
11264
+ if (io.exists(join22(dir, "package.json"))) dirs.add(dir);
11117
11265
  continue;
11118
11266
  }
11119
- const base3 = join21(worktree, glob.slice(0, star).replace(/\/$/, ""));
11267
+ const base3 = join22(worktree, glob.slice(0, star).replace(/\/$/, ""));
11120
11268
  for (const name of io.listDirs(base3)) {
11121
- const dir = join21(base3, name);
11122
- if (io.exists(join21(dir, "package.json"))) dirs.add(dir);
11269
+ const dir = join22(base3, name);
11270
+ if (io.exists(join22(dir, "package.json"))) dirs.add(dir);
11123
11271
  }
11124
11272
  }
11125
11273
  return [...dirs];
@@ -11140,18 +11288,18 @@ function entryPaths(pkg) {
11140
11288
  return out;
11141
11289
  }
11142
11290
  function workspaceBuildTargets(worktree, io = {}) {
11143
- const exists = io.exists ?? existsSync13;
11291
+ const exists = io.exists ?? existsSync14;
11144
11292
  const readText = io.readText ?? safeReadText;
11145
11293
  const listDirs = io.listDirs ?? safeListDirs;
11146
11294
  const globs = workspaceGlobs(worktree, readText);
11147
11295
  if (!globs.length) return [];
11148
11296
  const pkgs = [];
11149
11297
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
11150
- const pkg = readJson(readText, join21(dir, "package.json"));
11298
+ const pkg = readJson(readText, join22(dir, "package.json"));
11151
11299
  if (!pkg || typeof pkg.name !== "string") continue;
11152
11300
  const scripts = pkg.scripts ?? {};
11153
11301
  const entries = entryPaths(pkg);
11154
- const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join21(dir, e)));
11302
+ const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join22(dir, e)));
11155
11303
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
11156
11304
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
11157
11305
  }
@@ -11179,7 +11327,7 @@ function devDatabaseUrl(worktree, io = {}) {
11179
11327
  const fromEnv = (io.env ?? process.env).DATABASE_URL;
11180
11328
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
11181
11329
  for (const file of [".env", ".env.example"]) {
11182
- const raw = readText(join21(worktree, file));
11330
+ const raw = readText(join22(worktree, file));
11183
11331
  if (raw === null) continue;
11184
11332
  const m = /^\s*(?:export\s+)?DATABASE_URL\s*=\s*(.*)$/m.exec(raw);
11185
11333
  const value = (m?.[1] ?? "").trim().replace(/^['"]|['"]$/g, "");
@@ -11195,11 +11343,11 @@ function isLocalDatabase(url) {
11195
11343
  }
11196
11344
  }
11197
11345
  function prismaPackages(worktree, io) {
11198
- const exists = io.exists ?? existsSync13;
11346
+ const exists = io.exists ?? existsSync14;
11199
11347
  const readText = io.readText ?? safeReadText;
11200
11348
  const listDirs = io.listDirs ?? safeListDirs;
11201
11349
  const dirs = [worktree, ...workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs })];
11202
- return dirs.filter((dir) => exists(join21(dir, "prisma", "schema.prisma")) && listDirs(join21(dir, "prisma", "migrations")).length > 0);
11350
+ return dirs.filter((dir) => exists(join22(dir, "prisma", "schema.prisma")) && listDirs(join22(dir, "prisma", "migrations")).length > 0);
11203
11351
  }
11204
11352
  function jsExecBin(pm, command) {
11205
11353
  switch (pm) {
@@ -11229,25 +11377,25 @@ function migrateCommands(worktree, pm, io) {
11229
11377
  var ENV_DEFINING_FILES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb", "deno.lock", "pnpm-workspace.yaml"];
11230
11378
  function envCachePlan(worktree, io = {}) {
11231
11379
  if (!ENV_CACHE_ENABLED) return null;
11232
- const exists = io.exists ?? existsSync13;
11380
+ const exists = io.exists ?? existsSync14;
11233
11381
  const readText = io.readText ?? safeReadText;
11234
11382
  const listDirs = io.listDirs ?? safeListDirs;
11235
11383
  const pm = detectPackageManager(worktree, io);
11236
11384
  if (!pm) return null;
11237
11385
  const parts = [`pm:${pm}`];
11238
11386
  for (const name of ENV_DEFINING_FILES) {
11239
- const raw = readText(join21(worktree, name));
11387
+ const raw = readText(join22(worktree, name));
11240
11388
  if (raw !== null) parts.push(`${name}
11241
11389
  ${raw}`);
11242
11390
  }
11243
11391
  const pkgDirs = workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs });
11244
11392
  const rels = pkgDirs.map((dir) => dir.slice(worktree.length + 1)).sort();
11245
11393
  for (const rel of ["", ...rels]) {
11246
- const raw = readText(join21(worktree, rel, "package.json"));
11394
+ const raw = readText(join22(worktree, rel, "package.json"));
11247
11395
  if (raw !== null) parts.push(`${rel}/package.json
11248
11396
  ${raw}`);
11249
11397
  }
11250
- return { key: fingerprint(parts), dirs: ["node_modules", ...rels.map((rel) => join21(rel, "node_modules"))] };
11398
+ return { key: fingerprint(parts), dirs: ["node_modules", ...rels.map((rel) => join22(rel, "node_modules"))] };
11251
11399
  }
11252
11400
  function planPrepare(worktree, io = {}) {
11253
11401
  const plan = overridePlan(io.override) ?? detectPlan(worktree, io) ?? ciWorkflowPlan(worktree, io);
@@ -11255,7 +11403,7 @@ function planPrepare(worktree, io = {}) {
11255
11403
  return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
11256
11404
  }
11257
11405
  async function ensureWorktreeDeps(worktree, io = {}) {
11258
- const exists = io.exists ?? existsSync13;
11406
+ const exists = io.exists ?? existsSync14;
11259
11407
  const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
11260
11408
  const approve = io.approve ?? (() => true);
11261
11409
  const cachePlan = envCachePlan(worktree, io);
@@ -11272,7 +11420,7 @@ async function ensureWorktreeDeps(worktree, io = {}) {
11272
11420
  restored: [],
11273
11421
  cached: false
11274
11422
  };
11275
- if (cachePlan && !exists(join21(worktree, "node_modules"))) {
11423
+ if (cachePlan && !exists(join22(worktree, "node_modules"))) {
11276
11424
  report.restored = await restoreEnvSnapshot(worktree, cachePlan.key, cacheIO) ?? [];
11277
11425
  }
11278
11426
  const plan = planPrepare(worktree, io);
@@ -11561,7 +11709,7 @@ function parkReasonUnretriable(reason) {
11561
11709
  // package.json
11562
11710
  var package_default = {
11563
11711
  name: "@taskless-app/swarm",
11564
- version: "0.6.32",
11712
+ version: "0.6.33",
11565
11713
  type: "module",
11566
11714
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
11567
11715
  keywords: [
@@ -11612,7 +11760,7 @@ var package_default = {
11612
11760
  execa: "^10.0.0",
11613
11761
  zod: "3.25.76"
11614
11762
  },
11615
- gitHead: "f40f0fb4d2f96809f88b7d6ae4f086c5f4f004da"
11763
+ gitHead: "7b17b06b041af953afde46fbd41fc68ebc6dc4e9"
11616
11764
  };
11617
11765
 
11618
11766
  // src/version.ts
@@ -11688,6 +11836,7 @@ function reviewBudgetMark(cycle) {
11688
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";
11689
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";
11690
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";
11691
11840
  var ESCALATIONS = [
11692
11841
  {
11693
11842
  mark: SELF_REFERENCE_MARK,
@@ -11750,6 +11899,21 @@ var ESCALATIONS = [
11750
11899
  sides: ["author"],
11751
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"
11752
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
+ },
11753
11917
  {
11754
11918
  mark: CI_WAIT_ESCALATION_MARK,
11755
11919
  // Сторон НЕТ — и это решение, а не пропуск. Маркер лежит на прогоне РЕВЬЮЕРА (paused,
@@ -12213,7 +12377,7 @@ init_git();
12213
12377
 
12214
12378
  // src/diff.ts
12215
12379
  import { statSync as statSync10 } from "fs";
12216
- import { join as join26 } from "path";
12380
+ import { join as join27 } from "path";
12217
12381
  import { $ as $4 } from "execa";
12218
12382
  var MAX_FILES = 200;
12219
12383
  var MAX_LINES_PER_FILE = 1500;
@@ -12381,7 +12545,7 @@ async function collectRunDiff(worktree, opts = {}) {
12381
12545
  files.push(pf);
12382
12546
  }
12383
12547
  }
12384
- files.sort((a, b) => mtime(join26(worktree, b.path)) - mtime(join26(worktree, a.path)));
12548
+ files.sort((a, b) => mtime(join27(worktree, b.path)) - mtime(join27(worktree, a.path)));
12385
12549
  const truncatedFiles = files.length > MAX_FILES;
12386
12550
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
12387
12551
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -14464,22 +14628,22 @@ function makeMergeGateParts(deps) {
14464
14628
  }
14465
14629
 
14466
14630
  // src/envGate.ts
14467
- import { existsSync as existsSync16 } from "fs";
14468
- import { join as join28 } from "path";
14631
+ import { existsSync as existsSync17 } from "fs";
14632
+ import { join as join29 } from "path";
14469
14633
  import { createConnection } from "net";
14470
14634
  import { execa as execa2 } from "execa";
14471
14635
 
14472
14636
  // src/platform.ts
14473
- import { existsSync as existsSync15, readFileSync as readFileSync20 } from "fs";
14474
- import { delimiter, join as join27 } from "path";
14637
+ import { existsSync as existsSync16, readFileSync as readFileSync21 } from "fs";
14638
+ import { delimiter, join as join28 } from "path";
14475
14639
  function resolveCommandPath(name, env = process.env, platform = process.platform) {
14476
- if (name.includes("/") || name.includes("\\")) return existsSync15(name) ? name : null;
14640
+ if (name.includes("/") || name.includes("\\")) return existsSync16(name) ? name : null;
14477
14641
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
14478
14642
  const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
14479
14643
  for (const dir of dirs) {
14480
14644
  for (const ext of exts) {
14481
- const candidate = join27(dir, name + ext);
14482
- if (existsSync15(candidate)) return candidate;
14645
+ const candidate = join28(dir, name + ext);
14646
+ if (existsSync16(candidate)) return candidate;
14483
14647
  }
14484
14648
  }
14485
14649
  return null;
@@ -14562,8 +14726,8 @@ var COMPOSE_MARKERS = [
14562
14726
  "compose.yaml"
14563
14727
  ];
14564
14728
  function detectRequirements(repo, io = {}) {
14565
- const exists = io.exists ?? existsSync16;
14566
- const marker = COMPOSE_MARKERS.find((f) => exists(join28(repo, f)));
14729
+ const exists = io.exists ?? existsSync17;
14730
+ const marker = COMPOSE_MARKERS.find((f) => exists(join29(repo, f)));
14567
14731
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
14568
14732
  }
14569
14733
  function resolveRequirements(repo, declared, io = {}) {
@@ -16306,22 +16470,22 @@ function closeWebServer(server) {
16306
16470
 
16307
16471
  // src/hostCommands.ts
16308
16472
  init_src();
16309
- import { readFileSync as readFileSync21, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
16310
- import { join as join29 } from "path";
16473
+ import { readFileSync as readFileSync22, renameSync as renameSync10, writeFileSync as writeFileSync15 } from "fs";
16474
+ import { join as join30 } from "path";
16311
16475
  function hostCommandStatePath(home) {
16312
- return join29(home, "host-command.json");
16476
+ return join30(home, "host-command.json");
16313
16477
  }
16314
16478
  function readState(path) {
16315
16479
  try {
16316
- return JSON.parse(readFileSync21(path, "utf8"));
16480
+ return JSON.parse(readFileSync22(path, "utf8"));
16317
16481
  } catch {
16318
16482
  return {};
16319
16483
  }
16320
16484
  }
16321
16485
  function writeState(path, state) {
16322
16486
  try {
16323
- writeFileSync14(`${path}.tmp`, JSON.stringify(state));
16324
- renameSync9(`${path}.tmp`, path);
16487
+ writeFileSync15(`${path}.tmp`, JSON.stringify(state));
16488
+ renameSync10(`${path}.tmp`, path);
16325
16489
  } catch {
16326
16490
  }
16327
16491
  }
@@ -16483,8 +16647,8 @@ async function doRestart(cmd, opts, note) {
16483
16647
 
16484
16648
  // src/selfUpdate.ts
16485
16649
  init_git();
16486
- import { existsSync as existsSync17 } from "fs";
16487
- import { join as join30 } from "path";
16650
+ import { existsSync as existsSync18 } from "fs";
16651
+ import { join as join31 } from "path";
16488
16652
  import { $ as $5 } from "execa";
16489
16653
  var DAEMON_PACKAGE = "@taskless-app/swarm";
16490
16654
  var defaultRun = async (cmd, args, cwd) => {
@@ -16497,7 +16661,7 @@ function tail(text, max = 400) {
16497
16661
  }
16498
16662
  async function runSelfUpdate(io) {
16499
16663
  const run = io.run ?? defaultRun;
16500
- const exists = io.exists ?? existsSync17;
16664
+ const exists = io.exists ?? existsSync18;
16501
16665
  const repo = io.repo;
16502
16666
  await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
16503
16667
  const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
@@ -16526,7 +16690,7 @@ async function runSelfUpdate(io) {
16526
16690
  };
16527
16691
  }
16528
16692
  await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
16529
- if (!exists(join30(repo, "pnpm-lock.yaml"))) {
16693
+ if (!exists(join31(repo, "pnpm-lock.yaml"))) {
16530
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" };
16531
16695
  }
16532
16696
  const installed2 = await run("pnpm", ["install", "--frozen-lockfile"], repo);
@@ -16685,19 +16849,19 @@ async function fetchExternalStorm(url, opts = {}) {
16685
16849
  }
16686
16850
 
16687
16851
  // src/anomalyState.ts
16688
- import { existsSync as existsSync18, readFileSync as readFileSync22, renameSync as renameSync10, writeFileSync as writeFileSync15 } from "fs";
16689
- import { join as join31 } 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";
16690
16854
  var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
16691
16855
  var MAX_RESOLVED = 200;
16692
16856
  function emptyState() {
16693
16857
  return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
16694
16858
  }
16695
16859
  function anomalyStatePath(home) {
16696
- return join31(home, "anomaly-state.json");
16860
+ return join32(home, "anomaly-state.json");
16697
16861
  }
16698
16862
  function loadAnomalyState(home, now = Date.now(), io = {}) {
16699
- const exists = io.exists ?? existsSync18;
16700
- const readFile2 = io.readFile ?? ((p) => readFileSync22(p, "utf8"));
16863
+ const exists = io.exists ?? existsSync19;
16864
+ const readFile2 = io.readFile ?? ((p) => readFileSync23(p, "utf8"));
16701
16865
  const path = anomalyStatePath(home);
16702
16866
  if (!exists(path)) return emptyState();
16703
16867
  try {
@@ -16727,8 +16891,8 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
16727
16891
  function saveAnomalyState(home, state, io = {}) {
16728
16892
  const path = anomalyStatePath(home);
16729
16893
  const writeFile4 = io.writeFile ?? ((p, data) => {
16730
- writeFileSync15(`${p}.tmp`, data);
16731
- renameSync10(`${p}.tmp`, p);
16894
+ writeFileSync16(`${p}.tmp`, data);
16895
+ renameSync11(`${p}.tmp`, p);
16732
16896
  });
16733
16897
  writeFile4(path, JSON.stringify(state, null, 2));
16734
16898
  }
@@ -16882,6 +17046,7 @@ function decideParkAction(db, droneCap, exclusivityBlocked) {
16882
17046
  function decideFreshAction(db, wt) {
16883
17047
  const stalled = db.listRuns("stalled").filter((r) => r.resolves_run_id == null && r.reviews_run_id == null);
16884
17048
  for (const r of stalled) {
17049
+ if (findEscalation(db.events(r.id).map((e) => e.text ?? ""), r, "author")) continue;
16885
17050
  const path = r.worktree_path;
16886
17051
  const recoverable = !!path && wt.exists(path) && wt.hasTranscript(path);
16887
17052
  if (recoverable) continue;
@@ -17229,6 +17394,7 @@ function verdictCommentMark(createdAt) {
17229
17394
  return `\u043A\u043E\u043C\u043C\u0435\u043D\u0442-\u0432\u0435\u0440\u0434\u0438\u043A\u0442@${createdAt}`;
17230
17395
  }
17231
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)";
17232
17398
  function livePpids() {
17233
17399
  return livePpidsFrom(listProcs());
17234
17400
  }
@@ -17854,10 +18020,10 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
17854
18020
  }
17855
18021
  return parts.join("\n");
17856
18022
  }
17857
- var HERO_CHARTER_PATH = join32(dirname5(THIS_FILE), "..", "docs", "HERO.md");
18023
+ var HERO_CHARTER_PATH = join33(dirname5(THIS_FILE), "..", "docs", "HERO.md");
17858
18024
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
17859
18025
  const name = run.name || `#${run.id}`;
17860
- const charter = readFileSync23(HERO_CHARTER_PATH, "utf8");
18026
+ const charter = readFileSync24(HERO_CHARTER_PATH, "utf8");
17861
18027
  const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
17862
18028
 
17863
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).`;
@@ -18165,7 +18331,7 @@ async function wakeRun(deps, runId) {
18165
18331
  if (run.status === "review" && run.pr_url && !needsFixIteration(run) && !stoppedForHuman) {
18166
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` };
18167
18333
  }
18168
- const worktreeExists = deps.worktreeExists ?? existsSync19;
18334
+ const worktreeExists = deps.worktreeExists ?? existsSync20;
18169
18335
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
18170
18336
  const wt = run.worktree_path;
18171
18337
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -18285,7 +18451,7 @@ function repoHasCi(deps, run) {
18285
18451
  function repoHasCiWorkflows(worktreePath, repo) {
18286
18452
  for (const base3 of [worktreePath, repo]) {
18287
18453
  if (!base3) continue;
18288
- const dir = join32(base3, ".github", "workflows");
18454
+ const dir = join33(base3, ".github", "workflows");
18289
18455
  try {
18290
18456
  if (readdirSync13(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
18291
18457
  } catch {
@@ -18471,7 +18637,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
18471
18637
  return true;
18472
18638
  }
18473
18639
  function worktreeAndTranscriptAlive(deps, worktree) {
18474
- const exists = deps.worktreeExists ?? existsSync19;
18640
+ const exists = deps.worktreeExists ?? existsSync20;
18475
18641
  const transcript = deps.hasTranscript ?? executorHasSession;
18476
18642
  return exists(worktree) && transcript(worktree);
18477
18643
  }
@@ -18980,7 +19146,7 @@ async function resumeWatchdogParked(deps, droneCap) {
18980
19146
  }
18981
19147
  if (!candidate) return;
18982
19148
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
18983
- const worktreeExists = deps.worktreeExists ?? existsSync19;
19149
+ const worktreeExists = deps.worktreeExists ?? existsSync20;
18984
19150
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
18985
19151
  const wt = candidate.worktree_path;
18986
19152
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -19095,17 +19261,31 @@ async function manageRelief(deps, now, droneCap, stormActive) {
19095
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);
19096
19262
  }
19097
19263
  }
19264
+ let saved = false;
19098
19265
  if (candidate.worktree_path && deps.pauseSnapshot) {
19099
19266
  try {
19100
- const saved = await deps.pauseSnapshot(candidate.worktree_path);
19267
+ saved = await deps.pauseSnapshot(candidate.worktree_path);
19101
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");
19102
19269
  } catch (e) {
19103
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);
19104
19271
  }
19105
19272
  }
19273
+ const canContinue = !!candidate.worktree_path && worktreeAndTranscriptAlive(deps, candidate.worktree_path);
19106
19274
  deps.db.updateRun(candidate.id, { status: "paused", stop_reason: RELIEF_PAUSE_REASON });
19107
- deps.db.addEvent(candidate.id, "paused", RELIEF_PAUSE_REASON);
19275
+ deps.db.addEvent(candidate.id, "paused", reliefPauseReason(canContinue));
19108
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
+ }
19109
19289
  recordReliefPause(
19110
19290
  state.cost ??= { pauses: [] },
19111
19291
  { runId: candidate.id, label: droneLabel(candidate), at: now, resumedAt: null, stepsSinceResume },
@@ -19137,7 +19317,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
19137
19317
  state.lastLoggedGateReason = null;
19138
19318
  }
19139
19319
  if (countActiveDrones(deps.db) >= droneCap) return;
19140
- 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));
19141
19321
  const recovering = pausedForRelief.length > 0 || state.overloadedAt != null && now - state.overloadedAt < cfg.rampWindowMs;
19142
19322
  if (!recovering) {
19143
19323
  await batchSpawnQueue(deps, droneCap);
@@ -19148,10 +19328,27 @@ async function manageRelief(deps, now, droneCap, stormActive) {
19148
19328
  if (resumeCandidate) {
19149
19329
  if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
19150
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
+ }
19151
19350
  const wt = resumeCandidate.worktree_path;
19152
- const worktreeExists = deps.worktreeExists ?? existsSync19;
19153
- const hasTranscript = deps.hasTranscript ?? executorHasSession;
19154
- const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
19351
+ const canContinue = !!wt && worktreeAndTranscriptAlive(deps, wt);
19155
19352
  const mode = canContinue ? "continue" : "fresh";
19156
19353
  if (mode === "continue" && wt && deps.reclaimWorktree) {
19157
19354
  try {
@@ -19190,7 +19387,7 @@ async function buildReliefCostCandidatesFor(deps, now, droneCap) {
19190
19387
  (runId) => {
19191
19388
  const run = runById.get(runId);
19192
19389
  if (!run) return now;
19193
- if (run.status === "paused" && run.stop_reason === RELIEF_PAUSE_REASON) return null;
19390
+ if (run.status === "paused" && isReliefPause(run.stop_reason)) return null;
19194
19391
  return sqliteMs(run.updated_at);
19195
19392
  },
19196
19393
  now
@@ -19206,7 +19403,7 @@ async function buildReliefCostCandidatesFor(deps, now, droneCap) {
19206
19403
  if (!reliefAlertDue(cost, thresholds)) return [];
19207
19404
  const pressure = deps.hostPressure?.() ?? { culprits: [], readings: [], totalGb: 0 };
19208
19405
  const occupying = allRuns.filter(
19209
- (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))
19210
19407
  );
19211
19408
  const repoOf = (r) => normalizeRepo(r.repo || deps.repoFallback || "");
19212
19409
  const busyRepos = [...new Set(occupying.map(repoOf).filter(Boolean))].filter(
@@ -19427,7 +19624,7 @@ function buildEscalationCandidates(db) {
19427
19624
  }
19428
19625
  async function tickOnce(deps) {
19429
19626
  const now = deps.now ? deps.now() : Date.now();
19430
- const worktreeExists = deps.worktreeExists ?? existsSync19;
19627
+ const worktreeExists = deps.worktreeExists ?? existsSync20;
19431
19628
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
19432
19629
  deps.sampleHost?.();
19433
19630
  if (deps.rehydrate) {
@@ -20013,7 +20210,7 @@ async function tickOnce(deps) {
20013
20210
  }
20014
20211
  for (const run of deps.db.listRuns("paused")) {
20015
20212
  if (run.kind === "hero") continue;
20016
- 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;
20017
20214
  const last = lastActivityMs(deps.db, run);
20018
20215
  if (now - last <= STALL_FLOOR_MS) continue;
20019
20216
  const evs = deps.db.events(run.id);
@@ -20029,7 +20226,7 @@ async function tickOnce(deps) {
20029
20226
  for (const run of deps.db.listRuns("paused")) {
20030
20227
  if (run.kind === "hero") continue;
20031
20228
  if (run.reviews_run_id != null) continue;
20032
- if (run.stop_reason === RELIEF_PAUSE_REASON) continue;
20229
+ if (isReliefPause(run.stop_reason)) continue;
20033
20230
  if (NO_AUTO_WAKE_REASONS.some((r) => run.stop_reason?.startsWith(r))) continue;
20034
20231
  if (run.pid != null && deps.isAlive(run.pid)) continue;
20035
20232
  if (!await projectQuotaGateAllows(deps, run)) continue;
@@ -20223,7 +20420,7 @@ async function resolveConflict(deps, reviewRun) {
20223
20420
  model: reviewRun.model,
20224
20421
  review_model: reviewRun.review_model
20225
20422
  });
20226
- const wt = join32(deps.worktreesDir, basename6(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
20423
+ const wt = join33(deps.worktreesDir, basename6(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
20227
20424
  try {
20228
20425
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
20229
20426
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -20302,7 +20499,7 @@ async function spawnReviewer(deps, reviewRun, files) {
20302
20499
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
20303
20500
  workspace_id: reviewRun.workspace_id
20304
20501
  });
20305
- const wt = join32(deps.worktreesDir, basename6(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
20502
+ const wt = join33(deps.worktreesDir, basename6(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
20306
20503
  try {
20307
20504
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
20308
20505
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -20383,7 +20580,7 @@ async function spawnVerifier(deps, verifiedRun, files) {
20383
20580
  // штамп цикла: фикс-итерацию проверяет свежий верификатор
20384
20581
  workspace_id: verifiedRun.workspace_id
20385
20582
  });
20386
- const wt = join32(deps.worktreesDir, basename6(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
20583
+ const wt = join33(deps.worktreesDir, basename6(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
20387
20584
  try {
20388
20585
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
20389
20586
  await deps.git.setWorktreeIdentity(wt, verifier.name);
@@ -20416,7 +20613,7 @@ async function respawnRun(deps, run, mode, nudge) {
20416
20613
  if (mode === "continue") {
20417
20614
  const prompt2 = nudge ?? (run.kind === "hero" ? HERO_CONTINUE_NUDGE : CONTINUE_NUDGE);
20418
20615
  const pid2 = deps.launch({ worktree: wt, run, prompt: prompt2, continueSession: true });
20419
- deps.db.updateRun(run.id, { status: "running", pid: pid2 });
20616
+ deps.db.updateRun(run.id, { status: "running", pid: pid2, ...worktreePathPatch(run, wt) });
20420
20617
  return;
20421
20618
  }
20422
20619
  const [task, verifyMode, projectBase, zoneNotes] = await Promise.all([
@@ -20429,7 +20626,7 @@ async function respawnRun(deps, run, mode, nudge) {
20429
20626
  ]);
20430
20627
  const base3 = effectiveBaseBranch(run.base_branch, projectBase);
20431
20628
  const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
20432
- if (!existsSync19(wt)) {
20629
+ if (!existsSync20(wt)) {
20433
20630
  const branch = runToSpawn.branch || runToSpawn.task_slug;
20434
20631
  const source = await addWorktree(repo, wt, branch, {
20435
20632
  ...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
@@ -20453,10 +20650,13 @@ async function respawnRun(deps, run, mode, nudge) {
20453
20650
  const notice = depsPromptNotice(depsReport);
20454
20651
  const prompt = [freshPrompt, notice, nudge].filter(Boolean).join("\n\n");
20455
20652
  const pid = deps.launch({ worktree: wt, run: runToSpawn, prompt });
20456
- 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 };
20457
20657
  }
20458
20658
  async function syncCrashedOnStartup(deps) {
20459
- const worktreeExists = deps.worktreeExists ?? existsSync19;
20659
+ const worktreeExists = deps.worktreeExists ?? existsSync20;
20460
20660
  let orphaned = 0;
20461
20661
  let merged = 0;
20462
20662
  let requeued = 0;
@@ -20610,7 +20810,7 @@ async function migrateWorktreesAtStartup() {
20610
20810
  legacyDir: WORKTREES_LEGACY_DIR,
20611
20811
  targetDir: WORKTREES_DIR,
20612
20812
  envOverride: process.env.SWARM_WORKTREES || "",
20613
- exists: (d) => existsSync19(d),
20813
+ exists: (d) => existsSync20(d),
20614
20814
  isNonEmptyDir: (d) => {
20615
20815
  try {
20616
20816
  return readdirSync13(d).length > 0;
@@ -20619,13 +20819,13 @@ async function migrateWorktreesAtStartup() {
20619
20819
  }
20620
20820
  },
20621
20821
  liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
20622
- rename: (from, to) => renameSync11(from, to),
20822
+ rename: (from, to) => renameSync12(from, to),
20623
20823
  repairMoved: (t) => repairMovedWorktrees(t),
20624
20824
  log: (m) => console.log(m)
20625
20825
  });
20626
20826
  } catch (e) {
20627
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);
20628
- return existsSync19(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
20828
+ return existsSync20(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
20629
20829
  }
20630
20830
  }
20631
20831
  async function reportWorkspaceAccess(tl, db) {
@@ -21278,7 +21478,7 @@ async function runDaemon(opts) {
21278
21478
  isDirty: (p) => worktreeDirty(p),
21279
21479
  releaseWorktree: (repo, p) => releaseWorktree(repo || opts.repo, p),
21280
21480
  pruneWorktrees: (repo) => pruneWorktrees(repo || opts.repo),
21281
- worktreeExists: existsSync19,
21481
+ worktreeExists: existsSync20,
21282
21482
  liveWorktrees: () => new Set(listDroneProcesses().map((p) => p.worktree)),
21283
21483
  narrate: narrator.narrate
21284
21484
  });
@@ -21318,6 +21518,9 @@ async function runDaemon(opts) {
21318
21518
  // разгрузки (TASWM-316) — заводим ОДИН раз на цикл демона, tickOnce (manageRelief)
21319
21519
  // мутирует эти же поля каждый тик.
21320
21520
  relief: { cost: { pauses: [] } },
21521
+ // Счёт снятий разгрузкой подряд без сдачи (TASWM-531) — durable, в отличие от relief-состояния
21522
+ // выше: он обязан пережить и рестарт демона, и подмену прогона сторожем прогресса.
21523
+ reliefLoop: new ReliefLoopStore(SWARM_HOME),
21321
21524
  // Гейт окружения машины (TASWM-313): не спавним дрона, если проекту не хватает сервисов
21322
21525
  // (лежащий докер = заведомо красные api-тесты). Только client-режим — требования живут в
21323
21526
  // настройках проекта; без него гейта нет и рой работает как раньше. Дедуп событий —
@@ -21401,7 +21604,7 @@ async function runDaemon(opts) {
21401
21604
  mode,
21402
21605
  nudge
21403
21606
  ),
21404
- worktreeExists: (p) => existsSync19(p),
21607
+ worktreeExists: (p) => existsSync20(p),
21405
21608
  // TASWM-514: с этого момента считаем попытки спавна — счётчик живёт ровно одну жизнь
21406
21609
  // процесса демона и рестарт не переживает (см. preStartAttempts).
21407
21610
  startedAt: Date.now(),
@@ -21870,6 +22073,7 @@ export {
21870
22073
  HERO_CONTINUE_NUDGE,
21871
22074
  OPEN_PR_HOLD_MARK,
21872
22075
  OVERMIND_ENTRY,
22076
+ RELIEF_LOOP_STOP_REASON,
21873
22077
  RESOLVE_ESCALATION_MARK,
21874
22078
  RESOLVE_LIMIT,
21875
22079
  RESPAWN_LIMIT,