@taskless-app/swarm 0.6.39 → 0.6.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +104 -25
  2. package/dist/overmind.js +161 -30
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1366,7 +1366,7 @@ function buildZoneModel(entries, opts = {}) {
1366
1366
  if (!m) labelVotes.set(z, m = /* @__PURE__ */ new Map());
1367
1367
  bump(m, manual);
1368
1368
  }
1369
- const rename = /* @__PURE__ */ new Map();
1369
+ const rename2 = /* @__PURE__ */ new Map();
1370
1370
  const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
1371
1371
  const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
1372
1372
  const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
@@ -1375,11 +1375,11 @@ function buildZoneModel(entries, opts = {}) {
1375
1375
  for (const [zone, votes] of clustersByEvidence) {
1376
1376
  const top = topKey(votes);
1377
1377
  if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
1378
- rename.set(zone, top.key);
1378
+ rename2.set(zone, top.key);
1379
1379
  takenNames.add(top.key);
1380
1380
  }
1381
- const renamed = (z) => rename.get(z) ?? z;
1382
- if (rename.size) {
1381
+ const renamed = (z) => rename2.get(z) ?? z;
1382
+ if (rename2.size) {
1383
1383
  for (const [f, z] of fileZone) fileZone.set(f, renamed(z));
1384
1384
  for (const [s, z] of taskZone) taskZone.set(s, renamed(z));
1385
1385
  }
@@ -3191,7 +3191,7 @@ var init_procList = __esm({
3191
3191
 
3192
3192
  // src/procCleanup.ts
3193
3193
  function sleep(ms) {
3194
- return new Promise((resolve4) => setTimeout(resolve4, ms));
3194
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
3195
3195
  }
3196
3196
  function parseLsofCwd(output) {
3197
3197
  const rows = [];
@@ -3390,7 +3390,7 @@ var init_package = __esm({
3390
3390
  "package.json"() {
3391
3391
  package_default = {
3392
3392
  name: "@taskless-app/swarm",
3393
- version: "0.6.39",
3393
+ version: "0.6.41",
3394
3394
  type: "module",
3395
3395
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
3396
3396
  keywords: [
@@ -3441,7 +3441,7 @@ var init_package = __esm({
3441
3441
  execa: "^10.0.0",
3442
3442
  zod: "3.25.76"
3443
3443
  },
3444
- gitHead: "0c0b1bc0ee05add0d2b5b5d503a05ed7b7834dd8"
3444
+ gitHead: "813dfcc3b086eb6e25089ece0c234cbcf9992ed6"
3445
3445
  };
3446
3446
  }
3447
3447
  });
@@ -3838,9 +3838,9 @@ var init_oauth = __esm({
3838
3838
  });
3839
3839
 
3840
3840
  // src/git.ts
3841
- import { existsSync as existsSync10, readdirSync as readdirSync9, statSync as statSync9 } from "fs";
3842
- import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
3843
- import { isAbsolute, join as join15, sep as sep2 } from "path";
3841
+ import { existsSync as existsSync10, readdirSync as readdirSync9, realpathSync as realpathSync2, statSync as statSync9 } from "fs";
3842
+ import { appendFile, chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
3843
+ import { isAbsolute, join as join15, resolve as resolve2, sep as sep2 } from "path";
3844
3844
  import { $ as $5 } from "execa";
3845
3845
  function gitNet(repo, args, timeoutMs) {
3846
3846
  return withHardTimeout(
@@ -3853,6 +3853,72 @@ function gitNet(repo, args, timeoutMs) {
3853
3853
  async function reclaimStaleWorktree(repo, worktree) {
3854
3854
  await detachWorktree(repo, worktree);
3855
3855
  }
3856
+ async function inspectWorktreePath(path) {
3857
+ if (!existsSync10(path)) return "free";
3858
+ try {
3859
+ if (readdirSync9(path).length === 0) return "free";
3860
+ } catch {
3861
+ return "orphan";
3862
+ }
3863
+ try {
3864
+ if (statSync9(join15(path, ".git")).isDirectory()) return "main-checkout";
3865
+ } catch {
3866
+ }
3867
+ const top = await $5({ reject: false })`git -C ${path} rev-parse --show-toplevel`;
3868
+ return top.exitCode === 0 && samePath(top.stdout.trim(), path) ? "worktree" : "orphan";
3869
+ }
3870
+ function samePath(a, b) {
3871
+ const norm = (p) => {
3872
+ try {
3873
+ return realpathSync2(p);
3874
+ } catch {
3875
+ return resolve2(p);
3876
+ }
3877
+ };
3878
+ const [x, y] = [norm(a), norm(b)];
3879
+ return process.platform === "win32" ? posix(x).toLowerCase() === posix(y).toLowerCase() : x === y;
3880
+ }
3881
+ async function clearWorktreePath(repo, path, branch, opts = {}) {
3882
+ const holder = await inspectWorktreePath(path);
3883
+ if (holder === "free") return;
3884
+ if (holder === "main-checkout") {
3885
+ throw new WorktreeBusyError(
3886
+ branch,
3887
+ path,
3888
+ `\u043F\u043E \u043F\u0443\u0442\u0438 worktree ${path} \u043B\u0435\u0436\u0438\u0442 \u041E\u0421\u041D\u041E\u0412\u041D\u041E\u0419 \u0447\u0435\u043A\u0430\u0443\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (\`.git\` \u2014 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u0430 \u043D\u0435 \u0444\u0430\u0439\u043B) \u2014 \u044F \u0435\u0433\u043E \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E. \u041F\u043E\u0445\u043E\u0436\u0435, \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree'\u0435\u0432 \u0440\u043E\u044F \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D \u043D\u0430 \u0440\u0430\u0431\u043E\u0447\u0443\u044E \u043A\u043E\u043F\u0438\u044E \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430; \u043F\u043E\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0442\u044C, \u0441\u043F\u0430\u0432\u043D \u0442\u0443\u0434\u0430 \u0437\u0430\u0441\u0435\u043B\u044F\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0435\u0442.`
3889
+ );
3890
+ }
3891
+ if (holder === "worktree") {
3892
+ if (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(path) : true) throw new WorktreeBusyError(branch, path);
3893
+ if (await worktreeDirty(path)) {
3894
+ throw new WorktreeBusyError(
3895
+ branch,
3896
+ path,
3897
+ `worktree ${path} \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B, \u043D\u043E \u0432\u043D\u0443\u0442\u0440\u0438 \u0435\u0441\u0442\u044C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 (\u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u043E\u0435 \u0438\u043B\u0438 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435) \u2014 \u043D\u0435 \u0441\u043D\u043E\u0448\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435 \u0441\u0430\u043C\u0438 (\`git -C ${path} status --untracked-files=all\`) \u0438 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u043D\u0435 \u0436\u0430\u043B\u043A\u043E.`
3898
+ );
3899
+ }
3900
+ await detachWorktree(repo, path);
3901
+ opts.onStaleReclaim?.(path);
3902
+ }
3903
+ if (!existsSync10(path)) return;
3904
+ await parkOrphanDir(repo, path, branch);
3905
+ }
3906
+ async function parkOrphanDir(repo, path, branch) {
3907
+ const parked = `${path}.orphan-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
3908
+ try {
3909
+ await rename(path, parked);
3910
+ } catch (e) {
3911
+ throw new WorktreeBusyError(
3912
+ branch,
3913
+ path,
3914
+ `\u043F\u0443\u0442\u044C ${path} \u0437\u0430\u043D\u044F\u0442 \u043F\u043E\u0441\u0442\u043E\u0440\u043E\u043D\u043D\u0438\u043C \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u043E\u043C, \u0438 \u043E\u0442\u043E\u0434\u0432\u0438\u043D\u0443\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${String(e)}) \u2014 \u0441\u043F\u0430\u0432\u043D ${branch} \u0442\u0443\u0434\u0430 \u043D\u0435 \u0432\u0441\u0442\u0430\u043D\u0435\u0442. \u0423\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u0441\u0434\u0435\u043B\u0430\u0439\u0442\u0435 \`git -C ${repo} worktree prune\`.`
3915
+ );
3916
+ }
3917
+ console.error(
3918
+ `[git] \u043F\u043E \u043F\u0443\u0442\u0438 worktree ${path} \u043B\u0435\u0436\u0430\u043B \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 git \u0441\u0432\u043E\u0438\u043C \u0440\u0430\u0431\u043E\u0447\u0438\u043C \u0434\u0435\u0440\u0435\u0432\u043E\u043C \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u2014 \u043E\u0442\u043E\u0434\u0432\u0438\u043D\u0443\u043B \u0432 ${parked}, \u0447\u0442\u043E\u0431\u044B \u0441\u043F\u0430\u0432\u043D ${branch} \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435, \u0447\u0442\u043E \u0432\u043D\u0443\u0442\u0440\u0438, \u0438 \u0443\u0434\u0430\u043B\u0438\u0442\u0435, \u0435\u0441\u043B\u0438 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`
3919
+ );
3920
+ await $5({ reject: false })`git -C ${repo} worktree prune`;
3921
+ }
3856
3922
  function isLinkedWorktree(dir) {
3857
3923
  try {
3858
3924
  return statSync9(join15(dir, ".git")).isFile();
@@ -3925,6 +3991,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
3925
3991
  const originSha = await revParse(repo, `refs/remotes/origin/${branch}`);
3926
3992
  if (!localSha && !originSha) {
3927
3993
  const freshBase = opts.base ?? await freshWorktreeBase(repo);
3994
+ await clearWorktreePath(repo, path, branch, opts);
3928
3995
  await $5`git -C ${repo} worktree add -b ${branch} ${path} ${freshBase}`;
3929
3996
  return { reused: false };
3930
3997
  }
@@ -3937,6 +4004,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
3937
4004
  await reclaimStaleWorktree(repo, busyAt);
3938
4005
  opts.onStaleReclaim?.(busyAt);
3939
4006
  }
4007
+ await clearWorktreePath(repo, path, branch, opts);
3940
4008
  if (originSha) {
3941
4009
  await $5`git -C ${repo} worktree add -B ${branch} ${path} ${`origin/${branch}`}`;
3942
4010
  return { reused: true, from: "origin", sha: originSha };
@@ -4057,8 +4125,14 @@ var init_git = __esm({
4057
4125
  init_netTimeout();
4058
4126
  init_swarmFiles();
4059
4127
  WorktreeBusyError = class extends Error {
4060
- constructor(branch, worktreePath) {
4061
- super(`worktree \u0437\u0430\u043D\u044F\u0442: ${worktreePath} (\u0432\u0435\u0442\u043A\u0430 ${branch}) \u2014 \u043F\u0440\u0438\u0431\u0435\u0440\u0438 worktree \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438 \u043F\u0440\u043E\u0433\u043E\u043D (swarm cancel ${branch})`);
4128
+ /**
4129
+ * `why` (TASWM-553) — ПОЧЕМУ путь не отдали, если причина не «его держит живой прогон»:
4130
+ * основной чекаут по пути worktree, каталог с несданной работой, каталог, который не
4131
+ * получилось убрать. Дефолтная формулировка («прибери worktree или отмени прогон»)
4132
+ * человеку в таких случаях не помогает — она отправляет его не туда.
4133
+ */
4134
+ constructor(branch, worktreePath, why) {
4135
+ super(why ?? `worktree \u0437\u0430\u043D\u044F\u0442: ${worktreePath} (\u0432\u0435\u0442\u043A\u0430 ${branch}) \u2014 \u043F\u0440\u0438\u0431\u0435\u0440\u0438 worktree \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438 \u043F\u0440\u043E\u0433\u043E\u043D (swarm cancel ${branch})`);
4062
4136
  this.branch = branch;
4063
4137
  this.worktreePath = worktreePath;
4064
4138
  this.name = "WorktreeBusyError";
@@ -4353,14 +4427,14 @@ function createNetworkSettleTracker(target) {
4353
4427
  });
4354
4428
  target.on("requestfinished", onDone);
4355
4429
  target.on("requestfailed", onDone);
4356
- const settleOnce = (quietMs, budgetMs) => new Promise((resolve4) => {
4430
+ const settleOnce = (quietMs, budgetMs) => new Promise((resolve5) => {
4357
4431
  let quietTimer;
4358
4432
  const hardTimer = setTimeout(finish, Math.max(0, budgetMs));
4359
4433
  function finish() {
4360
4434
  clearTimeout(quietTimer);
4361
4435
  clearTimeout(hardTimer);
4362
4436
  onChange.delete(check);
4363
- resolve4();
4437
+ resolve5();
4364
4438
  }
4365
4439
  function check() {
4366
4440
  clearTimeout(quietTimer);
@@ -4637,7 +4711,7 @@ __export(db_exports, {
4637
4711
  import { createRequire as createRequire2 } from "module";
4638
4712
  import { randomUUID as randomUUID3 } from "crypto";
4639
4713
  import { existsSync as existsSync12 } from "fs";
4640
- import { resolve as resolve2 } from "path";
4714
+ import { resolve as resolve3 } from "path";
4641
4715
  function rowToQuestion(row) {
4642
4716
  return {
4643
4717
  id: row.id,
@@ -5266,7 +5340,7 @@ CREATE TABLE IF NOT EXISTS imports (
5266
5340
  * транзакции: прервётся — маркер не встанет, повтор безопасен.
5267
5341
  */
5268
5342
  importDb(sourcePath, repo) {
5269
- const abs = resolve2(sourcePath);
5343
+ const abs = resolve3(sourcePath);
5270
5344
  const repoN = normalizeRepo(repo);
5271
5345
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
5272
5346
  if (already) return { imported: false, runs: already.run_count };
@@ -5609,7 +5683,7 @@ var init_repoScan = __esm({
5609
5683
  // src/repoDiscovery.ts
5610
5684
  import { homedir as homedir6 } from "os";
5611
5685
  import { readFileSync as readFileSync15, renameSync as renameSync7, writeFileSync as writeFileSync12 } from "fs";
5612
- import { isAbsolute as isAbsolute3, join as join21, resolve as resolve3 } from "path";
5686
+ import { isAbsolute as isAbsolute3, join as join21, resolve as resolve4 } from "path";
5613
5687
  function repoMapLines(rows) {
5614
5688
  if (!rows?.length) return [];
5615
5689
  const missing = rows.filter((r) => !r.path).length;
@@ -11784,14 +11858,14 @@ function makeHandler(deps) {
11784
11858
  const ref = url.searchParams.get("pr") ?? "";
11785
11859
  if (!ref.trim()) return Response.json({ error: "\u043D\u0443\u0436\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 pr=<url|\u043D\u043E\u043C\u0435\u0440>" }, { status: 400 });
11786
11860
  const ctxRepo = url.searchParams.get("repo");
11787
- const resolve4 = deps.mergeGate ?? makeMergeGateResolver({
11861
+ const resolve5 = deps.mergeGate ?? makeMergeGateResolver({
11788
11862
  listRuns: () => deps.db.listRuns(),
11789
11863
  fallbackAutoReview: SWARM_AUTO_REVIEW,
11790
11864
  autoVerify: SWARM_AUTO_VERIFY,
11791
11865
  // TASWM-344
11792
11866
  ...defaultRepo ? { repoFallback: defaultRepo } : {}
11793
11867
  });
11794
- return Response.json(await resolve4(ref, ctxRepo));
11868
+ return Response.json(await resolve5(ref, ctxRepo));
11795
11869
  }
11796
11870
  if (p === "/resources") return Response.json(hostResourceSnapshot());
11797
11871
  if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
@@ -12678,10 +12752,10 @@ async function restoreDaemon(d, loginOk) {
12678
12752
  d.err("\u0434\u0435\u043C\u043E\u043D \u043E\u0431\u0440\u0430\u0442\u043D\u043E \u041D\u0415 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F (\u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u0432\u044B\u0448\u0435) \u2014 \u0440\u043E\u0439 \u0441\u0442\u043E\u0438\u0442. \u041F\u043E\u0434\u043D\u0438\u043C\u0438 \u0432\u0440\u0443\u0447\u043D\u0443\u044E: swarm start");
12679
12753
  }
12680
12754
  function isPortFree(port, host = "127.0.0.1") {
12681
- return new Promise((resolve4) => {
12755
+ return new Promise((resolve5) => {
12682
12756
  const probe = createServer();
12683
- probe.once("error", () => resolve4(false));
12684
- probe.listen(port, host, () => probe.close(() => resolve4(true)));
12757
+ probe.once("error", () => resolve5(false));
12758
+ probe.listen(port, host, () => probe.close(() => resolve5(true)));
12685
12759
  });
12686
12760
  }
12687
12761
  async function chooseWorkspace(d) {
@@ -13511,7 +13585,7 @@ async function scoutRepo(repo) {
13511
13585
  mkdirSync10(dirname5(abs(rel)), { recursive: true });
13512
13586
  writeFileSync14(abs(rel), text);
13513
13587
  },
13514
- runScout: (prompt) => new Promise((resolve4) => {
13588
+ runScout: (prompt) => new Promise((resolve5) => {
13515
13589
  const child = spawn3(bin, [...ISOLATION_FLAGS, "--dangerously-skip-permissions", "-p", prompt], {
13516
13590
  cwd: worktree,
13517
13591
  stdio: ["ignore", "inherit", "inherit"]
@@ -13519,9 +13593,9 @@ async function scoutRepo(repo) {
13519
13593
  });
13520
13594
  child.on("error", (e) => {
13521
13595
  console.error(`\u043D\u0435 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B\u0441\u044F \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C (${bin}): ${e.message}`);
13522
- resolve4(1);
13596
+ resolve5(1);
13523
13597
  });
13524
- child.on("exit", (c) => resolve4(c ?? 1));
13598
+ child.on("exit", (c) => resolve5(c ?? 1));
13525
13599
  }),
13526
13600
  publish: (files) => publishScoutPr({ worktree, repo, files, name, baseBranch, openPr: openPr.url, fresh: openPr.known && !openPr.url }),
13527
13601
  log: (line) => console.log(line)
@@ -14059,6 +14133,11 @@ switch (cmd) {
14059
14133
  {
14060
14134
  db,
14061
14135
  kill: (pid) => process.kill(pid, "SIGTERM"),
14136
+ // TASWM-553: тот же порядок, что у демона (stop_run / POST /cancel) — снос worktree
14137
+ // ПОСЛЕ смерти процесса, а не следом за просьбой умереть. Здесь это нужнее всего:
14138
+ // сюда попадают ровно тогда, когда демон не ответил, и подчистить за собой некому.
14139
+ isAlive: pidAlive,
14140
+ hardKill: (pid) => process.kill(pid, "SIGKILL"),
14062
14141
  removeWorktree: (repo, p) => {
14063
14142
  const r = repo || SWARM_REPO;
14064
14143
  return r ? removeWorktree(r, p) : Promise.resolve();
package/dist/overmind.js CHANGED
@@ -1593,7 +1593,7 @@ function buildZoneModel(entries, opts = {}) {
1593
1593
  if (!m) labelVotes.set(z2, m = /* @__PURE__ */ new Map());
1594
1594
  bump(m, manual);
1595
1595
  }
1596
- const rename2 = /* @__PURE__ */ new Map();
1596
+ const rename3 = /* @__PURE__ */ new Map();
1597
1597
  const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
1598
1598
  const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
1599
1599
  const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
@@ -1602,11 +1602,11 @@ function buildZoneModel(entries, opts = {}) {
1602
1602
  for (const [zone, votes] of clustersByEvidence) {
1603
1603
  const top = topKey(votes);
1604
1604
  if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
1605
- rename2.set(zone, top.key);
1605
+ rename3.set(zone, top.key);
1606
1606
  takenNames.add(top.key);
1607
1607
  }
1608
- const renamed = (z2) => rename2.get(z2) ?? z2;
1609
- if (rename2.size) {
1608
+ const renamed = (z2) => rename3.get(z2) ?? z2;
1609
+ if (rename3.size) {
1610
1610
  for (const [f, z2] of fileZone) fileZone.set(f, renamed(z2));
1611
1611
  for (const [s, z2] of taskZone) taskZone.set(s, renamed(z2));
1612
1612
  }
@@ -3895,7 +3895,7 @@ var init_procList = __esm({
3895
3895
 
3896
3896
  // src/procCleanup.ts
3897
3897
  function sleep(ms) {
3898
- return new Promise((resolve4) => setTimeout(resolve4, ms));
3898
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
3899
3899
  }
3900
3900
  function parseLsofCwd(output) {
3901
3901
  const rows = [];
@@ -4113,9 +4113,9 @@ var init_procCleanup = __esm({
4113
4113
  });
4114
4114
 
4115
4115
  // src/git.ts
4116
- import { existsSync as existsSync12, readdirSync as readdirSync12, statSync as statSync10 } from "fs";
4117
- import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
4118
- import { isAbsolute as isAbsolute2, join as join20, sep as sep2 } from "path";
4116
+ import { existsSync as existsSync12, readdirSync as readdirSync12, realpathSync as realpathSync2, statSync as statSync10 } from "fs";
4117
+ import { appendFile, chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
4118
+ import { isAbsolute as isAbsolute2, join as join20, resolve as resolve3, sep as sep2 } from "path";
4119
4119
  import { $ as $2 } from "execa";
4120
4120
  function gitNet(repo, args, timeoutMs) {
4121
4121
  return withHardTimeout(
@@ -4136,6 +4136,72 @@ function isUnrecoverableRefError(e) {
4136
4136
  async function reclaimStaleWorktree(repo, worktree) {
4137
4137
  await detachWorktree(repo, worktree);
4138
4138
  }
4139
+ async function inspectWorktreePath(path) {
4140
+ if (!existsSync12(path)) return "free";
4141
+ try {
4142
+ if (readdirSync12(path).length === 0) return "free";
4143
+ } catch {
4144
+ return "orphan";
4145
+ }
4146
+ try {
4147
+ if (statSync10(join20(path, ".git")).isDirectory()) return "main-checkout";
4148
+ } catch {
4149
+ }
4150
+ const top = await $2({ reject: false })`git -C ${path} rev-parse --show-toplevel`;
4151
+ return top.exitCode === 0 && samePath(top.stdout.trim(), path) ? "worktree" : "orphan";
4152
+ }
4153
+ function samePath(a, b) {
4154
+ const norm = (p) => {
4155
+ try {
4156
+ return realpathSync2(p);
4157
+ } catch {
4158
+ return resolve3(p);
4159
+ }
4160
+ };
4161
+ const [x, y] = [norm(a), norm(b)];
4162
+ return process.platform === "win32" ? posix(x).toLowerCase() === posix(y).toLowerCase() : x === y;
4163
+ }
4164
+ async function clearWorktreePath(repo, path, branch, opts = {}) {
4165
+ const holder = await inspectWorktreePath(path);
4166
+ if (holder === "free") return;
4167
+ if (holder === "main-checkout") {
4168
+ throw new WorktreeBusyError(
4169
+ branch,
4170
+ path,
4171
+ `\u043F\u043E \u043F\u0443\u0442\u0438 worktree ${path} \u043B\u0435\u0436\u0438\u0442 \u041E\u0421\u041D\u041E\u0412\u041D\u041E\u0419 \u0447\u0435\u043A\u0430\u0443\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (\`.git\` \u2014 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u0430 \u043D\u0435 \u0444\u0430\u0439\u043B) \u2014 \u044F \u0435\u0433\u043E \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E. \u041F\u043E\u0445\u043E\u0436\u0435, \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree'\u0435\u0432 \u0440\u043E\u044F \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D \u043D\u0430 \u0440\u0430\u0431\u043E\u0447\u0443\u044E \u043A\u043E\u043F\u0438\u044E \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430; \u043F\u043E\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0442\u044C, \u0441\u043F\u0430\u0432\u043D \u0442\u0443\u0434\u0430 \u0437\u0430\u0441\u0435\u043B\u044F\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0435\u0442.`
4172
+ );
4173
+ }
4174
+ if (holder === "worktree") {
4175
+ if (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(path) : true) throw new WorktreeBusyError(branch, path);
4176
+ if (await worktreeDirty(path)) {
4177
+ throw new WorktreeBusyError(
4178
+ branch,
4179
+ path,
4180
+ `worktree ${path} \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B, \u043D\u043E \u0432\u043D\u0443\u0442\u0440\u0438 \u0435\u0441\u0442\u044C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 (\u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u043E\u0435 \u0438\u043B\u0438 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435) \u2014 \u043D\u0435 \u0441\u043D\u043E\u0448\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435 \u0441\u0430\u043C\u0438 (\`git -C ${path} status --untracked-files=all\`) \u0438 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u043D\u0435 \u0436\u0430\u043B\u043A\u043E.`
4181
+ );
4182
+ }
4183
+ await detachWorktree(repo, path);
4184
+ opts.onStaleReclaim?.(path);
4185
+ }
4186
+ if (!existsSync12(path)) return;
4187
+ await parkOrphanDir(repo, path, branch);
4188
+ }
4189
+ async function parkOrphanDir(repo, path, branch) {
4190
+ const parked = `${path}.orphan-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
4191
+ try {
4192
+ await rename(path, parked);
4193
+ } catch (e) {
4194
+ throw new WorktreeBusyError(
4195
+ branch,
4196
+ path,
4197
+ `\u043F\u0443\u0442\u044C ${path} \u0437\u0430\u043D\u044F\u0442 \u043F\u043E\u0441\u0442\u043E\u0440\u043E\u043D\u043D\u0438\u043C \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u043E\u043C, \u0438 \u043E\u0442\u043E\u0434\u0432\u0438\u043D\u0443\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${String(e)}) \u2014 \u0441\u043F\u0430\u0432\u043D ${branch} \u0442\u0443\u0434\u0430 \u043D\u0435 \u0432\u0441\u0442\u0430\u043D\u0435\u0442. \u0423\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u0441\u0434\u0435\u043B\u0430\u0439\u0442\u0435 \`git -C ${repo} worktree prune\`.`
4198
+ );
4199
+ }
4200
+ console.error(
4201
+ `[git] \u043F\u043E \u043F\u0443\u0442\u0438 worktree ${path} \u043B\u0435\u0436\u0430\u043B \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 git \u0441\u0432\u043E\u0438\u043C \u0440\u0430\u0431\u043E\u0447\u0438\u043C \u0434\u0435\u0440\u0435\u0432\u043E\u043C \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u2014 \u043E\u0442\u043E\u0434\u0432\u0438\u043D\u0443\u043B \u0432 ${parked}, \u0447\u0442\u043E\u0431\u044B \u0441\u043F\u0430\u0432\u043D ${branch} \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435, \u0447\u0442\u043E \u0432\u043D\u0443\u0442\u0440\u0438, \u0438 \u0443\u0434\u0430\u043B\u0438\u0442\u0435, \u0435\u0441\u043B\u0438 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`
4202
+ );
4203
+ await $2({ reject: false })`git -C ${repo} worktree prune`;
4204
+ }
4139
4205
  function isLinkedWorktree(dir) {
4140
4206
  try {
4141
4207
  return statSync10(join20(dir, ".git")).isFile();
@@ -4208,6 +4274,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
4208
4274
  const originSha = await revParse(repo, `refs/remotes/origin/${branch}`);
4209
4275
  if (!localSha && !originSha) {
4210
4276
  const freshBase = opts.base ?? await freshWorktreeBase(repo);
4277
+ await clearWorktreePath(repo, path, branch, opts);
4211
4278
  await $2`git -C ${repo} worktree add -b ${branch} ${path} ${freshBase}`;
4212
4279
  return { reused: false };
4213
4280
  }
@@ -4220,6 +4287,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
4220
4287
  await reclaimStaleWorktree(repo, busyAt);
4221
4288
  opts.onStaleReclaim?.(busyAt);
4222
4289
  }
4290
+ await clearWorktreePath(repo, path, branch, opts);
4223
4291
  if (originSha) {
4224
4292
  await $2`git -C ${repo} worktree add -B ${branch} ${path} ${`origin/${branch}`}`;
4225
4293
  return { reused: true, from: "origin", sha: originSha };
@@ -4371,10 +4439,11 @@ async function journalForBranch(repo, branch, slug) {
4371
4439
  const out = await $2({ reject: false })`git -C ${repo} show ${`origin/${branch}:journal/${slug}.md`}`;
4372
4440
  return out.exitCode === 0 && out.stdout ? out.stdout : null;
4373
4441
  }
4374
- async function prepareDetachedWorktree(repo, branch, worktree) {
4442
+ async function prepareDetachedWorktree(repo, branch, worktree, opts = {}) {
4375
4443
  await gitNetStrict(repo, ["fetch", "--quiet", "origin"], GIT_HEAVY_NET_TIMEOUT_MS);
4376
4444
  const ref = `origin/${branch}`;
4377
4445
  if (!await revParse(repo, `${ref}^{commit}`)) throw new MissingRefError(ref, repo);
4446
+ await clearWorktreePath(repo, worktree, branch, opts);
4378
4447
  await $2`git -C ${repo} worktree add --detach --force ${worktree} ${ref}`;
4379
4448
  await ensureWorktreeHygiene(worktree);
4380
4449
  }
@@ -4417,8 +4486,14 @@ var init_git = __esm({
4417
4486
  init_netTimeout();
4418
4487
  init_swarmFiles();
4419
4488
  WorktreeBusyError = class extends Error {
4420
- constructor(branch, worktreePath) {
4421
- super(`worktree \u0437\u0430\u043D\u044F\u0442: ${worktreePath} (\u0432\u0435\u0442\u043A\u0430 ${branch}) \u2014 \u043F\u0440\u0438\u0431\u0435\u0440\u0438 worktree \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438 \u043F\u0440\u043E\u0433\u043E\u043D (swarm cancel ${branch})`);
4489
+ /**
4490
+ * `why` (TASWM-553) — ПОЧЕМУ путь не отдали, если причина не «его держит живой прогон»:
4491
+ * основной чекаут по пути worktree, каталог с несданной работой, каталог, который не
4492
+ * получилось убрать. Дефолтная формулировка («прибери worktree или отмени прогон»)
4493
+ * человеку в таких случаях не помогает — она отправляет его не туда.
4494
+ */
4495
+ constructor(branch, worktreePath, why) {
4496
+ super(why ?? `worktree \u0437\u0430\u043D\u044F\u0442: ${worktreePath} (\u0432\u0435\u0442\u043A\u0430 ${branch}) \u2014 \u043F\u0440\u0438\u0431\u0435\u0440\u0438 worktree \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438 \u043F\u0440\u043E\u0433\u043E\u043D (swarm cancel ${branch})`);
4422
4497
  this.branch = branch;
4423
4498
  this.worktreePath = worktreePath;
4424
4499
  this.name = "WorktreeBusyError";
@@ -4705,8 +4780,8 @@ function startDroneChannel(deps, port) {
4705
4780
  return { port, server };
4706
4781
  }
4707
4782
  function closeDroneChannel(server) {
4708
- return new Promise((resolve4) => {
4709
- server.close(() => resolve4());
4783
+ return new Promise((resolve5) => {
4784
+ server.close(() => resolve5());
4710
4785
  server.closeAllConnections?.();
4711
4786
  });
4712
4787
  }
@@ -6061,7 +6136,7 @@ __export(db_exports, {
6061
6136
  import { createRequire as createRequire2 } from "module";
6062
6137
  import { randomUUID as randomUUID3 } from "crypto";
6063
6138
  import { existsSync as existsSync15 } from "fs";
6064
- import { resolve as resolve3 } from "path";
6139
+ import { resolve as resolve4 } from "path";
6065
6140
  function rowToQuestion(row) {
6066
6141
  return {
6067
6142
  id: row.id,
@@ -6690,7 +6765,7 @@ CREATE TABLE IF NOT EXISTS imports (
6690
6765
  * транзакции: прервётся — маркер не встанет, повтор безопасен.
6691
6766
  */
6692
6767
  importDb(sourcePath, repo) {
6693
- const abs = resolve3(sourcePath);
6768
+ const abs = resolve4(sourcePath);
6694
6769
  const repoN = normalizeRepo(repo);
6695
6770
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
6696
6771
  if (already) return { imported: false, runs: already.run_count };
@@ -11327,7 +11402,7 @@ init_config();
11327
11402
  // src/envCache.ts
11328
11403
  init_config();
11329
11404
  import { createHash as createHash2 } from "crypto";
11330
- import { cp, link, lstat, mkdir as mkdir2, readdir, readlink, rename, rm as rm2, stat, symlink, utimes, writeFile as writeFile2 } from "fs/promises";
11405
+ import { cp, link, lstat, mkdir as mkdir2, readdir, readlink, rename as rename2, rm as rm2, stat, symlink, utimes, writeFile as writeFile2 } from "fs/promises";
11331
11406
  import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
11332
11407
  import { join as join22 } from "path";
11333
11408
  function fingerprint(parts) {
@@ -11418,7 +11493,7 @@ async function saveEnvSnapshot(worktree, key, dirs, io = {}) {
11418
11493
  for (let i = 0; i < present.length; i++) await clone(join22(worktree, present[i] ?? ""), join22(tmp, "trees", String(i)));
11419
11494
  const manifest = { key, dirs: present, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
11420
11495
  await writeFile2(join22(tmp, "manifest.json"), JSON.stringify(manifest, null, 2));
11421
- await rename(tmp, snapshotDir(io, key));
11496
+ await rename2(tmp, snapshotDir(io, key));
11422
11497
  return true;
11423
11498
  } catch {
11424
11499
  await rm2(tmp, { recursive: true, force: true }).catch(() => {
@@ -12219,7 +12294,7 @@ function parkReasonUnretriable(reason) {
12219
12294
  // package.json
12220
12295
  var package_default = {
12221
12296
  name: "@taskless-app/swarm",
12222
- version: "0.6.39",
12297
+ version: "0.6.41",
12223
12298
  type: "module",
12224
12299
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
12225
12300
  keywords: [
@@ -12270,7 +12345,7 @@ var package_default = {
12270
12345
  execa: "^10.0.0",
12271
12346
  zod: "3.25.76"
12272
12347
  },
12273
- gitHead: "0c0b1bc0ee05add0d2b5b5d503a05ed7b7834dd8"
12348
+ gitHead: "813dfcc3b086eb6e25089ece0c234cbcf9992ed6"
12274
12349
  };
12275
12350
 
12276
12351
  // src/version.ts
@@ -12882,10 +12957,28 @@ var CI_INFRA_MARKERS = [
12882
12957
  // PR #1074): «ENOENT: no such file or directory, chmod '…/setup-pnpm/…/pnpm/bin/pnpx.cjs'» —
12883
12958
  // установщик распаковал pnpm и пошёл выставлять права, а файла уже нет. Окно гонки тут не
12884
12959
  // «две джобы стартуют вместе», а миллисекунды посреди работы одного установщика.
12960
+ // CI-118: pnpm ставит corepack, а не pnpm/action-setup, и каталог установки переехал с
12961
+ // `~/setup-pnpm` на `~/.corepack-<раннер>` / `~/.corepack-bin-<раннер>`. Маркер, который
12962
+ // НАЗЫВАЕТ каталог установки, обязан переехать вместе с ним — иначе та же гонка после
12963
+ // правки перестала бы опознаваться и снова будила бы дрона «чинить типы». Ни один путь в
12964
+ // репозитории слова `corepack` не содержит, так что сужение не размывается.
12885
12965
  /ENOENT[^\n]*setup-pnpm/,
12966
+ /ENOENT[^\n]*corepack/,
12886
12967
  /Cannot find module '[^']*setup-pnpm/,
12968
+ /Cannot find module '[^']*corepack/,
12887
12969
  /Cannot find module '[^']*node_modules\/pnpm\/bin\/pnpm\.cjs'/,
12888
12970
  /self-installer exits with code/,
12971
+ // CI-118: раннер не смог скачать сам ЭКШЕН с codeload.github.com — три попытки, каждая по
12972
+ // `HttpClient.Timeout of 100 seconds`, дальше все шаги джобы skipped. Это происходит ДО
12973
+ // первой строки кода и не лечится ничем внутри workflow: архив тянет сам раннер. Чинить
12974
+ // дрону тут нечего по определению — как и в остальных маркерах этого списка.
12975
+ // Берём ТОЛЬКО строку про архив. Голое `Failed to download action` сюда не годится по тому
12976
+ // же правилу, что сузило `Cannot find module`: ею раннер отвечает и на `uses: foo/bar@v99`,
12977
+ // то есть на опечатку в нашем же workflow — дефект кода, и как раз его дрон чинит. Отдай мы
12978
+ // такой отказ за инфраструктуру, дрона бы не разбудили вовсе и красный PR стоял бы до
12979
+ // человека. Транспортный отказ codeload всегда доносит `Failed to download archive … after
12980
+ // N attempts` — по ней и судим.
12981
+ /Failed to download archive/,
12889
12982
  // Сетевые отказы раннера. Тот же класс: чинить нечего, помогает перезапуск. Держатся
12890
12983
  // здесь потому, что ИМЕННО ЭТИ строки уже перечислены в `INFRA_MARKERS`
12891
12984
  // (.github/scripts/typecheck-verdict.sh) — там они не гасят прогон. Пока список был
@@ -15307,11 +15400,11 @@ async function hasCommand(name, env) {
15307
15400
  return hasCommandInPath(name, env);
15308
15401
  }
15309
15402
  async function portOpen(host, port) {
15310
- return new Promise((resolve4) => {
15403
+ return new Promise((resolve5) => {
15311
15404
  const socket = createConnection({ host, port });
15312
15405
  const finish = (ok) => {
15313
15406
  socket.destroy();
15314
- resolve4(ok);
15407
+ resolve5(ok);
15315
15408
  };
15316
15409
  socket.setTimeout(PORT_TIMEOUT_MS);
15317
15410
  socket.once("connect", () => finish(true));
@@ -16860,14 +16953,14 @@ function makeHandler(deps) {
16860
16953
  const ref = url.searchParams.get("pr") ?? "";
16861
16954
  if (!ref.trim()) return Response.json({ error: "\u043D\u0443\u0436\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 pr=<url|\u043D\u043E\u043C\u0435\u0440>" }, { status: 400 });
16862
16955
  const ctxRepo = url.searchParams.get("repo");
16863
- const resolve4 = deps.mergeGate ?? makeMergeGateResolver({
16956
+ const resolve5 = deps.mergeGate ?? makeMergeGateResolver({
16864
16957
  listRuns: () => deps.db.listRuns(),
16865
16958
  fallbackAutoReview: SWARM_AUTO_REVIEW,
16866
16959
  autoVerify: SWARM_AUTO_VERIFY,
16867
16960
  // TASWM-344
16868
16961
  ...defaultRepo ? { repoFallback: defaultRepo } : {}
16869
16962
  });
16870
- return Response.json(await resolve4(ref, ctxRepo));
16963
+ return Response.json(await resolve5(ref, ctxRepo));
16871
16964
  }
16872
16965
  if (p === "/resources") return Response.json(hostResourceSnapshot());
16873
16966
  if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
@@ -17020,8 +17113,8 @@ function startWeb(deps, port = WEB_PORT) {
17020
17113
  return { port, server };
17021
17114
  }
17022
17115
  function closeWebServer(server) {
17023
- return new Promise((resolve4) => {
17024
- server.close(() => resolve4());
17116
+ return new Promise((resolve5) => {
17117
+ server.close(() => resolve5());
17025
17118
  server.closeAllConnections?.();
17026
17119
  });
17027
17120
  }
@@ -17355,7 +17448,7 @@ function executorHealthy(opts = {}) {
17355
17448
  const executor = opts.executor ?? resolveExecutor();
17356
17449
  const bin = opts.bin ?? executor.resolveBin();
17357
17450
  const timeoutMs = opts.timeoutMs ?? 2e4;
17358
- return new Promise((resolve4) => {
17451
+ return new Promise((resolve5) => {
17359
17452
  let done = false;
17360
17453
  let out = "";
17361
17454
  const finish = (r) => {
@@ -17366,7 +17459,7 @@ function executorHealthy(opts = {}) {
17366
17459
  child.kill("SIGKILL");
17367
17460
  } catch {
17368
17461
  }
17369
- resolve4(r);
17462
+ resolve5(r);
17370
17463
  };
17371
17464
  const child = spawn3(bin, [...executor.probe.args], { stdio: ["ignore", "pipe", "pipe"] });
17372
17465
  child.stdout?.on("data", (d) => {
@@ -21160,6 +21253,27 @@ async function replayDecision(deps, qid, chosen, by = "human") {
21160
21253
  deps.db.supersede(old.id, fresh.id, by);
21161
21254
  return fresh;
21162
21255
  }
21256
+ var STOP_POLL_MS = 200;
21257
+ var STOP_TERM_GRACE_MS = 1e4;
21258
+ var STOP_KILL_GRACE_MS = 3e3;
21259
+ async function awaitDroneDeath(deps, pid) {
21260
+ const isAlive = deps.isAlive;
21261
+ if (!isAlive) return false;
21262
+ const wait = deps.wait ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
21263
+ const poll = async (budgetMs) => {
21264
+ for (let spent = 0; spent < budgetMs; spent += STOP_POLL_MS) {
21265
+ if (!isAlive(pid)) return true;
21266
+ await wait(STOP_POLL_MS);
21267
+ }
21268
+ return !isAlive(pid);
21269
+ };
21270
+ if (await poll(STOP_TERM_GRACE_MS)) return true;
21271
+ try {
21272
+ (deps.hardKill ?? deps.kill)(pid);
21273
+ } catch {
21274
+ }
21275
+ return await poll(STOP_KILL_GRACE_MS);
21276
+ }
21163
21277
  async function cancelRun(deps, slug, reason) {
21164
21278
  const run = deps.db.activeRunForSlug(slug);
21165
21279
  if (!run) return null;
@@ -21168,11 +21282,21 @@ async function cancelRun(deps, slug, reason) {
21168
21282
  deps.kill(run.pid);
21169
21283
  } catch {
21170
21284
  }
21285
+ if (deps.isAlive && !await awaitDroneDeath(deps, run.pid)) {
21286
+ deps.db.addEvent(
21287
+ run.id,
21288
+ "anomaly",
21289
+ `\u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0434\u0440\u043E\u043D\u0430 (pid ${run.pid}) \u043F\u0435\u0440\u0435\u0436\u0438\u043B \u0438 SIGTERM, \u0438 SIGKILL \u2014 worktree ${run.worktree_path ?? ""} \u0441\u043D\u043E\u0448\u0443 \u043F\u043E\u0432\u0435\u0440\u0445 \u0436\u0438\u0432\u043E\u0433\u043E. \u0415\u0441\u043B\u0438 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u044D\u0442\u043E\u0433\u043E \u0441\u043B\u0430\u0433\u0430 \u0443\u043F\u0440\u0451\u0442\u0441\u044F \u0432 \u0437\u0430\u043D\u044F\u0442\u044B\u0439 \u043F\u0443\u0442\u044C, \u043D\u0430\u0447\u043D\u0438\u0442\u0435 \u043E\u0442\u0441\u044E\u0434\u0430.`
21290
+ );
21291
+ }
21171
21292
  }
21172
21293
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
21173
21294
  });
21174
21295
  return deps.db.cancel(run.id, reason);
21175
21296
  }
21297
+ function detachedHelperWorktree(db) {
21298
+ return (repo, branch, wt) => prepareDetachedWorktree(repo, branch, wt, { isWorktreeHeldLive: (p) => worktreePathHeldLive(db, p) });
21299
+ }
21176
21300
  async function resolveConflict(deps, reviewRun) {
21177
21301
  const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
21178
21302
  const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
@@ -21987,7 +22111,7 @@ async function runDaemon(opts) {
21987
22111
  repoFallback: opts.repo,
21988
22112
  worktreesDir,
21989
22113
  launch,
21990
- git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
22114
+ git: { prepareDetachedWorktree: detachedHelperWorktree(db), setWorktreeIdentity, removeWorktree },
21991
22115
  // TASWM-356: ветка ревьюера — фактическая ветка PR. Штатно она уже в поле прогона
21992
22116
  // (реконсайл синхронит её из headBranch), этот ридер — для прогонов без неё.
21993
22117
  prHeadBranch,
@@ -22022,7 +22146,7 @@ async function runDaemon(opts) {
22022
22146
  repoFallback: opts.repo,
22023
22147
  worktreesDir,
22024
22148
  launch,
22025
- git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
22149
+ git: { prepareDetachedWorktree: detachedHelperWorktree(db), setWorktreeIdentity, removeWorktree },
22026
22150
  prHeadBranch,
22027
22151
  ...verifyModeReader ? { verifyMode: verifyModeReader } : {},
22028
22152
  ...checkerAttachments ? { taskAttachments: checkerAttachments } : {}
@@ -22366,7 +22490,7 @@ async function runDaemon(opts) {
22366
22490
  repoFallback: opts.repo,
22367
22491
  worktreesDir,
22368
22492
  launch,
22369
- git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree },
22493
+ git: { prepareDetachedWorktree: detachedHelperWorktree(db), setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree },
22370
22494
  prHeadBranch
22371
22495
  // TASWM-356: ветка PR для прогонов без записанного поля branch
22372
22496
  },
@@ -22564,6 +22688,10 @@ async function runDaemon(opts) {
22564
22688
  {
22565
22689
  db,
22566
22690
  kill: (pid) => killProcessGroup(pid, "SIGTERM"),
22691
+ // TASWM-553: остановка обязана ОСВОБОДИТЬ машину — значит, дождаться смерти
22692
+ // процесса и только потом сносить worktree (иначе платит следующий спавн).
22693
+ isAlive: pidAlive,
22694
+ hardKill: (pid) => killProcessGroup(pid, "SIGKILL"),
22567
22695
  removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
22568
22696
  },
22569
22697
  slug,
@@ -22749,6 +22877,9 @@ async function runDaemon(opts) {
22749
22877
  {
22750
22878
  db,
22751
22879
  kill: (pid) => killProcessGroup(pid, "SIGTERM"),
22880
+ isAlive: pidAlive,
22881
+ // TASWM-553: снос worktree — после смерти процесса, не следом за SIGTERM
22882
+ hardKill: (pid) => killProcessGroup(pid, "SIGKILL"),
22752
22883
  removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
22753
22884
  },
22754
22885
  slug,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.39",
3
+ "version": "0.6.41",
4
4
  "type": "module",
5
5
  "description": "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "execa": "^10.0.0",
40
40
  "zod": "3.25.76"
41
41
  },
42
- "gitHead": "0c0b1bc0ee05add0d2b5b5d503a05ed7b7834dd8",
42
+ "gitHead": "813dfcc3b086eb6e25089ece0c234cbcf9992ed6",
43
43
  "scripts": {
44
44
  "overmind": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/overmind.ts",
45
45
  "swarm": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/cli.ts",