@taskless-app/swarm 0.6.0 → 0.6.1

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
@@ -746,10 +746,12 @@ function hostCommandLabel(cmd) {
746
746
  if (isRunScopedCommand(cmd.kind) && cmd.runSlug) return `${base3} ${cmd.runSlug}`;
747
747
  return base3;
748
748
  }
749
- var HOST_COMMAND_KINDS, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, UPDATE_STEP_LABELS, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_LABELS, RUN_SCOPED_COMMAND_KINDS, RUN_SCOPED, MAX_DRONES_LIMIT;
749
+ var SWARM_PACKAGE_NAME, MANUAL_UPDATE_COMMAND, HOST_COMMAND_KINDS, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, UPDATE_STEP_LABELS, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_ACK_GRACE_MS, HOST_COMMAND_MUTE_REASON, HOST_COMMAND_LABELS, RUN_SCOPED_COMMAND_KINDS, RUN_SCOPED, MAX_DRONES_LIMIT;
750
750
  var init_hostCommands = __esm({
751
751
  "../swarm-core/src/hostCommands.ts"() {
752
752
  "use strict";
753
+ SWARM_PACKAGE_NAME = "@taskless-app/swarm";
754
+ MANUAL_UPDATE_COMMAND = `npm i -g ${SWARM_PACKAGE_NAME}@latest`;
753
755
  HOST_COMMAND_KINDS = ["pause", "resume", "restart", "update", "set_max_drones", "stop_run", "wake_run"];
754
756
  HOST_COMMAND_STATUSES = ["pending", "delivered", "running", "done", "failed", "rejected", "expired"];
755
757
  TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "rejected", "expired"]);
@@ -763,6 +765,8 @@ var init_hostCommands = __esm({
763
765
  };
764
766
  HOST_COMMAND_TTL_MS = 30 * 6e4;
765
767
  HOST_COMMAND_KEEPALIVE_MS = 10 * 6e4;
768
+ HOST_COMMAND_ACK_GRACE_MS = 5 * 6e4;
769
+ HOST_COMMAND_MUTE_REASON = `\u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B\u0430 \u0438\u0441\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u2014 \u0432\u0435\u0440\u043E\u044F\u0442\u043D\u043E, \u0434\u0435\u043C\u043E\u043D \u0441\u0442\u0430\u0440\u0435\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B. \u041E\u0431\u043D\u043E\u0432\u0438 \u0432\u0440\u0443\u0447\u043D\u0443\u044E: ${MANUAL_UPDATE_COMMAND}`;
766
770
  HOST_COMMAND_LABELS = {
767
771
  pause: "\u043F\u0430\u0443\u0437\u0430",
768
772
  resume: "\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C",
@@ -3263,7 +3267,7 @@ var init_droneChannel = __esm({
3263
3267
 
3264
3268
  // src/attachments.ts
3265
3269
  import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
3266
- import { join as join17 } from "path";
3270
+ import { join as join18 } from "path";
3267
3271
  function attachmentsFromDoc(doc, origin) {
3268
3272
  const out = [];
3269
3273
  const walk2 = (node) => {
@@ -3354,7 +3358,7 @@ async function materializeAttachments(opts) {
3354
3358
  };
3355
3359
  const result = { files: [], skipped: [] };
3356
3360
  if (opts.refs.length === 0) return result;
3357
- const dir = join17(opts.worktree, ATTACHMENTS_DIR);
3361
+ const dir = join18(opts.worktree, ATTACHMENTS_DIR);
3358
3362
  await io.removeDir(dir);
3359
3363
  let dirReady = false;
3360
3364
  let total = 0;
@@ -3400,7 +3404,7 @@ async function materializeAttachments(opts) {
3400
3404
  await io.mkdir(dir);
3401
3405
  dirReady = true;
3402
3406
  }
3403
- await io.writeFile(join17(dir, name), file.bytes);
3407
+ await io.writeFile(join18(dir, name), file.bytes);
3404
3408
  } catch (e) {
3405
3409
  result.skipped.push(`${label} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText(e)})`);
3406
3410
  continue;
@@ -3553,7 +3557,7 @@ var init_attachments = __esm({
3553
3557
 
3554
3558
  // src/shotUpload.ts
3555
3559
  import { readFile as fsReadFile } from "fs/promises";
3556
- import { basename as basename2, isAbsolute as isAbsolute3, join as join18 } from "path";
3560
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join19 } from "path";
3557
3561
  function shotMimetype(path) {
3558
3562
  const dot = path.lastIndexOf(".");
3559
3563
  if (dot < 0) return null;
@@ -3579,7 +3583,7 @@ async function readShots(paths, opts) {
3579
3583
  }
3580
3584
  let bytes;
3581
3585
  try {
3582
- bytes = await read(isAbsolute3(path) ? path : join18(opts.worktree, path));
3586
+ bytes = await read(isAbsolute3(path) ? path : join19(opts.worktree, path));
3583
3587
  } catch (e) {
3584
3588
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
3585
3589
  continue;
@@ -3656,12 +3660,12 @@ var init_shotUpload = __esm({
3656
3660
  });
3657
3661
 
3658
3662
  // src/oauth.ts
3659
- import { createHash, randomBytes as randomBytes2 } from "crypto";
3663
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
3660
3664
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
3661
- import { join as join19 } from "path";
3665
+ import { join as join20 } from "path";
3662
3666
  function makePkce() {
3663
3667
  const verifier = randomBytes2(48).toString("base64url");
3664
- const challenge = createHash("sha256").update(verifier).digest("base64url");
3668
+ const challenge = createHash2("sha256").update(verifier).digest("base64url");
3665
3669
  return { verifier, challenge };
3666
3670
  }
3667
3671
  async function registerClient(args, fetchFn = fetch) {
@@ -3793,15 +3797,15 @@ var init_oauth = __esm({
3793
3797
  AUTHORIZE = "/api/oauth/authorize";
3794
3798
  TOKEN = "/api/oauth/token";
3795
3799
  SCOPE = "openid profile email taskless:full";
3796
- STORE_PATH = join19(SWARM_HOME, "oauth.json");
3797
- CLIENT_PATH = join19(SWARM_HOME, "oauth-client.json");
3800
+ STORE_PATH = join20(SWARM_HOME, "oauth.json");
3801
+ CLIENT_PATH = join20(SWARM_HOME, "oauth-client.json");
3798
3802
  base = (api) => api.replace(/\/$/, "");
3799
3803
  }
3800
3804
  });
3801
3805
 
3802
3806
  // src/agentToken.ts
3803
3807
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
3804
- import { join as join20 } from "path";
3808
+ import { join as join21 } from "path";
3805
3809
  function loadAgentToken() {
3806
3810
  try {
3807
3811
  const s = JSON.parse(readFileSync15(STORE_PATH2, "utf8"));
@@ -3821,6 +3825,14 @@ async function probeAgentToken(args, fetchFn = fetch) {
3821
3825
  return "unreachable";
3822
3826
  }
3823
3827
  }
3828
+ function isRevokedTokenError(e) {
3829
+ if (e && typeof e === "object" && e.code === 401) return true;
3830
+ const text = e instanceof Error ? e.message : typeof e === "string" ? e : "";
3831
+ return /invalid_token|(?:http|status|code)[ =:]*401\b|\(401\)|401 unauthorized/i.test(text);
3832
+ }
3833
+ function revokedTokenLine() {
3834
+ return "[taskless] \u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u043E\u0437\u0432\u0430\u043D (401 invalid_token) \u2014 \u0440\u043E\u0439 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442 \u0440\u0430\u0431\u043E\u0442\u0443. \u041F\u043E\u0447\u0438\u043D\u0438 \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: swarm login";
3835
+ }
3824
3836
  function revokedTokenComplaint() {
3825
3837
  return [
3826
3838
  "",
@@ -3841,14 +3853,14 @@ var init_agentToken = __esm({
3841
3853
  "src/agentToken.ts"() {
3842
3854
  "use strict";
3843
3855
  init_config();
3844
- STORE_PATH2 = join20(SWARM_HOME, "agent.json");
3856
+ STORE_PATH2 = join21(SWARM_HOME, "agent.json");
3845
3857
  base2 = (api) => api.replace(/\/$/, "");
3846
3858
  }
3847
3859
  });
3848
3860
 
3849
3861
  // src/workspace.ts
3850
3862
  import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
3851
- import { join as join21 } from "path";
3863
+ import { join as join22 } from "path";
3852
3864
  function loadWorkspace() {
3853
3865
  try {
3854
3866
  const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
@@ -3865,7 +3877,7 @@ var init_workspace = __esm({
3865
3877
  "src/workspace.ts"() {
3866
3878
  "use strict";
3867
3879
  init_config();
3868
- WORKSPACE_PATH = join21(SWARM_HOME, "workspace.json");
3880
+ WORKSPACE_PATH = join22(SWARM_HOME, "workspace.json");
3869
3881
  }
3870
3882
  });
3871
3883
 
@@ -4220,7 +4232,9 @@ async function connectTaskless(cfg) {
4220
4232
  try {
4221
4233
  await client.connect(transport, { timeout: TASKLESS_CONNECT_TIMEOUT_MS });
4222
4234
  } catch (e) {
4223
- console.warn(`Taskless: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 slug-only. \u0412\u043E\u0439\u0434\u0438 \u0437\u0430\u043D\u043E\u0432\u043E.`);
4235
+ console.warn(
4236
+ isRevokedTokenError(e) ? `${revokedTokenLine()} (\u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442 \u2014 slug-only \u0440\u0435\u0436\u0438\u043C)` : `Taskless: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 slug-only. \u0412\u043E\u0439\u0434\u0438 \u0437\u0430\u043D\u043E\u0432\u043E.`
4237
+ );
4224
4238
  return null;
4225
4239
  }
4226
4240
  const call = async (name, args) => await client.callTool({ name, arguments: args });
@@ -4244,7 +4258,9 @@ async function connectSwarmMcp(cfg) {
4244
4258
  try {
4245
4259
  await client.connect(transport);
4246
4260
  } catch (e) {
4247
- console.warn(`Swarm MCP: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 \u0431\u0443\u0444\u0435\u0440 \u0432 outbox.`);
4261
+ console.warn(
4262
+ isRevokedTokenError(e) ? `${revokedTokenLine()} (\u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442 \u2014 \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u043A\u043E\u043F\u044F\u0442\u0441\u044F \u0432 outbox)` : `Swarm MCP: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 \u0431\u0443\u0444\u0435\u0440 \u0432 outbox.`
4263
+ );
4248
4264
  return null;
4249
4265
  }
4250
4266
  return async (name, args) => await client.callTool({ name, arguments: swarmToolArgs(name, args, workspace) });
@@ -5169,7 +5185,7 @@ var init_droneStore = __esm({
5169
5185
  // src/overmind.ts
5170
5186
  import { homedir as homedir5 } from "os";
5171
5187
  import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync12, renameSync as renameSync10 } from "fs";
5172
- import { basename as basename4, dirname as dirname5, join as join28 } from "path";
5188
+ import { basename as basename4, dirname as dirname4, join as join29 } from "path";
5173
5189
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
5174
5190
  import { spawn as spawn3 } from "child_process";
5175
5191
 
@@ -8231,19 +8247,89 @@ init_git();
8231
8247
 
8232
8248
  // src/deps.ts
8233
8249
  import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync12 } from "fs";
8234
- import { join as join16 } from "path";
8250
+ import { join as join17 } from "path";
8235
8251
  import { execa } from "execa";
8252
+
8253
+ // src/prepareLock.ts
8254
+ import { createHash } from "crypto";
8255
+ import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as statSync8, writeSync } from "fs";
8256
+ import { join as join16 } from "path";
8257
+ import { tmpdir } from "os";
8258
+ var WAIT_TIMEOUT_MS = 5 * 6e4;
8259
+ var POLL_MS = 300;
8260
+ var STALE_MS = 20 * 6e4;
8261
+ function machineLockPath(key, dir = tmpdir()) {
8262
+ return join16(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
8263
+ }
8264
+ function defaultTryCreate(path, content) {
8265
+ try {
8266
+ const fd = openSync3(path, "wx");
8267
+ try {
8268
+ writeSync(fd, content);
8269
+ } finally {
8270
+ closeSync3(fd);
8271
+ }
8272
+ return true;
8273
+ } catch {
8274
+ return false;
8275
+ }
8276
+ }
8277
+ function defaultAgeMs(path) {
8278
+ try {
8279
+ return Date.now() - statSync8(path).mtimeMs;
8280
+ } catch {
8281
+ return null;
8282
+ }
8283
+ }
8284
+ async function withMachineLock(key, fn, io = {}) {
8285
+ const tryCreate = io.tryCreate ?? defaultTryCreate;
8286
+ const remove = io.remove ?? ((p) => {
8287
+ try {
8288
+ rmSync2(p, { force: true });
8289
+ } catch {
8290
+ }
8291
+ });
8292
+ const ageMs2 = io.ageMs ?? defaultAgeMs;
8293
+ const wait = io.wait ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
8294
+ const now = io.now ?? Date.now;
8295
+ const path = machineLockPath(key, io.dir ?? tmpdir());
8296
+ const deadline = now() + WAIT_TIMEOUT_MS;
8297
+ let locked = false;
8298
+ for (; ; ) {
8299
+ if (tryCreate(path, `${process.pid}
8300
+ `)) {
8301
+ locked = true;
8302
+ break;
8303
+ }
8304
+ const age = ageMs2(path);
8305
+ if (age !== null && age > STALE_MS) remove(path);
8306
+ if (now() >= deadline) break;
8307
+ await wait(POLL_MS);
8308
+ }
8309
+ try {
8310
+ return await fn(locked);
8311
+ } finally {
8312
+ if (locked) remove(path);
8313
+ }
8314
+ }
8315
+
8316
+ // src/deps.ts
8236
8317
  var PREPARE_TIMEOUT_MS = 15 * 6e4;
8237
8318
  var MAX_PLAN_COMMANDS = 12;
8238
8319
  async function runCommand(cmd, worktree) {
8239
- const res = await execa(cmd.run, {
8240
- cwd: cmd.cwd ?? worktree,
8241
- shell: true,
8242
- // команда — строка целиком (у настройки/CI иначе никак), поэтому именно шелл
8243
- reject: false,
8244
- timeout: PREPARE_TIMEOUT_MS
8245
- });
8246
- return res.exitCode === 0;
8320
+ const exec = async () => {
8321
+ const res = await execa(cmd.run, {
8322
+ cwd: cmd.cwd ?? worktree,
8323
+ shell: true,
8324
+ // команда — строка целиком (у настройки/CI иначе никак), поэтому именно шелл
8325
+ reject: false,
8326
+ timeout: PREPARE_TIMEOUT_MS,
8327
+ ...cmd.env ? { env: cmd.env } : {}
8328
+ // execa мержит с окружением процесса, не заменяет его
8329
+ });
8330
+ return res.exitCode === 0;
8331
+ };
8332
+ return cmd.lock ? withMachineLock(cmd.lock, exec) : exec();
8247
8333
  }
8248
8334
  function safeReadText(p) {
8249
8335
  try {
@@ -8289,19 +8375,19 @@ function overridePlan(override) {
8289
8375
  function detectPackageManager(worktree, io = {}) {
8290
8376
  const exists = io.exists ?? existsSync10;
8291
8377
  const readText = io.readText ?? safeReadText;
8292
- const hasPkgJson = exists(join16(worktree, "package.json"));
8378
+ const hasPkgJson = exists(join17(worktree, "package.json"));
8293
8379
  if (!hasPkgJson) {
8294
- if (exists(join16(worktree, "deno.json")) || exists(join16(worktree, "deno.jsonc")) || exists(join16(worktree, "deno.lock"))) return "deno";
8380
+ if (exists(join17(worktree, "deno.json")) || exists(join17(worktree, "deno.jsonc")) || exists(join17(worktree, "deno.lock"))) return "deno";
8295
8381
  return null;
8296
8382
  }
8297
- const pkg = readJson(readText, join16(worktree, "package.json"));
8383
+ const pkg = readJson(readText, join17(worktree, "package.json"));
8298
8384
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
8299
8385
  if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
8300
- if (exists(join16(worktree, "bun.lock")) || exists(join16(worktree, "bun.lockb"))) return "bun";
8301
- if (exists(join16(worktree, "pnpm-lock.yaml"))) return "pnpm";
8302
- if (exists(join16(worktree, "yarn.lock"))) return "yarn";
8303
- if (exists(join16(worktree, "package-lock.json"))) return "npm";
8304
- if (exists(join16(worktree, "deno.lock"))) return "deno";
8386
+ if (exists(join17(worktree, "bun.lock")) || exists(join17(worktree, "bun.lockb"))) return "bun";
8387
+ if (exists(join17(worktree, "pnpm-lock.yaml"))) return "pnpm";
8388
+ if (exists(join17(worktree, "yarn.lock"))) return "yarn";
8389
+ if (exists(join17(worktree, "package-lock.json"))) return "npm";
8390
+ if (exists(join17(worktree, "deno.lock"))) return "deno";
8305
8391
  return "npm";
8306
8392
  }
8307
8393
  function jsInstallCommand(pm) {
@@ -8362,20 +8448,21 @@ function detectPlan(worktree, io) {
8362
8448
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
8363
8449
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
8364
8450
  // заново, но он дешёвый: тянет из того же общего кэша.
8365
- { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join16(worktree, "node_modules"), source: "detect" }
8451
+ { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join17(worktree, "node_modules"), source: "detect" }
8366
8452
  ];
8367
8453
  for (const target of workspaceBuildTargets(worktree, io)) {
8368
8454
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
8369
8455
  }
8456
+ commands.push(...migrateCommands(worktree, pm, io));
8370
8457
  return { source: "detect", ecosystem: `node/${pm}`, commands };
8371
8458
  }
8372
8459
  for (const marker of ECOSYSTEM_MARKERS) {
8373
- const hit = marker.files.some((f) => exists(join16(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
8460
+ const hit = marker.files.some((f) => exists(join17(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
8374
8461
  if (!hit) continue;
8375
8462
  let ecosystem = marker.ecosystem;
8376
8463
  let command = marker.command;
8377
8464
  for (const refine of marker.refine ?? []) {
8378
- if (!exists(join16(worktree, refine.file))) continue;
8465
+ if (!exists(join17(worktree, refine.file))) continue;
8379
8466
  ecosystem = refine.ecosystem;
8380
8467
  command = refine.command;
8381
8468
  break;
@@ -8432,12 +8519,12 @@ var CI_INSTALL_PATTERNS = [
8432
8519
  function ciWorkflowPlan(worktree, io) {
8433
8520
  const readText = io.readText ?? safeReadText;
8434
8521
  const listFiles = io.listFiles ?? safeListFiles;
8435
- const dir = join16(worktree, ".github", "workflows");
8522
+ const dir = join17(worktree, ".github", "workflows");
8436
8523
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
8437
8524
  const seen = /* @__PURE__ */ new Set();
8438
8525
  const commands = [];
8439
8526
  for (const name of files.sort()) {
8440
- const yaml = readText(join16(dir, name));
8527
+ const yaml = readText(join17(dir, name));
8441
8528
  if (yaml === null) continue;
8442
8529
  for (const raw of workflowRunLines(yaml)) {
8443
8530
  const line = raw.replace(/\s+/g, " ").trim();
@@ -8452,7 +8539,7 @@ function ciWorkflowPlan(worktree, io) {
8452
8539
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
8453
8540
  }
8454
8541
  function workspaceGlobs(worktree, readText) {
8455
- const yaml = readText(join16(worktree, "pnpm-workspace.yaml"));
8542
+ const yaml = readText(join17(worktree, "pnpm-workspace.yaml"));
8456
8543
  if (yaml !== null) {
8457
8544
  const globs = [];
8458
8545
  let inPackages = false;
@@ -8472,7 +8559,7 @@ function workspaceGlobs(worktree, readText) {
8472
8559
  }
8473
8560
  if (globs.length) return globs;
8474
8561
  }
8475
- const pkg = readJson(readText, join16(worktree, "package.json"));
8562
+ const pkg = readJson(readText, join17(worktree, "package.json"));
8476
8563
  const ws = pkg?.workspaces;
8477
8564
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
8478
8565
  return list.filter((g) => typeof g === "string");
@@ -8483,14 +8570,14 @@ function workspaceDirs(worktree, globs, io) {
8483
8570
  if (glob.startsWith("!")) continue;
8484
8571
  const star = glob.indexOf("*");
8485
8572
  if (star === -1) {
8486
- const dir = join16(worktree, glob);
8487
- if (io.exists(join16(dir, "package.json"))) dirs.add(dir);
8573
+ const dir = join17(worktree, glob);
8574
+ if (io.exists(join17(dir, "package.json"))) dirs.add(dir);
8488
8575
  continue;
8489
8576
  }
8490
- const base3 = join16(worktree, glob.slice(0, star).replace(/\/$/, ""));
8577
+ const base3 = join17(worktree, glob.slice(0, star).replace(/\/$/, ""));
8491
8578
  for (const name of io.listDirs(base3)) {
8492
- const dir = join16(base3, name);
8493
- if (io.exists(join16(dir, "package.json"))) dirs.add(dir);
8579
+ const dir = join17(base3, name);
8580
+ if (io.exists(join17(dir, "package.json"))) dirs.add(dir);
8494
8581
  }
8495
8582
  }
8496
8583
  return [...dirs];
@@ -8518,11 +8605,11 @@ function workspaceBuildTargets(worktree, io = {}) {
8518
8605
  if (!globs.length) return [];
8519
8606
  const pkgs = [];
8520
8607
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
8521
- const pkg = readJson(readText, join16(dir, "package.json"));
8608
+ const pkg = readJson(readText, join17(dir, "package.json"));
8522
8609
  if (!pkg || typeof pkg.name !== "string") continue;
8523
8610
  const scripts = pkg.scripts ?? {};
8524
8611
  const entries = entryPaths(pkg);
8525
- const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join16(dir, e)));
8612
+ const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join17(dir, e)));
8526
8613
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
8527
8614
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
8528
8615
  }
@@ -8544,6 +8631,59 @@ function workspaceBuildTargets(worktree, io = {}) {
8544
8631
  for (const p of targets.values()) visit(p);
8545
8632
  return ordered;
8546
8633
  }
8634
+ var LOCAL_DB_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]", "host.docker.internal"]);
8635
+ function devDatabaseUrl(worktree, io = {}) {
8636
+ const readText = io.readText ?? safeReadText;
8637
+ const fromEnv = (io.env ?? process.env).DATABASE_URL;
8638
+ if (fromEnv && fromEnv.trim()) return fromEnv.trim();
8639
+ for (const file of [".env", ".env.example"]) {
8640
+ const raw = readText(join17(worktree, file));
8641
+ if (raw === null) continue;
8642
+ const m = /^\s*(?:export\s+)?DATABASE_URL\s*=\s*(.*)$/m.exec(raw);
8643
+ const value = (m?.[1] ?? "").trim().replace(/^['"]|['"]$/g, "");
8644
+ if (value) return value;
8645
+ }
8646
+ return null;
8647
+ }
8648
+ function isLocalDatabase(url) {
8649
+ try {
8650
+ return LOCAL_DB_HOSTS.has(new URL(url).hostname);
8651
+ } catch {
8652
+ return false;
8653
+ }
8654
+ }
8655
+ function prismaPackages(worktree, io) {
8656
+ const exists = io.exists ?? existsSync10;
8657
+ const readText = io.readText ?? safeReadText;
8658
+ const listDirs = io.listDirs ?? safeListDirs;
8659
+ const dirs = [worktree, ...workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs })];
8660
+ return dirs.filter((dir) => exists(join17(dir, "prisma", "schema.prisma")) && listDirs(join17(dir, "prisma", "migrations")).length > 0);
8661
+ }
8662
+ function jsExecBin(pm, command) {
8663
+ switch (pm) {
8664
+ case "npm":
8665
+ return `npm exec -- ${command}`;
8666
+ case "bun":
8667
+ return `bun x ${command}`;
8668
+ default:
8669
+ return `${pm} exec ${command}`;
8670
+ }
8671
+ }
8672
+ function migrateCommands(worktree, pm, io) {
8673
+ if (pm === "deno") return [];
8674
+ const url = devDatabaseUrl(worktree, io);
8675
+ if (!url || !isLocalDatabase(url)) return [];
8676
+ return prismaPackages(worktree, io).map((dir) => ({
8677
+ label: `\u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0411\u0414${dir === worktree ? "" : ` (${dir.slice(worktree.length + 1)})`}`,
8678
+ run: jsExecBin(pm, "prisma migrate deploy"),
8679
+ cwd: dir,
8680
+ env: { DATABASE_URL: url },
8681
+ // Ключ мьютекса — сама база: дроны разных репозиториев с разными базами друг друга не ждут.
8682
+ lock: `migrate:${url}`,
8683
+ optional: true,
8684
+ source: "detect"
8685
+ }));
8686
+ }
8547
8687
  function planPrepare(worktree, io = {}) {
8548
8688
  const plan = overridePlan(io.override) ?? detectPlan(worktree, io) ?? ciWorkflowPlan(worktree, io);
8549
8689
  if (!plan) return { source: "none", ecosystem: null, commands: [] };
@@ -8561,7 +8701,8 @@ async function ensureWorktreeDeps(worktree, io = {}) {
8561
8701
  ran: [],
8562
8702
  failed: [],
8563
8703
  skipped: [],
8564
- blocked: []
8704
+ blocked: [],
8705
+ warned: []
8565
8706
  };
8566
8707
  for (const cmd of plan.commands) {
8567
8708
  if (cmd.skipIfExists && exists(cmd.skipIfExists)) {
@@ -8576,6 +8717,8 @@ async function ensureWorktreeDeps(worktree, io = {}) {
8576
8717
  if (await exec(cmd)) {
8577
8718
  report.ran.push(cmd.label);
8578
8719
  report.outcome = "prepared";
8720
+ } else if (cmd.optional) {
8721
+ report.warned.push(cmd.label);
8579
8722
  } else {
8580
8723
  report.failed.push(cmd.label);
8581
8724
  report.outcome = "failed";
@@ -8592,13 +8735,17 @@ function depsEvents(report) {
8592
8735
  events.push(
8593
8736
  `\u26A0 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 worktree${where} \u0423\u041F\u0410\u041B\u0410 \u043D\u0430 \xAB${report.failed.join(", ")}\xBB \u2014 \u0434\u0440\u043E\u043D \u0441\u0442\u0430\u0440\u0442\u0443\u0435\u0442 \u0411\u0415\u0417 \u0433\u043E\u0442\u043E\u0432\u043E\u0433\u043E \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F: pre-commit \u0445\u0443\u043A \u043C\u043E\u0436\u0435\u0442 \u043C\u043E\u043B\u0447\u0430 \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C, typecheck/\u0442\u0435\u0441\u0442\u044B \u0431\u0443\u0434\u0443\u0442 \u043A\u0440\u0430\u0441\u043D\u044B\u043C\u0438 \u043D\u0435 \u043F\u043E \u0434\u0435\u043B\u0443 (TASWM-312)`
8594
8737
  );
8738
+ if (report.warned.length)
8739
+ events.push(
8740
+ `\u26A0 \u0448\u0430\u0433 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0438 \xAB${report.warned.join(", ")}\xBB \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B \u2014 worktree \u0433\u043E\u0442\u043E\u0432, \u043D\u043E \u0411\u0410\u0417\u0410 \u041C\u041E\u0413\u041B\u0410 \u041E\u0421\u0422\u0410\u0422\u042C\u0421\u042F \u041D\u0410 \u0421\u0422\u0410\u0420\u041E\u0419 \u0421\u0425\u0415\u041C\u0415 (\u0447\u0430\u0449\u0435 \u0432\u0441\u0435\u0433\u043E \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0430\u044F \u0431\u0430\u0437\u0430 \u043F\u0440\u043E\u0441\u0442\u043E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u0430) \u2014 TASWM-362`
8741
+ );
8595
8742
  if (report.blocked.length)
8596
8743
  events.push(`\u26A0 \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0438 \u043D\u0435 \u043F\u0443\u0449\u0435\u043D\u0430 \u0433\u0435\u0439\u0442\u043E\u043C \u0434\u043E\u0432\u0435\u0440\u0438\u044F: ${report.blocked.join(", ")} \u2014 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u043D\u0435 \u0433\u043E\u0442\u043E\u0432\u0438\u043B\u0438 (TASWM-312)`);
8597
8744
  if (report.outcome === "not-detected")
8598
8745
  events.push(
8599
8746
  `\u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u0430: \u043F\u0440\u043E\u0435\u043A\u0442 \u043D\u0435 \u043E\u043F\u043E\u0437\u043D\u0430\u043D (\u043D\u0435\u0442 \u0437\u043D\u0430\u043A\u043E\u043C\u044B\u0445 \u043C\u0430\u0440\u043A\u0435\u0440\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432, \u0432 .github/workflows \u043A\u043E\u043C\u0430\u043D\u0434 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438 \u043D\u0435 \u043D\u0430\u0448\u043B\u0438) \u2014 \u0434\u0435\u043C\u043E\u043D \u041D\u0418\u0427\u0415\u0413\u041E \u043D\u0435 \u0441\u0442\u0430\u0432\u0438\u043B; \u0437\u0430\u0434\u0430\u0442\u044C \u043A\u043E\u043C\u0430\u043D\u0434\u0443 \u043C\u043E\u0436\u043D\u043E \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (swarmConfig.prepare) \u2014 TASWM-312`
8600
8747
  );
8601
- if (report.outcome === "nothing-to-do" && !report.skipped.length) {
8748
+ if (report.outcome === "nothing-to-do" && !report.skipped.length && !report.warned.length) {
8602
8749
  if (report.source === "project-config") events.push(`\u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0430 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u043E\u0439 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (swarmConfig.prepare \u043F\u0443\u0441\u0442) \u2014 TASWM-312`);
8603
8750
  else if (report.ecosystem) events.push(`\u043F\u0440\u043E\u0435\u043A\u0442${where}: \u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043F\u0435\u0440\u0435\u0434 \u0441\u0442\u0430\u0440\u0442\u043E\u043C \u043D\u0435\u0447\u0435\u0433\u043E, \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F \u2014 TASWM-312`);
8604
8751
  }
@@ -8617,6 +8764,9 @@ function depsPromptNotice(report) {
8617
8764
  if (problems.length) {
8618
8765
  return `\u26A0 \u041E\u041A\u0420\u0423\u0416\u0415\u041D\u0418\u0415 WORKTREE \u041D\u0415 \u041F\u041E\u0414\u0413\u041E\u0422\u041E\u0412\u041B\u0415\u041D\u041E (TASWM-312): ${problems.join("; ")}. \u041F\u043E\u043A\u0430 \u044D\u0442\u043E \u0442\u0430\u043A, \u0413\u0415\u0419\u0422 \u041C\u041E\u0416\u0415\u0422 \u041D\u0415 \u0420\u0410\u0411\u041E\u0422\u0410\u0422\u042C: pre-commit \u0445\u0443\u043A \u043C\u043E\u043B\u0447\u0430 \u043D\u0435 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0441\u044F (\u043F\u0435\u0440\u0432\u044B\u0439 \u043A\u043E\u043C\u043C\u0438\u0442 \u0443\u0439\u0434\u0451\u0442 \u0431\u0435\u0437 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A), \u0430 typecheck/\u0442\u0435\u0441\u0442\u044B \u0431\u0443\u0434\u0443\u0442 \u043A\u0440\u0430\u0441\u043D\u044B\u043C\u0438 \u043D\u0435 \u043F\u043E \u0434\u0435\u043B\u0443. \u041F\u0435\u0440\u0432\u044B\u043C \u0434\u0435\u043B\u043E\u043C \u043F\u043E\u0447\u0438\u043D\u0438 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u0441\u0430\u043C (\u043F\u043E\u0441\u0442\u0430\u0432\u044C \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u0442\u0430\u043A, \u043A\u0430\u043A \u044D\u0442\u043E \u0434\u0435\u043B\u0430\u0435\u0442 README/CI \u043F\u0440\u043E\u0435\u043A\u0442\u0430) \u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0442\u043E\u043C \u0432\u0435\u0440\u044C \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430\u043C \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A; \u043D\u0435 \u0441\u0434\u0430\u0432\u0430\u0439 \u0440\u0430\u0431\u043E\u0442\u0443, \u043E\u0431\u044A\u044F\u0441\u043D\u044F\u044F \u043F\u0430\u0434\u0435\u043D\u0438\u044F \xAB\u0441\u043B\u043E\u043C\u0430\u043D\u043D\u044B\u043C \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435\u043C\xBB, \u043D\u0435 \u043F\u043E\u043F\u0440\u043E\u0431\u043E\u0432\u0430\u0432 \u044D\u0442\u043E.`;
8619
8766
  }
8767
+ if (report.warned.length) {
8768
+ return `\u2139 \u0411\u0410\u0417\u0410 \u041C\u041E\u0413\u041B\u0410 \u041E\u0421\u0422\u0410\u0422\u042C\u0421\u042F \u041D\u0410 \u0421\u0422\u0410\u0420\u041E\u0419 \u0421\u0425\u0415\u041C\u0415 (TASWM-362): \u0448\u0430\u0433 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0438 \xAB${report.warned.join(", ")}\xBB \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B \u2014 \u043E\u0431\u044B\u0447\u043D\u043E \u043F\u043E\u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0430\u044F \u0431\u0430\u0437\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u0430. \u0421\u0430\u043C worktree \u0433\u043E\u0442\u043E\u0432, typecheck \u0438 \u044E\u043D\u0438\u0442-\u0442\u0435\u0441\u0442\u044B \u0434\u043E\u0441\u0442\u043E\u0432\u0435\u0440\u043D\u044B. \u0415\u0441\u043B\u0438 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0448\u044C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0438 \u0443\u0432\u0438\u0434\u0438\u0448\u044C \u043E\u0448\u0438\u0431\u043A\u0443 \u043F\u0440\u043E \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0449\u0443\u044E \u043A\u043E\u043B\u043E\u043D\u043A\u0443/\u0442\u0430\u0431\u043B\u0438\u0446\u0443 \u2014 \u044D\u0442\u043E \u043E\u043D\u0430: \u043F\u043E\u0434\u043D\u0438\u043C\u0438 \u0431\u0430\u0437\u0443 \u0438 \u043D\u0430\u043A\u0430\u0442\u0438 \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0442\u0430\u043A, \u043A\u0430\u043A \u044D\u0442\u043E \u0434\u0435\u043B\u0430\u0435\u0442 dev-\u0441\u043A\u0440\u0438\u043F\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.`;
8769
+ }
8620
8770
  if (report.outcome === "not-detected") {
8621
8771
  return `\u2139 \u0414\u0415\u041C\u041E\u041D \u041D\u0415 \u0413\u041E\u0422\u041E\u0412\u0418\u041B \u041E\u041A\u0420\u0423\u0416\u0415\u041D\u0418\u0415 (TASWM-312): \u043F\u0440\u043E\u0435\u043A\u0442 \u043D\u0435 \u043E\u043F\u043E\u0437\u043D\u0430\u043D \u043F\u043E \u043C\u0430\u0440\u043A\u0435\u0440\u043D\u044B\u043C \u0444\u0430\u0439\u043B\u0430\u043C, \u043A\u043E\u043C\u0430\u043D\u0434 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438 \u0432 .github/workflows \u0442\u043E\u0436\u0435 \u043D\u0435\u0442. \u0417\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438, \u0435\u0441\u043B\u0438 \u043E\u043D\u0438 \u043D\u0443\u0436\u043D\u044B, \u043D\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u044B \u2014 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 README/CI \u0438 \u043F\u043E\u0441\u0442\u0430\u0432\u044C \u0438\u0445 \u0441\u0430\u043C \u041F\u0415\u0420\u0415\u0414 \u0442\u0435\u043C, \u043A\u0430\u043A \u0432\u0435\u0440\u0438\u0442\u044C typecheck/\u0442\u0435\u0441\u0442\u0430\u043C.`;
8622
8772
  }
@@ -8976,6 +9126,15 @@ function branchFromPrPatch(currentBranch, headBranch) {
8976
9126
  event: currentBranch ? `\u0432\u0435\u0442\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0432\u0437\u044F\u0442\u0430 \u0443 PR: ${currentBranch} \u2192 ${headBranch} (\u043F\u043E\u043B\u0435 \u0431\u044B\u043B\u043E \u0441\u043E\u0431\u0440\u0430\u043D\u043E \u0438\u0437 \u0441\u043B\u0430\u0433\u0430 \u2014 TASWM-356)` : `\u0432\u0435\u0442\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0432\u0437\u044F\u0442\u0430 \u0443 PR: ${headBranch} (TASWM-356)`
8977
9127
  };
8978
9128
  }
9129
+ function prLiveness(st) {
9130
+ if (!st) return "unknown";
9131
+ if (st.merged) return "merged";
9132
+ const state = (st.state ?? "").toUpperCase();
9133
+ if (state === "MERGED") return "merged";
9134
+ if (state === "CLOSED") return "closed";
9135
+ if (state === "UNKNOWN") return "unknown";
9136
+ return "open";
9137
+ }
8979
9138
  function applyReviewTransition(db, run, prUrl) {
8980
9139
  const { patch, ...t } = reviewTransitionPatch(run, prUrl);
8981
9140
  db.updateRun(run.id, patch);
@@ -9486,7 +9645,7 @@ async function runDroneServer(runId) {
9486
9645
 
9487
9646
  // src/droneBridge.ts
9488
9647
  init_src();
9489
- import { createHash as createHash2 } from "crypto";
9648
+ import { createHash as createHash3 } from "crypto";
9490
9649
 
9491
9650
  // src/anomalies.ts
9492
9651
  init_src();
@@ -9881,7 +10040,7 @@ function questionKey(runId, text) {
9881
10040
  return `${runId} ${text.trim().replace(/\s+/g, " ")}`;
9882
10041
  }
9883
10042
  function noteKey(runId, text) {
9884
- return `note:${runId}:${createHash2("sha1").update(questionKey(runId, text)).digest("hex").slice(0, 16)}`;
10043
+ return `note:${runId}:${createHash3("sha1").update(questionKey(runId, text)).digest("hex").slice(0, 16)}`;
9885
10044
  }
9886
10045
  async function syncDroneChannel(opts) {
9887
10046
  const result = { imported: [], answered: [], notes: [] };
@@ -10162,20 +10321,20 @@ function makeExclusiveReader(opts) {
10162
10321
 
10163
10322
  // src/envGate.ts
10164
10323
  import { existsSync as existsSync13 } from "fs";
10165
- import { join as join23 } from "path";
10324
+ import { join as join24 } from "path";
10166
10325
  import { createConnection } from "net";
10167
10326
  import { execa as execa2 } from "execa";
10168
10327
 
10169
10328
  // src/platform.ts
10170
10329
  import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
10171
- import { delimiter, join as join22 } from "path";
10330
+ import { delimiter, join as join23 } from "path";
10172
10331
  function resolveCommandPath(name, env = process.env, platform = process.platform) {
10173
10332
  if (name.includes("/") || name.includes("\\")) return existsSync12(name) ? name : null;
10174
10333
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
10175
10334
  const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
10176
10335
  for (const dir of dirs) {
10177
10336
  for (const ext of exts) {
10178
- const candidate = join22(dir, name + ext);
10337
+ const candidate = join23(dir, name + ext);
10179
10338
  if (existsSync12(candidate)) return candidate;
10180
10339
  }
10181
10340
  }
@@ -10184,6 +10343,31 @@ function resolveCommandPath(name, env = process.env, platform = process.platform
10184
10343
  function hasCommandInPath(name, env = process.env, platform = process.platform) {
10185
10344
  return resolveCommandPath(name, env, platform) !== null;
10186
10345
  }
10346
+ function detectGlobalPm(entry, io = {}) {
10347
+ const p = (entry ?? "").replace(/\\/g, "/");
10348
+ if (!p) return "unknown";
10349
+ if (p.endsWith(".ts") || /\/packages\/swarm-daemon\/(src|dist)\//.test(p)) return "sources";
10350
+ if (/\/\.bun\/install\/global\//.test(p) || /\/\.bun\/bin\//.test(p)) return "bun";
10351
+ if (/\/pnpm\//.test(p) || /\/\.pnpm-global\//.test(p) || /\/Library\/pnpm\//.test(p)) return "pnpm";
10352
+ if (/\/\.yarn\//.test(p) || /\/Yarn\/Data\/global\//.test(p) || /\/yarn\/global\//.test(p)) return "yarn";
10353
+ if (/\/node_modules\//.test(p)) return "npm";
10354
+ const env = io.env ?? process.env;
10355
+ const platform = io.platform ?? process.platform;
10356
+ const has = io.hasCommand ?? ((n) => hasCommandInPath(n, env, platform));
10357
+ return has("npm") ? "npm" : "unknown";
10358
+ }
10359
+ function globalInstallCommand(pm, spec) {
10360
+ switch (pm) {
10361
+ case "pnpm":
10362
+ return `pnpm add -g ${spec}`;
10363
+ case "bun":
10364
+ return `bun add -g ${spec}`;
10365
+ case "yarn":
10366
+ return `yarn global add ${spec}`;
10367
+ default:
10368
+ return `npm i -g ${spec}`;
10369
+ }
10370
+ }
10187
10371
 
10188
10372
  // src/envGate.ts
10189
10373
  var PROBE_TIMEOUT_MS = 2500;
@@ -10235,7 +10419,7 @@ var COMPOSE_MARKERS = [
10235
10419
  ];
10236
10420
  function detectRequirements(repo, io = {}) {
10237
10421
  const exists = io.exists ?? existsSync13;
10238
- const marker = COMPOSE_MARKERS.find((f) => exists(join23(repo, f)));
10422
+ const marker = COMPOSE_MARKERS.find((f) => exists(join24(repo, f)));
10239
10423
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
10240
10424
  }
10241
10425
  function resolveRequirements(repo, declared, io = {}) {
@@ -10371,8 +10555,8 @@ import { basename as basename3 } from "path";
10371
10555
  import { serve as serve2 } from "@hono/node-server";
10372
10556
 
10373
10557
  // src/diff.ts
10374
- import { statSync as statSync8 } from "fs";
10375
- import { join as join24 } from "path";
10558
+ import { statSync as statSync9 } from "fs";
10559
+ import { join as join25 } from "path";
10376
10560
  import { $ as $4 } from "execa";
10377
10561
  var MAX_FILES = 200;
10378
10562
  var MAX_LINES_PER_FILE = 1500;
@@ -10515,7 +10699,7 @@ async function resolveBase(wt, baseBranch) {
10515
10699
  }
10516
10700
  function mtime(path) {
10517
10701
  try {
10518
- return statSync8(path).mtimeMs;
10702
+ return statSync9(path).mtimeMs;
10519
10703
  } catch {
10520
10704
  return 0;
10521
10705
  }
@@ -10540,7 +10724,7 @@ async function collectRunDiff(worktree, opts = {}) {
10540
10724
  files.push(pf);
10541
10725
  }
10542
10726
  }
10543
- files.sort((a, b) => mtime(join24(worktree, b.path)) - mtime(join24(worktree, a.path)));
10727
+ files.sort((a, b) => mtime(join25(worktree, b.path)) - mtime(join25(worktree, a.path)));
10544
10728
  const truncatedFiles = files.length > MAX_FILES;
10545
10729
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
10546
10730
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -10573,7 +10757,7 @@ async function changedPaths(worktree, opts = {}) {
10573
10757
  // package.json
10574
10758
  var package_default = {
10575
10759
  name: "@taskless-app/swarm",
10576
- version: "0.6.0",
10760
+ version: "0.6.1",
10577
10761
  type: "module",
10578
10762
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
10579
10763
  keywords: [
@@ -10622,7 +10806,8 @@ var package_default = {
10622
10806
  "@modelcontextprotocol/sdk": "^1.29.0",
10623
10807
  execa: "^10.0.0",
10624
10808
  zod: "3.25.76"
10625
- }
10809
+ },
10810
+ gitHead: "415ad928e18430894ae24ac2e4267073f5c6da14"
10626
10811
  };
10627
10812
 
10628
10813
  // src/version.ts
@@ -12201,48 +12386,10 @@ function closeWebServer(server) {
12201
12386
  });
12202
12387
  }
12203
12388
 
12204
- // src/daemonVersion.ts
12205
- init_git();
12206
- import { dirname as dirname4 } from "path";
12207
- import { $ as $5 } from "execa";
12208
- async function captureDaemonStart(entryFile, now = () => /* @__PURE__ */ new Date()) {
12209
- try {
12210
- const repo = await repoRoot(dirname4(entryFile));
12211
- const sha = (await $5`git -C ${repo} rev-parse HEAD`).stdout.trim();
12212
- if (!sha) return null;
12213
- return { repo, startedAt: now().toISOString(), startCommit: sha };
12214
- } catch {
12215
- return null;
12216
- }
12217
- }
12218
- var DAEMON_CODE_PATHS = ["packages/swarm-daemon", "packages/swarm-core"];
12219
- async function checkDaemonVersion(start, now = () => /* @__PURE__ */ new Date()) {
12220
- const base3 = { startedAt: start.startedAt, startCommit: start.startCommit, checkedAt: now().toISOString() };
12221
- try {
12222
- await $5({ reject: false })`git -C ${start.repo} fetch --quiet origin`;
12223
- const mainRef = await remoteMainRef(start.repo);
12224
- const mainCommit = (await $5`git -C ${start.repo} rev-parse ${mainRef}`).stdout.trim();
12225
- if (!mainCommit || mainCommit === start.startCommit) {
12226
- return { ...base3, mainCommit: mainCommit || null, stale: false, staleFiles: [] };
12227
- }
12228
- const ancestor = await $5({ reject: false })`git -C ${start.repo} merge-base --is-ancestor ${start.startCommit} ${mainCommit}`;
12229
- if (ancestor.exitCode !== 0) {
12230
- return { ...base3, mainCommit, stale: false, staleFiles: [] };
12231
- }
12232
- const diff = await $5({ reject: false })(
12233
- "git",
12234
- ["-C", start.repo, "diff", "--name-only", start.startCommit, mainCommit, "--", ...DAEMON_CODE_PATHS]
12235
- );
12236
- const staleFiles = diff.stdout.trim() ? diff.stdout.trim().split("\n") : [];
12237
- return { ...base3, mainCommit, stale: staleFiles.length > 0, staleFiles: staleFiles.slice(0, 20) };
12238
- } catch {
12239
- return { ...base3, mainCommit: null, stale: false, staleFiles: [] };
12240
- }
12241
- }
12242
-
12243
12389
  // src/release.ts
12390
+ init_src();
12244
12391
  init_claudeBin();
12245
- var SWARM_PACKAGE = "@taskless-app/swarm";
12392
+ var SWARM_PACKAGE = SWARM_PACKAGE_NAME;
12246
12393
  var LATEST_TIMEOUT_MS = 4e3;
12247
12394
  async function fetchLatestVersion(opts = {}) {
12248
12395
  const fetchFn = opts.fetchFn ?? fetch;
@@ -12273,12 +12420,30 @@ function outdatedLine(current, latest, updateCommand = "swarm update") {
12273
12420
  return `\u26A0 \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u043E\u044F \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0430: \u0443 \u0442\u0435\u0431\u044F v${current}, \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest} \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: ${updateCommand}`;
12274
12421
  }
12275
12422
 
12423
+ // src/daemonVersion.ts
12424
+ function captureDaemonStart(now = () => /* @__PURE__ */ new Date()) {
12425
+ return { startedAt: now().toISOString(), version: PACKAGE_VERSION };
12426
+ }
12427
+ var DAEMON_CODE_PATHS = ["packages/swarm-daemon", "packages/swarm-core"];
12428
+ async function checkDaemonVersion(start, opts = {}) {
12429
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
12430
+ const fetchLatest = opts.latest ?? (() => fetchLatestVersion());
12431
+ const latestVersion = await fetchLatest().catch(() => null);
12432
+ return {
12433
+ startedAt: start.startedAt,
12434
+ version: start.version,
12435
+ latestVersion,
12436
+ stale: isOutdated(start.version, latestVersion),
12437
+ checkedAt: now().toISOString()
12438
+ };
12439
+ }
12440
+
12276
12441
  // src/hostCommands.ts
12277
12442
  init_src();
12278
12443
  import { readFileSync as readFileSync18, renameSync as renameSync8, writeFileSync as writeFileSync13 } from "fs";
12279
- import { join as join25 } from "path";
12444
+ import { join as join26 } from "path";
12280
12445
  function hostCommandStatePath(home) {
12281
- return join25(home, "host-command.json");
12446
+ return join26(home, "host-command.json");
12282
12447
  }
12283
12448
  function readState(path) {
12284
12449
  try {
@@ -12421,6 +12586,11 @@ async function executeOne(cmd, opts) {
12421
12586
  effects.narrate(`\u26A0\uFE0F \u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0448\u0430\u0433\u0435 \xAB${UPDATE_STEP_LABELS[res.step]}\xBB: ${res.message}`);
12422
12587
  return false;
12423
12588
  }
12589
+ if (res.restart === false) {
12590
+ await ack(cmd.id, "done", { step: res.step, result: res.message });
12591
+ effects.narrate(`\u2705 ${res.message}`);
12592
+ return false;
12593
+ }
12424
12594
  return await doRestart(cmd, opts, res.message);
12425
12595
  }
12426
12596
  }
@@ -12448,11 +12618,11 @@ async function doRestart(cmd, opts, note) {
12448
12618
  // src/selfUpdate.ts
12449
12619
  init_git();
12450
12620
  import { existsSync as existsSync14 } from "fs";
12451
- import { join as join26 } from "path";
12452
- import { $ as $6 } from "execa";
12621
+ import { join as join27 } from "path";
12622
+ import { $ as $5 } from "execa";
12453
12623
  var DAEMON_PACKAGE = "@taskless-app/swarm";
12454
12624
  var defaultRun = async (cmd, args, cwd) => {
12455
- const res = await $6({ reject: false, cwd })(cmd, args);
12625
+ const res = await $5({ reject: false, cwd })(cmd, args);
12456
12626
  return { exitCode: res.exitCode ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
12457
12627
  };
12458
12628
  function tail(text, max = 400) {
@@ -12490,7 +12660,7 @@ async function runSelfUpdate(io) {
12490
12660
  };
12491
12661
  }
12492
12662
  await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
12493
- if (!exists(join26(repo, "pnpm-lock.yaml"))) {
12663
+ if (!exists(join27(repo, "pnpm-lock.yaml"))) {
12494
12664
  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" };
12495
12665
  }
12496
12666
  const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
@@ -12509,6 +12679,31 @@ async function runSelfUpdate(io) {
12509
12679
  function defaultSelfUpdateIO(repo, onStep) {
12510
12680
  return { repo, run: defaultRun, mainRef: remoteMainRef, onStep };
12511
12681
  }
12682
+ async function runPackageSelfUpdate(io) {
12683
+ await io.onStep("fetch", "\u0441\u043C\u043E\u0442\u0440\u044E, \u0447\u0442\u043E \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043E");
12684
+ const latest = await io.latest();
12685
+ if (!latest) {
12686
+ return { ok: false, step: "fetch", message: "\u0434\u043E registry.npmjs.org \u043D\u0435 \u0434\u043E\u0437\u0432\u043E\u043D\u0438\u043B\u0438\u0441\u044C \u2014 \u043A\u0430\u043A\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430, \u0443\u0437\u043D\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u043C" };
12687
+ }
12688
+ if (!isOutdated(io.currentVersion, latest)) {
12689
+ return { ok: true, restart: false, step: "install", message: `\u0443\u0436\u0435 \u0441\u0432\u0435\u0436\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F: v${io.currentVersion} (\u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest})` };
12690
+ }
12691
+ await io.onStep("install", `\u0441\u0442\u0430\u0432\u043B\u044E v${latest}`);
12692
+ const command = io.installCommand(`${SWARM_PACKAGE}@${latest}`);
12693
+ const code = await io.exec(command);
12694
+ if (code !== 0) {
12695
+ return { ok: false, step: "install", message: `\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0448\u043B\u0430 (\u043A\u043E\u0434 ${code}): ${command} \u2014 \u0432\u044B\u043F\u043E\u043B\u043D\u0438 \u044D\u0442\u0443 \u0441\u0442\u0440\u043E\u043A\u0443 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438, \u043D\u0430 \u0447\u0451\u043C \u0441\u043F\u043E\u0442\u044B\u043A\u0430\u0435\u0442\u0441\u044F` };
12696
+ }
12697
+ return { ok: true, step: "install", message: `\u043E\u0431\u043D\u043E\u0432\u0438\u043B\u0441\u044F \u0434\u043E v${latest} (\u0431\u044B\u043B\u043E v${io.currentVersion})` };
12698
+ }
12699
+
12700
+ // src/cliIo.ts
12701
+ import { createInterface } from "readline/promises";
12702
+ import { execa as execa3 } from "execa";
12703
+ async function execLoud(command) {
12704
+ const res = await execa3(command, { shell: true, reject: false, stdio: "inherit" });
12705
+ return res.exitCode ?? 1;
12706
+ }
12512
12707
 
12513
12708
  // src/narration.ts
12514
12709
  var RING_LIMIT = 20;
@@ -12584,14 +12779,14 @@ async function fetchExternalStorm(url, opts = {}) {
12584
12779
 
12585
12780
  // src/anomalyState.ts
12586
12781
  import { existsSync as existsSync15, readFileSync as readFileSync19, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
12587
- import { join as join27 } from "path";
12782
+ import { join as join28 } from "path";
12588
12783
  var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
12589
12784
  var MAX_RESOLVED = 200;
12590
12785
  function emptyState() {
12591
12786
  return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
12592
12787
  }
12593
12788
  function anomalyStatePath(home) {
12594
- return join27(home, "anomaly-state.json");
12789
+ return join28(home, "anomaly-state.json");
12595
12790
  }
12596
12791
  function loadAnomalyState(home, now = Date.now(), io = {}) {
12597
12792
  const exists = io.exists ?? existsSync15;
@@ -12871,7 +13066,7 @@ init_src();
12871
13066
  init_src();
12872
13067
 
12873
13068
  // src/gitHistory.ts
12874
- import { $ as $7 } from "execa";
13069
+ import { $ as $6 } from "execa";
12875
13070
  var REC = "";
12876
13071
  var FIELD = "";
12877
13072
  var LOG_FORMAT = `${REC}%H${FIELD}%s`;
@@ -12923,7 +13118,7 @@ function groupTaskHistory(commits, opts = {}) {
12923
13118
  async function readCommitLog(repo, opts = {}) {
12924
13119
  const max = opts.maxCommits ?? 1500;
12925
13120
  try {
12926
- const { stdout } = await $7({ cwd: repo, reject: false })`git log --no-merges --name-only --pretty=format:${LOG_FORMAT} -n ${String(max)} ${opts.ref ?? "HEAD"}`;
13121
+ const { stdout } = await $6({ cwd: repo, reject: false })`git log --no-merges --name-only --pretty=format:${LOG_FORMAT} -n ${String(max)} ${opts.ref ?? "HEAD"}`;
12927
13122
  return parseGitLog(stdout ?? "");
12928
13123
  } catch {
12929
13124
  return [];
@@ -12934,7 +13129,7 @@ async function readTaskHistory(repo, opts = {}) {
12934
13129
  }
12935
13130
  async function headSha(repo) {
12936
13131
  try {
12937
- const { stdout, exitCode } = await $7({ cwd: repo, reject: false })`git rev-parse HEAD`;
13132
+ const { stdout, exitCode } = await $6({ cwd: repo, reject: false })`git rev-parse HEAD`;
12938
13133
  return exitCode === 0 ? stdout.trim() : null;
12939
13134
  } catch {
12940
13135
  return null;
@@ -13217,20 +13412,20 @@ function countActiveDrones(db) {
13217
13412
  function countActiveReviewers(db) {
13218
13413
  return db.listRuns().filter((r) => r.kind === "reviewer" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
13219
13414
  }
13220
- async function closeReviewersOnMerge(deps, reviewRunId) {
13221
- const stale = deps.db.activeReviewersFor(reviewRunId);
13222
- for (const rv of stale) {
13223
- if (rv.pid != null && deps.kill && deps.isAlive(rv.pid)) {
13224
- try {
13225
- deps.kill(rv.pid);
13226
- } catch {
13227
- }
13415
+ async function closeReviewerRun(deps, rv, reason) {
13416
+ if (rv.pid != null && deps.kill && deps.isAlive(rv.pid)) {
13417
+ try {
13418
+ deps.kill(rv.pid);
13419
+ } catch {
13228
13420
  }
13229
- if (rv.worktree_path) await deps.removeWorktree(rv.repo, rv.worktree_path).catch(() => {
13230
- });
13231
- deps.db.updateRun(rv.id, { status: "done" });
13232
- deps.db.addEvent(rv.id, "done", "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E \u2014 \u0440\u0435\u0432\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E");
13233
13421
  }
13422
+ if (rv.worktree_path) await deps.removeWorktree(rv.repo, rv.worktree_path).catch(() => {
13423
+ });
13424
+ deps.db.updateRun(rv.id, { status: "done", stop_reason: null });
13425
+ deps.db.addEvent(rv.id, "done", reason);
13426
+ }
13427
+ async function closeReviewersOnDeadPr(deps, reviewRunId, reason) {
13428
+ for (const rv of deps.db.activeReviewersFor(reviewRunId)) await closeReviewerRun(deps, rv, reason);
13234
13429
  }
13235
13430
  function ciWaitStartedMs(db, runId) {
13236
13431
  const evs = db.events(runId);
@@ -13304,7 +13499,39 @@ async function finalizeMergedReview(deps, run) {
13304
13499
  deps.db.addEvent(run.id, "merged", run.pr_url ?? "");
13305
13500
  deps.db.updateRun(run.id, { status: "done" });
13306
13501
  deps.db.addEvent(run.id, "done");
13307
- await closeReviewersOnMerge(deps, run.id);
13502
+ await closeReviewersOnDeadPr(deps, run.id, "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E \u2014 \u0440\u0435\u0432\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E");
13503
+ }
13504
+ async function finalizeClosedPrReview(deps, run, reason) {
13505
+ await closeReviewersOnDeadPr(deps, run.id, "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E");
13506
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
13507
+ });
13508
+ deps.db.updateRun(run.id, { status: "done" });
13509
+ deps.db.addEvent(run.id, "done", reason);
13510
+ }
13511
+ var REVIEW_PR_UNKNOWN_MARK = "\u0441\u0442\u0430\u0442\u0443\u0441 PR \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D";
13512
+ async function prGateForReviewer(deps, run, prUrl, when) {
13513
+ const st = await deps.prStatus(prUrl).catch(() => null);
13514
+ switch (prLiveness(st)) {
13515
+ case "merged":
13516
+ await finalizeMergedReview(deps, run);
13517
+ narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D, ${when} \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D, \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D.`);
13518
+ return { kind: "finalized" };
13519
+ case "closed":
13520
+ await finalizeClosedPrReview(deps, run, `PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 (${when}) \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E`);
13521
+ narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, ${when} \u2014 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E, \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442.`);
13522
+ return { kind: "finalized" };
13523
+ case "unknown":
13524
+ if (!deps.db.events(run.id).some((e) => (e.text ?? "").includes(REVIEW_PR_UNKNOWN_MARK))) {
13525
+ deps.db.addEvent(run.id, "step", `${REVIEW_PR_UNKNOWN_MARK} (gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0438\u043B\u0438 \u0432 pr_url \u043D\u0435 PR) \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u043C, \u0441\u043F\u0440\u043E\u0441\u0438\u043C \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C \u0442\u0438\u043A\u0435`);
13526
+ }
13527
+ return { kind: "wait" };
13528
+ default:
13529
+ if (st.conflicting || st.isDraft) {
13530
+ deps.db.addEvent(run.id, "step", `PR \u0441\u0442\u0430\u043B \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u044E\u0449\u0438\u043C/\u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u043E\u043C, ${when} \u2014 \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D \u0434\u043E \u0441\u043B\u0435\u0434. \u0442\u0438\u043A\u0430`);
13531
+ return { kind: "wait" };
13532
+ }
13533
+ return { kind: "spawn", st };
13534
+ }
13308
13535
  }
13309
13536
  function doneMergedPrForSlug(db, slug) {
13310
13537
  const done = db.listRuns("done").find((r) => r.task_slug === slug && r.pr_url && r.kind !== "hero" && r.resolves_run_id == null && r.reviews_run_id == null);
@@ -13367,6 +13594,22 @@ async function closeMergedTaskRuns(deps) {
13367
13594
  }
13368
13595
  }
13369
13596
  }
13597
+ async function closeOrphanedReviewers(deps) {
13598
+ for (const rv of deps.db.listRuns()) {
13599
+ if (rv.kind !== "reviewer" || rv.reviews_run_id == null) continue;
13600
+ if (!REVIEWER_ALIVE_STATUSES.has(rv.status)) continue;
13601
+ const parent = deps.db.getRun(rv.reviews_run_id);
13602
+ if (parent && !isTerminalStatus(parent.status)) continue;
13603
+ try {
13604
+ const why = parent ? `\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442 (${STATUS_META[parent.status]?.label ?? parent.status})` : "\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u0438\u0441\u0447\u0435\u0437";
13605
+ await closeReviewerRun(deps, rv, `${why} \u2014 PR \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u0440\u0435\u0432\u044C\u044E\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E`);
13606
+ if (parent) deps.db.addEvent(parent.id, "step", `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${rv.name} (#${rv.id}) \u0437\u0430\u043A\u0440\u044B\u0442: ${why}`);
13607
+ narrate(deps, `${droneLabel(rv)} \u043E\u0441\u0442\u0430\u043B\u0441\u044F \u043D\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u043E\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u0435 \u2014 \u0433\u0430\u0448\u0443 \u0441\u0430\u043C, \u0441\u043B\u043E\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0451\u043D.`);
13608
+ } catch (e) {
13609
+ console.error(`[overmind] \u0437\u0430\u043A\u0440\u044B\u0442\u0438\u0435 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 #${rv.id} \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:`, e);
13610
+ }
13611
+ }
13612
+ }
13370
13613
  var CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C (\u0441\u0435\u0442\u044C/\u043F\u0440\u043E\u0446\u0435\u0441\u0441) \u2014 \u0442\u044B headless-\u0434\u0440\u043E\u043D, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u0423 \u0416\u0415 \u0437\u0430\u0434\u0430\u0447\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 `git status`/`git log` \u0438 \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0441\u0432\u043E\u0438\u0445 \u0448\u0430\u0433\u043E\u0432: \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E \u2014 \u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438 \u0441 \u043C\u0435\u0441\u0442\u0430 \u043E\u0431\u0440\u044B\u0432\u0430. \u0414\u043E\u0432\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u043E \u043A\u043E\u043D\u0446\u0430, \u041A\u041E\u041C\u041C\u0418\u0422\u042C \u0418 \u041F\u0423\u0428\u042C \u043F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0448\u0430\u0433\u0430, \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url). \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 MCP-\u0442\u0443\u043B ask.";
13371
13614
  function buildCiWakeNudge(summary) {
13372
13615
  return [
@@ -13472,7 +13715,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
13472
13715
  }
13473
13716
  return parts.join("\n");
13474
13717
  }
13475
- var HERO_CHARTER_PATH = join28(dirname5(THIS_FILE), "..", "docs", "HERO.md");
13718
+ var HERO_CHARTER_PATH = join29(dirname4(THIS_FILE), "..", "docs", "HERO.md");
13476
13719
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
13477
13720
  const name = run.name || `#${run.id}`;
13478
13721
  const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
@@ -13811,7 +14054,7 @@ function repoHasCi(deps, run) {
13811
14054
  function repoHasCiWorkflows(worktreePath, repo) {
13812
14055
  for (const base3 of [worktreePath, repo]) {
13813
14056
  if (!base3) continue;
13814
- const dir = join28(base3, ".github", "workflows");
14057
+ const dir = join29(base3, ".github", "workflows");
13815
14058
  try {
13816
14059
  if (readdirSync12(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
13817
14060
  } catch {
@@ -13974,10 +14217,9 @@ async function spawnFreshReviewerAfterStall(deps, run, why) {
13974
14217
  if (run.review_cycle >= REVIEW_CYCLE_LIMIT || deps.db.reviewerAttemptCountFor(run.id) >= REVIEW_SPAWN_LIMIT) {
13975
14218
  return;
13976
14219
  }
13977
- const st = await deps.prStatus(run.pr_url).catch(() => null);
13978
- if (!st || st.merged || st.state === "CLOSED" || st.conflicting || st.isDraft) {
13979
- return;
13980
- }
14220
+ const gate = await prGateForReviewer(deps, run, run.pr_url, "\u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B CI");
14221
+ if (gate.kind !== "spawn") return;
14222
+ const st = gate.st;
13981
14223
  try {
13982
14224
  await deps.spawnReviewer(run, st.files ?? []);
13983
14225
  narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u043F\u043E\u0442\u0435\u0440\u044F\u043B\u043E \u0441\u0435\u0441\u0441\u0438\u044E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430, \u0436\u0434\u0430\u0432 CI (${why}) \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0441\u0440\u0430\u0437\u0443, \u043D\u0435 \u0436\u0434\u0443 \u043A\u0430\u0440\u0443\u0441\u0435\u043B\u044C.`);
@@ -14710,6 +14952,11 @@ async function tickOnce(deps) {
14710
14952
  narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${run.task_slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
14711
14953
  continue;
14712
14954
  }
14955
+ if (prLiveness(st) === "closed") {
14956
+ await finalizeClosedPrReview(deps, run, "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E, \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442");
14957
+ narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 ${run.task_slug} \u0441\u043D\u0438\u043C\u0430\u0435\u0442\u0441\u044F \u0441 \u0440\u0435\u0432\u044C\u044E, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
14958
+ continue;
14959
+ }
14713
14960
  if (touchesDaemonCode(st.files ?? [])) {
14714
14961
  const already = deps.db.events(run.id).some((e) => (e.text ?? "").includes(SELF_REFERENCE_MARK));
14715
14962
  if (!already) {
@@ -14803,20 +15050,9 @@ async function tickOnce(deps) {
14803
15050
  } else {
14804
15051
  const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
14805
15052
  if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk) {
14806
- const freshSt = await deps.prStatus(prUrl);
14807
- if (freshSt.merged) {
14808
- await finalizeMergedReview(deps, run);
14809
- narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E, \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
14810
- } else if (freshSt.state === "CLOSED") {
14811
- if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
14812
- });
14813
- deps.db.updateRun(run.id, { status: "done" });
14814
- deps.db.addEvent(run.id, "done", "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E");
14815
- narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
14816
- } else if (freshSt.conflicting || freshSt.isDraft) {
14817
- deps.db.addEvent(run.id, "step", "PR \u0441\u0442\u0430\u043B \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u044E\u0449\u0438\u043C/\u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u043E\u043C, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D \u0434\u043E \u0441\u043B\u0435\u0434. \u0442\u0438\u043A\u0430");
14818
- } else {
14819
- run = syncBranchFromPr(deps.db, run, freshSt.headBranch);
15053
+ const gate = await prGateForReviewer(deps, run, prUrl, "\u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430");
15054
+ if (gate.kind === "spawn") {
15055
+ run = syncBranchFromPr(deps.db, run, gate.st.headBranch);
14820
15056
  try {
14821
15057
  await deps.spawnReviewer(run, st.files ?? []);
14822
15058
  narrate(deps, `PR ${droneLabel(run)} \u0433\u043E\u0442\u043E\u0432 \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430.`);
@@ -14841,6 +15077,7 @@ async function tickOnce(deps) {
14841
15077
  }
14842
15078
  }
14843
15079
  await closeMergedTaskRuns(deps);
15080
+ await closeOrphanedReviewers(deps);
14844
15081
  refreshReviewPhases(deps);
14845
15082
  let liveChildPpids;
14846
15083
  try {
@@ -15218,7 +15455,7 @@ async function resolveConflict(deps, reviewRun) {
15218
15455
  workspace_id: reviewRun.workspace_id
15219
15456
  // та же задача, тот же воркспейс (TASWM-288)
15220
15457
  });
15221
- const wt = join28(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
15458
+ const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
15222
15459
  try {
15223
15460
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
15224
15461
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -15270,7 +15507,7 @@ async function spawnReviewer(deps, reviewRun, files) {
15270
15507
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
15271
15508
  workspace_id: reviewRun.workspace_id
15272
15509
  });
15273
- const wt = join28(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
15510
+ const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
15274
15511
  try {
15275
15512
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
15276
15513
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -15503,7 +15740,7 @@ async function reportWorkspaceAccess(tl, db) {
15503
15740
  }
15504
15741
  async function runDaemon(opts) {
15505
15742
  installCrashGuards();
15506
- const daemonStart = await captureDaemonStart(THIS_FILE);
15743
+ const daemonStart = captureDaemonStart();
15507
15744
  const latestRelease = fetchLatestVersion();
15508
15745
  const worktreesDir = await migrateWorktreesAtStartup();
15509
15746
  if (!opts.repo) {
@@ -15589,7 +15826,7 @@ async function runDaemon(opts) {
15589
15826
  repoMap = { run: rebuildRepoMap, intervalMs: SWARM_REPO_MAP_MS, lastAt: Date.now() };
15590
15827
  }
15591
15828
  let daemonVersion;
15592
- if (daemonStart && SWARM_VERSION_CHECK_MS > 0) {
15829
+ if (SWARM_VERSION_CHECK_MS > 0) {
15593
15830
  let cached = null;
15594
15831
  daemonVersion = {
15595
15832
  run: async () => {
@@ -16200,10 +16437,21 @@ async function runDaemon(opts) {
16200
16437
  return wakeRun({ db, isAlive: pidAlive, respawn: deps.respawn, narrate: narrator.narrate }, run.id);
16201
16438
  },
16202
16439
  update: async (onStep) => {
16203
- if (!daemonStart) {
16204
- return { ok: false, step: "fetch", message: "\u043A\u043E\u0434 \u0434\u0435\u043C\u043E\u043D\u0430 \u043B\u0435\u0436\u0438\u0442 \u043D\u0435 \u0432 git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E" };
16440
+ const pm = detectGlobalPm(process.argv[1]);
16441
+ if (pm !== "sources") {
16442
+ return runPackageSelfUpdate({
16443
+ currentVersion: PACKAGE_VERSION,
16444
+ installCommand: (spec) => globalInstallCommand(pm, spec),
16445
+ latest: () => fetchLatestVersion(),
16446
+ exec: execLoud,
16447
+ onStep
16448
+ });
16449
+ }
16450
+ const repo = await repoRoot(dirname4(THIS_FILE)).catch(() => null);
16451
+ if (!repo) {
16452
+ return { ok: false, step: "fetch", message: "\u0431\u0435\u0433\u0443 \u0438\u0437 \u0438\u0441\u0445\u043E\u0434\u043D\u0438\u043A\u043E\u0432, \u043D\u043E git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u043D\u0430\u0434 \u043D\u0438\u043C\u0438 \u043D\u0435\u0442 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E" };
16205
16453
  }
16206
- return runSelfUpdate(defaultSelfUpdateIO(daemonStart.repo, onStep));
16454
+ return runSelfUpdate(defaultSelfUpdateIO(repo, onStep));
16207
16455
  },
16208
16456
  narrate: narrator.narrate
16209
16457
  };
@@ -16409,6 +16657,7 @@ export {
16409
16657
  REVIEW_CYCLE_ESCALATION_MARK,
16410
16658
  REVIEW_CYCLE_LIMIT,
16411
16659
  REVIEW_ESCALATION_MARK,
16660
+ REVIEW_PR_UNKNOWN_MARK,
16412
16661
  REVIEW_SPAWN_LIMIT,
16413
16662
  REVIEW_UNRECOVERABLE_MARK,
16414
16663
  SELF_REFERENCE_MARK,