@taskless-app/swarm 0.6.21 → 0.6.23

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/cli.js CHANGED
@@ -1131,8 +1131,8 @@ var init_runGuards = __esm({
1131
1131
  function hasZoneModel(m) {
1132
1132
  return m.tasks > 0 && m.zones.length > 0;
1133
1133
  }
1134
- function testFileFeature(basename5) {
1135
- const head = basename5.replace(/\.(test|spec)\.[a-z]+$/i, "").split(".")[0].toLowerCase();
1134
+ function testFileFeature(basename7) {
1135
+ const head = basename7.replace(/\.(test|spec)\.[a-z]+$/i, "").split(".")[0].toLowerCase();
1136
1136
  return head && !GENERIC_DIRS.has(head) ? head : null;
1137
1137
  }
1138
1138
  function pathZoneCandidate(path) {
@@ -1776,10 +1776,24 @@ function asTokenUsage(m) {
1776
1776
  function budgetExhausted(result) {
1777
1777
  return result.subtype === "error_max_budget_usd" || result.terminal_reason === "budget_exhausted";
1778
1778
  }
1779
+ function sessionAbort(result) {
1780
+ if (budgetExhausted(result)) return null;
1781
+ const reason = result.terminal_reason ?? "";
1782
+ const subtype = result.subtype ?? "";
1783
+ if (reason === "aborted_tools") return { kind: "tools", raw: reason };
1784
+ if (reason === "aborted_streaming" || reason === "api_error") return { kind: "link", raw: reason };
1785
+ if (HONEST_ENDINGS.has(subtype) || HONEST_ENDINGS.has(reason)) return null;
1786
+ const failed = result.is_error === true || subtype === "error_during_execution" || reason.startsWith("aborted");
1787
+ if (!failed) return null;
1788
+ const raw = reason && reason !== "completed" ? reason : subtype || reason || "\u043E\u0448\u0438\u0431\u043A\u0430 \u0431\u0435\u0437 \u043C\u0435\u0442\u043A\u0438";
1789
+ return { kind: "other", raw };
1790
+ }
1791
+ var HONEST_ENDINGS;
1779
1792
  var init_claudeResult = __esm({
1780
1793
  "src/claudeResult.ts"() {
1781
1794
  "use strict";
1782
1795
  init_pricing();
1796
+ HONEST_ENDINGS = /* @__PURE__ */ new Set(["error_max_turns"]);
1783
1797
  }
1784
1798
  });
1785
1799
 
@@ -1854,6 +1868,9 @@ function writeMcpConfig(spec) {
1854
1868
  writeFileSync(path, JSON.stringify(cfg, null, 2));
1855
1869
  return path;
1856
1870
  }
1871
+ function abortField(abort) {
1872
+ return abort ? { aborted: abort } : {};
1873
+ }
1857
1874
  var ISOLATION_FLAGS, MCP_CONFIG_FILE, claudeCodeExecutor;
1858
1875
  var init_claudeCode = __esm({
1859
1876
  "src/executors/claudeCode.ts"() {
@@ -1892,7 +1909,10 @@ var init_claudeCode = __esm({
1892
1909
  durationMs: spend.durationMs,
1893
1910
  // TASWM-377: «упёрся в потолок» — отдельный факт об этом же итоге, а не отдельный
1894
1911
  // разбор файла: у claude он лежит в тех же полях, что деньги и токены.
1895
- ...budgetExhausted(result) ? { stoppedByBudget: true } : {}
1912
+ ...budgetExhausted(result) ? { stoppedByBudget: true } : {},
1913
+ // TASWM-487: и «оборвалось» — третий факт об этом же итоге. Порядок важен: потолок
1914
+ // трат sessionAbort отсекает сам, поэтому оба ключа одновременно не появятся.
1915
+ ...abortField(sessionAbort(result))
1896
1916
  };
1897
1917
  },
1898
1918
  session: {
@@ -2373,7 +2393,7 @@ var init_package = __esm({
2373
2393
  "package.json"() {
2374
2394
  package_default = {
2375
2395
  name: "@taskless-app/swarm",
2376
- version: "0.6.21",
2396
+ version: "0.6.23",
2377
2397
  type: "module",
2378
2398
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
2379
2399
  keywords: [
@@ -2423,7 +2443,7 @@ var init_package = __esm({
2423
2443
  execa: "^10.0.0",
2424
2444
  zod: "3.25.76"
2425
2445
  },
2426
- gitHead: "584411cbe69583a8727e89caa8ae7e626f8fd8ca"
2446
+ gitHead: "af6c4af0f523891b5db805d9b4583021688ed1f5"
2427
2447
  };
2428
2448
  }
2429
2449
  });
@@ -5230,7 +5250,7 @@ var init_updateCli = __esm({
5230
5250
 
5231
5251
  // src/cli.ts
5232
5252
  init_cliIo();
5233
- import { basename as basename4, dirname as dirname4, join as join24 } from "path";
5253
+ import { basename as basename6, dirname as dirname4, join as join24 } from "path";
5234
5254
  import { existsSync as existsSync16, readFileSync as readFileSync17, writeFileSync as writeFileSync13, mkdirSync as mkdirSync9, readdirSync as readdirSync10, statSync as statSync10, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
5235
5255
  import { setTimeout as sleep2 } from "timers/promises";
5236
5256
 
@@ -8302,7 +8322,7 @@ function hostLines(hosts, opts = {}) {
8302
8322
  // src/web.ts
8303
8323
  init_src();
8304
8324
  import { randomBytes as randomBytes3 } from "crypto";
8305
- import { basename as basename2 } from "path";
8325
+ import { basename as basename4 } from "path";
8306
8326
  import { serve as serve2 } from "@hono/node-server";
8307
8327
 
8308
8328
  // src/diff.ts
@@ -8498,7 +8518,123 @@ init_storm();
8498
8518
  // src/gh.ts
8499
8519
  init_src();
8500
8520
  import { $ as $4 } from "execa";
8501
- var realRepoRunner = async (args, cwd) => (await $4({ cwd })`gh ${args}`).stdout;
8521
+ import { basename as basename3 } from "path";
8522
+
8523
+ // src/logNoise.ts
8524
+ var DEFAULT_REMIND_MS = 10 * 6e4;
8525
+ function timesWord(n) {
8526
+ const tens = n % 100;
8527
+ if (tens >= 11 && tens <= 14) return "\u0440\u0430\u0437";
8528
+ return n % 10 >= 2 && n % 10 <= 4 ? "\u0440\u0430\u0437\u0430" : "\u0440\u0430\u0437";
8529
+ }
8530
+ function makeRepeatFilter(opts = {}) {
8531
+ const remindAfterMs = opts.remindAfterMs ?? DEFAULT_REMIND_MS;
8532
+ const streaks = /* @__PURE__ */ new Map();
8533
+ return {
8534
+ say(key, message, now) {
8535
+ const prev = streaks.get(key);
8536
+ if (!prev || prev.message !== message) {
8537
+ const tail = prev && prev.hidden > 0 ? ` (\u0434\u043E \u044D\u0442\u043E\u0433\u043E \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 \u043C\u043E\u043B\u0447\u0430 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u043B\u043E\u0441\u044C \u0435\u0449\u0451 ${prev.hidden} ${timesWord(prev.hidden)})` : "";
8538
+ streaks.set(key, { message, since: now, saidAt: now, hidden: 0 });
8539
+ return `${message}${tail}`;
8540
+ }
8541
+ prev.hidden += 1;
8542
+ if (now - prev.saidAt < remindAfterMs) return null;
8543
+ const hidden = prev.hidden;
8544
+ const minutes = Math.max(1, Math.round((now - prev.since) / 6e4));
8545
+ prev.saidAt = now;
8546
+ prev.hidden = 0;
8547
+ return `${message} \u2014 \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 ${hidden} ${timesWord(hidden)} \u043F\u043E\u0434\u0440\u044F\u0434, \u0442\u044F\u043D\u0435\u0442\u0441\u044F ${minutes} \u043C\u0438\u043D`;
8548
+ },
8549
+ forget(key) {
8550
+ streaks.delete(key);
8551
+ }
8552
+ };
8553
+ }
8554
+
8555
+ // src/netTimeout.ts
8556
+ import { basename as basename2 } from "path";
8557
+ var GH_CALL_TIMEOUT_MS = 15e3;
8558
+ var GIT_LOCAL_TIMEOUT_MS = 5e3;
8559
+ var HARD_CAP_GRACE_MS = 2e3;
8560
+ var CallTimedOutError = class extends Error {
8561
+ timedOut = true;
8562
+ constructor(call, timeoutMs) {
8563
+ super(`\u0432\u044B\u0437\u043E\u0432 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0437\u0430 ${timeoutMs}\u043C\u0441 \u0438 \u043F\u0440\u0435\u0440\u0432\u0430\u043D: ${call}`);
8564
+ this.name = "CallTimedOutError";
8565
+ }
8566
+ };
8567
+ function isTimedOut(outcome) {
8568
+ return typeof outcome === "object" && outcome !== null && outcome.timedOut === true;
8569
+ }
8570
+ function noteTimedOut(scope, call, timeoutMs, outcome, warn = console.warn) {
8571
+ const line = `[${scope}] \u23F1 \u0432\u044B\u0437\u043E\u0432 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0437\u0430 ${humanMs(timeoutMs)} \u0438 \u043F\u0440\u0435\u0440\u0432\u0430\u043D: ${call} \u2014 \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u0438\u0434\u0451\u0442 \u0434\u0430\u043B\u044C\u0448\u0435`;
8572
+ if (!isTimedOut(outcome)) {
8573
+ timeoutNoise.forget(noiseKey(scope, call));
8574
+ return false;
8575
+ }
8576
+ const say = timeoutNoise.say(noiseKey(scope, call), line, Date.now());
8577
+ if (say) warn(say);
8578
+ return true;
8579
+ }
8580
+ function noiseKey(scope, call) {
8581
+ return `timeout:${scope}:${call}`;
8582
+ }
8583
+ var timeoutNoise = makeRepeatFilter();
8584
+ function humanMs(ms) {
8585
+ return ms >= 1e3 ? `${Math.round(ms / 1e3)}\u0441` : `${ms}\u043C\u0441`;
8586
+ }
8587
+ function destroyPipes(child) {
8588
+ for (const pipe of [child.stdout, child.stderr, child.stdin]) {
8589
+ try {
8590
+ pipe?.destroy?.();
8591
+ } catch {
8592
+ }
8593
+ }
8594
+ }
8595
+ async function withHardTimeout(child, scope, call, timeoutMs, warn = console.warn) {
8596
+ let timer;
8597
+ const capped = new Promise((_, reject) => {
8598
+ timer = setTimeout(() => {
8599
+ child.kill("SIGKILL");
8600
+ destroyPipes(child);
8601
+ reject(new CallTimedOutError(call, timeoutMs));
8602
+ }, timeoutMs + HARD_CAP_GRACE_MS);
8603
+ timer.unref?.();
8604
+ });
8605
+ try {
8606
+ const res = await Promise.race([child, capped]);
8607
+ noteTimedOut(scope, call, timeoutMs, res, warn);
8608
+ return res;
8609
+ } catch (e) {
8610
+ noteTimedOut(scope, call, timeoutMs, e, warn);
8611
+ throw e;
8612
+ } finally {
8613
+ if (timer) clearTimeout(timer);
8614
+ }
8615
+ }
8616
+ function callLabel(cmd2, args, head = 4) {
8617
+ const shown = args.slice(0, head).join(" ");
8618
+ return `${cmd2} ${shown}${args.length > head ? " \u2026" : ""}`.trim();
8619
+ }
8620
+ function gitCallLabel(repo, args) {
8621
+ return `git -C ${basename2(repo) || repo} ${args.join(" ")}`;
8622
+ }
8623
+
8624
+ // src/gh.ts
8625
+ async function runGh(args, timeoutMs, cwd) {
8626
+ const label2 = callLabel("gh", args) + (cwd ? ` (repo ${basename3(cwd)})` : "");
8627
+ const child = cwd ? $4({ cwd, timeout: timeoutMs })`gh ${args}` : $4({ timeout: timeoutMs })`gh ${args}`;
8628
+ return (await withHardTimeout(child, "gh", label2, timeoutMs)).stdout;
8629
+ }
8630
+ function ghRunner(timeoutMs = GH_CALL_TIMEOUT_MS) {
8631
+ return (args) => runGh(args, timeoutMs);
8632
+ }
8633
+ function ghRepoRunner(timeoutMs = GH_CALL_TIMEOUT_MS) {
8634
+ return (args, cwd) => runGh(args, timeoutMs, cwd);
8635
+ }
8636
+ var realRunner = ghRunner();
8637
+ var realRepoRunner = ghRepoRunner();
8502
8638
  async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
8503
8639
  if (!branch || !repoDir) return [];
8504
8640
  try {
@@ -8523,7 +8659,12 @@ function firstLine(s) {
8523
8659
  return s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "gh \u0432\u0435\u0440\u043D\u0443\u043B 403";
8524
8660
  }
8525
8661
  var realChecksRunner = async (pr) => {
8526
- const r = await $4({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
8662
+ const r = await withHardTimeout(
8663
+ $4({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`,
8664
+ "gh",
8665
+ callLabel("gh", ["pr", "checks", pr]),
8666
+ 15e3
8667
+ );
8527
8668
  const stdout = r.stdout ?? "";
8528
8669
  const stderr = r.stderr ?? "";
8529
8670
  if (!stdout.trim() && NO_CHECKS_STDERR.test(stderr)) return "[]";
@@ -9085,7 +9226,7 @@ function timelineState(db2, now = Date.now(), overmindLog = []) {
9085
9226
  id: r.id,
9086
9227
  name: r.name,
9087
9228
  slug: r.task_slug,
9088
- repo: basename2(r.repo || "") || "?",
9229
+ repo: basename4(r.repo || "") || "?",
9089
9230
  status: r.status,
9090
9231
  area: r.area,
9091
9232
  start: sqliteIso(r.created_at),
@@ -10862,6 +11003,14 @@ import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
10862
11003
  import { isAbsolute, join as join13, sep as sep2 } from "path";
10863
11004
  import { $ as $5 } from "execa";
10864
11005
  init_swarmFiles();
11006
+ function gitNet(repo, args, timeoutMs) {
11007
+ return withHardTimeout(
11008
+ $5({ reject: false, timeout: timeoutMs })`git -C ${repo} ${args}`,
11009
+ "git",
11010
+ gitCallLabel(repo, args),
11011
+ timeoutMs
11012
+ ).catch(() => null);
11013
+ }
10865
11014
  var WorktreeBusyError = class extends Error {
10866
11015
  constructor(branch, worktreePath) {
10867
11016
  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})`);
@@ -11066,8 +11215,9 @@ async function remoteMainRef(repo) {
11066
11215
  throw new Error(`\u043D\u0435 \u043D\u0430\u0448\u0451\u043B \u0434\u0435\u0444\u043E\u043B\u0442-\u0432\u0435\u0442\u043A\u0443 origin \u0432 ${repo} (\u043D\u0435\u0442 origin/HEAD, origin/main, origin/master)`);
11067
11216
  }
11068
11217
  var GITHUB_REMOTE_RE = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/i;
11069
- async function remoteRepoFullName(repo) {
11070
- const url = (await $5({ reject: false })`git -C ${repo} remote get-url origin`).stdout.trim();
11218
+ async function remoteRepoFullName(repo, timeoutMs = GIT_LOCAL_TIMEOUT_MS) {
11219
+ const res = await gitNet(repo, ["remote", "get-url", "origin"], timeoutMs);
11220
+ const url = res?.stdout.trim() ?? "";
11071
11221
  const m = url.match(GITHUB_REMOTE_RE);
11072
11222
  return m ? m[1] : null;
11073
11223
  }
@@ -11094,7 +11244,7 @@ var KNOWN_EXT = /* @__PURE__ */ new Set([...IMAGE_EXT, ...DOC_EXT, ...TEXT_EXT])
11094
11244
 
11095
11245
  // src/shotUpload.ts
11096
11246
  import { readFile as fsReadFile } from "fs/promises";
11097
- import { basename as basename3, isAbsolute as isAbsolute2, join as join15 } from "path";
11247
+ import { basename as basename5, isAbsolute as isAbsolute2, join as join15 } from "path";
11098
11248
  var SHOT_MAX_FILE_BYTES = 10 * 1024 * 1024;
11099
11249
 
11100
11250
  // src/taskless.ts
@@ -12194,7 +12344,7 @@ var thisHost = loadHostIdentity({ home: SWARM_HOME, envHostId: process.env.SWARM
12194
12344
  function fmtRun(r, opts = {}) {
12195
12345
  const mark = STATUS_MARK[r.status] ?? "?";
12196
12346
  const area = r.area ? ` [${r.area}]` : "";
12197
- const repo = r.repo ? ` (${basename4(r.repo)})` : "";
12347
+ const repo = r.repo ? ` (${basename6(r.repo)})` : "";
12198
12348
  const q = r.status === "blocked" ? " \u2190 \u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430" : "";
12199
12349
  const why = r.status === "stopped" && r.stop_reason ? ` \u2014 ${r.stop_reason}` : "";
12200
12350
  const priority = r.priority ? ` \u2691${r.priority > 0 ? "+" : ""}${r.priority}` : "";
@@ -12952,7 +13102,7 @@ switch (cmd) {
12952
13102
  process.exit(1);
12953
13103
  }
12954
13104
  const run = db.getRun(runId);
12955
- if (run) console.log(`#${run.id} ${run.name} ${run.task_slug}${run.repo ? ` (${basename4(run.repo)})` : ""}`);
13105
+ if (run) console.log(`#${run.id} ${run.name} ${run.task_slug}${run.repo ? ` (${basename6(run.repo)})` : ""}`);
12956
13106
  const events = db.events(runId);
12957
13107
  if (events.length === 0) console.log("\u0441\u043E\u0431\u044B\u0442\u0438\u0439 \u043D\u0435\u0442");
12958
13108
  else events.forEach((e) => console.log(`${e.at} ${e.kind}${e.text ? " " + e.text : ""}`));
package/dist/overmind.js CHANGED
@@ -1212,8 +1212,8 @@ var init_runGuards = __esm({
1212
1212
  function hasZoneModel(m) {
1213
1213
  return m.tasks > 0 && m.zones.length > 0;
1214
1214
  }
1215
- function testFileFeature(basename5) {
1216
- const head = basename5.replace(/\.(test|spec)\.[a-z]+$/i, "").split(".")[0].toLowerCase();
1215
+ function testFileFeature(basename7) {
1216
+ const head = basename7.replace(/\.(test|spec)\.[a-z]+$/i, "").split(".")[0].toLowerCase();
1217
1217
  return head && !GENERIC_DIRS.has(head) ? head : null;
1218
1218
  }
1219
1219
  function pathZoneCandidate(path) {
@@ -1850,10 +1850,24 @@ function asTokenUsage(m) {
1850
1850
  function budgetExhausted(result) {
1851
1851
  return result.subtype === "error_max_budget_usd" || result.terminal_reason === "budget_exhausted";
1852
1852
  }
1853
+ function sessionAbort(result) {
1854
+ if (budgetExhausted(result)) return null;
1855
+ const reason = result.terminal_reason ?? "";
1856
+ const subtype = result.subtype ?? "";
1857
+ if (reason === "aborted_tools") return { kind: "tools", raw: reason };
1858
+ if (reason === "aborted_streaming" || reason === "api_error") return { kind: "link", raw: reason };
1859
+ if (HONEST_ENDINGS.has(subtype) || HONEST_ENDINGS.has(reason)) return null;
1860
+ const failed = result.is_error === true || subtype === "error_during_execution" || reason.startsWith("aborted");
1861
+ if (!failed) return null;
1862
+ const raw = reason && reason !== "completed" ? reason : subtype || reason || "\u043E\u0448\u0438\u0431\u043A\u0430 \u0431\u0435\u0437 \u043C\u0435\u0442\u043A\u0438";
1863
+ return { kind: "other", raw };
1864
+ }
1865
+ var HONEST_ENDINGS;
1853
1866
  var init_claudeResult = __esm({
1854
1867
  "src/claudeResult.ts"() {
1855
1868
  "use strict";
1856
1869
  init_pricing();
1870
+ HONEST_ENDINGS = /* @__PURE__ */ new Set(["error_max_turns"]);
1857
1871
  }
1858
1872
  });
1859
1873
 
@@ -1928,6 +1942,9 @@ function writeMcpConfig(spec) {
1928
1942
  writeFileSync2(path, JSON.stringify(cfg, null, 2));
1929
1943
  return path;
1930
1944
  }
1945
+ function abortField(abort) {
1946
+ return abort ? { aborted: abort } : {};
1947
+ }
1931
1948
  var ISOLATION_FLAGS, MCP_CONFIG_FILE, claudeCodeExecutor;
1932
1949
  var init_claudeCode = __esm({
1933
1950
  "src/executors/claudeCode.ts"() {
@@ -1966,7 +1983,10 @@ var init_claudeCode = __esm({
1966
1983
  durationMs: spend.durationMs,
1967
1984
  // TASWM-377: «упёрся в потолок» — отдельный факт об этом же итоге, а не отдельный
1968
1985
  // разбор файла: у claude он лежит в тех же полях, что деньги и токены.
1969
- ...budgetExhausted(result) ? { stoppedByBudget: true } : {}
1986
+ ...budgetExhausted(result) ? { stoppedByBudget: true } : {},
1987
+ // TASWM-487: и «оборвалось» — третий факт об этом же итоге. Порядок важен: потолок
1988
+ // трат sessionAbort отсекает сам, поэтому оба ключа одновременно не появятся.
1989
+ ...abortField(sessionAbort(result))
1970
1990
  };
1971
1991
  },
1972
1992
  session: {
@@ -2887,12 +2907,50 @@ var init_heartbeat = __esm({
2887
2907
  }
2888
2908
  });
2889
2909
 
2910
+ // src/logNoise.ts
2911
+ function timesWord(n) {
2912
+ const tens = n % 100;
2913
+ if (tens >= 11 && tens <= 14) return "\u0440\u0430\u0437";
2914
+ return n % 10 >= 2 && n % 10 <= 4 ? "\u0440\u0430\u0437\u0430" : "\u0440\u0430\u0437";
2915
+ }
2916
+ function makeRepeatFilter(opts = {}) {
2917
+ const remindAfterMs = opts.remindAfterMs ?? DEFAULT_REMIND_MS;
2918
+ const streaks = /* @__PURE__ */ new Map();
2919
+ return {
2920
+ say(key, message, now) {
2921
+ const prev = streaks.get(key);
2922
+ if (!prev || prev.message !== message) {
2923
+ const tail2 = prev && prev.hidden > 0 ? ` (\u0434\u043E \u044D\u0442\u043E\u0433\u043E \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 \u043C\u043E\u043B\u0447\u0430 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u043B\u043E\u0441\u044C \u0435\u0449\u0451 ${prev.hidden} ${timesWord(prev.hidden)})` : "";
2924
+ streaks.set(key, { message, since: now, saidAt: now, hidden: 0 });
2925
+ return `${message}${tail2}`;
2926
+ }
2927
+ prev.hidden += 1;
2928
+ if (now - prev.saidAt < remindAfterMs) return null;
2929
+ const hidden = prev.hidden;
2930
+ const minutes = Math.max(1, Math.round((now - prev.since) / 6e4));
2931
+ prev.saidAt = now;
2932
+ prev.hidden = 0;
2933
+ return `${message} \u2014 \u0442\u043E \u0436\u0435 \u0441\u0430\u043C\u043E\u0435 ${hidden} ${timesWord(hidden)} \u043F\u043E\u0434\u0440\u044F\u0434, \u0442\u044F\u043D\u0435\u0442\u0441\u044F ${minutes} \u043C\u0438\u043D`;
2934
+ },
2935
+ forget(key) {
2936
+ streaks.delete(key);
2937
+ }
2938
+ };
2939
+ }
2940
+ var DEFAULT_REMIND_MS;
2941
+ var init_logNoise = __esm({
2942
+ "src/logNoise.ts"() {
2943
+ "use strict";
2944
+ DEFAULT_REMIND_MS = 10 * 6e4;
2945
+ }
2946
+ });
2947
+
2890
2948
  // src/agentToken.ts
2891
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
2892
- import { join as join9 } from "path";
2949
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3 } from "fs";
2950
+ import { join as join8 } from "path";
2893
2951
  function loadAgentToken() {
2894
2952
  try {
2895
- const s = JSON.parse(readFileSync7(STORE_PATH, "utf8"));
2953
+ const s = JSON.parse(readFileSync6(STORE_PATH, "utf8"));
2896
2954
  return s.token ? s : null;
2897
2955
  } catch {
2898
2956
  return null;
@@ -2945,7 +3003,7 @@ var init_agentToken = __esm({
2945
3003
  "src/agentToken.ts"() {
2946
3004
  "use strict";
2947
3005
  init_config();
2948
- STORE_PATH = join9(SWARM_HOME, "agent.json");
3006
+ STORE_PATH = join8(SWARM_HOME, "agent.json");
2949
3007
  base = (api) => api.replace(/\/$/, "");
2950
3008
  TOKEN_PROBE_TIMEOUT_MS = 8e3;
2951
3009
  }
@@ -3304,11 +3362,100 @@ var init_procCleanup = __esm({
3304
3362
  }
3305
3363
  });
3306
3364
 
3365
+ // src/netTimeout.ts
3366
+ import { basename as basename2 } from "path";
3367
+ function isTimedOut(outcome) {
3368
+ return typeof outcome === "object" && outcome !== null && outcome.timedOut === true;
3369
+ }
3370
+ function noteTimedOut(scope, call, timeoutMs, outcome, warn = console.warn) {
3371
+ const line = `[${scope}] \u23F1 \u0432\u044B\u0437\u043E\u0432 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0437\u0430 ${humanMs(timeoutMs)} \u0438 \u043F\u0440\u0435\u0440\u0432\u0430\u043D: ${call} \u2014 \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u0438\u0434\u0451\u0442 \u0434\u0430\u043B\u044C\u0448\u0435`;
3372
+ if (!isTimedOut(outcome)) {
3373
+ timeoutNoise.forget(noiseKey(scope, call));
3374
+ return false;
3375
+ }
3376
+ const say = timeoutNoise.say(noiseKey(scope, call), line, Date.now());
3377
+ if (say) warn(say);
3378
+ return true;
3379
+ }
3380
+ function noiseKey(scope, call) {
3381
+ return `timeout:${scope}:${call}`;
3382
+ }
3383
+ function humanMs(ms) {
3384
+ return ms >= 1e3 ? `${Math.round(ms / 1e3)}\u0441` : `${ms}\u043C\u0441`;
3385
+ }
3386
+ function destroyPipes(child) {
3387
+ for (const pipe of [child.stdout, child.stderr, child.stdin]) {
3388
+ try {
3389
+ pipe?.destroy?.();
3390
+ } catch {
3391
+ }
3392
+ }
3393
+ }
3394
+ async function withHardTimeout(child, scope, call, timeoutMs, warn = console.warn) {
3395
+ let timer;
3396
+ const capped = new Promise((_, reject) => {
3397
+ timer = setTimeout(() => {
3398
+ child.kill("SIGKILL");
3399
+ destroyPipes(child);
3400
+ reject(new CallTimedOutError(call, timeoutMs));
3401
+ }, timeoutMs + HARD_CAP_GRACE_MS);
3402
+ timer.unref?.();
3403
+ });
3404
+ try {
3405
+ const res = await Promise.race([child, capped]);
3406
+ noteTimedOut(scope, call, timeoutMs, res, warn);
3407
+ return res;
3408
+ } catch (e) {
3409
+ noteTimedOut(scope, call, timeoutMs, e, warn);
3410
+ throw e;
3411
+ } finally {
3412
+ if (timer) clearTimeout(timer);
3413
+ }
3414
+ }
3415
+ function callLabel(cmd, args, head = 4) {
3416
+ const shown = args.slice(0, head).join(" ");
3417
+ return `${cmd} ${shown}${args.length > head ? " \u2026" : ""}`.trim();
3418
+ }
3419
+ function gitCallLabel(repo, args) {
3420
+ return `git -C ${basename2(repo) || repo} ${args.join(" ")}`;
3421
+ }
3422
+ var GH_CALL_TIMEOUT_MS, GIT_NET_TIMEOUT_MS, GIT_LOCAL_TIMEOUT_MS, GIT_HEAVY_NET_TIMEOUT_MS, HARD_CAP_GRACE_MS, CallTimedOutError, timeoutNoise;
3423
+ var init_netTimeout = __esm({
3424
+ "src/netTimeout.ts"() {
3425
+ "use strict";
3426
+ init_logNoise();
3427
+ GH_CALL_TIMEOUT_MS = 15e3;
3428
+ GIT_NET_TIMEOUT_MS = 15e3;
3429
+ GIT_LOCAL_TIMEOUT_MS = 5e3;
3430
+ GIT_HEAVY_NET_TIMEOUT_MS = 6e4;
3431
+ HARD_CAP_GRACE_MS = 2e3;
3432
+ CallTimedOutError = class extends Error {
3433
+ timedOut = true;
3434
+ constructor(call, timeoutMs) {
3435
+ super(`\u0432\u044B\u0437\u043E\u0432 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0437\u0430 ${timeoutMs}\u043C\u0441 \u0438 \u043F\u0440\u0435\u0440\u0432\u0430\u043D: ${call}`);
3436
+ this.name = "CallTimedOutError";
3437
+ }
3438
+ };
3439
+ timeoutNoise = makeRepeatFilter();
3440
+ }
3441
+ });
3442
+
3307
3443
  // src/git.ts
3308
3444
  import { existsSync as existsSync10, readdirSync as readdirSync11, statSync as statSync8 } from "fs";
3309
3445
  import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
3310
3446
  import { isAbsolute as isAbsolute2, join as join17, sep as sep2 } from "path";
3311
3447
  import { $ as $2 } from "execa";
3448
+ function gitNet(repo, args, timeoutMs) {
3449
+ return withHardTimeout(
3450
+ $2({ reject: false, timeout: timeoutMs })`git -C ${repo} ${args}`,
3451
+ "git",
3452
+ gitCallLabel(repo, args),
3453
+ timeoutMs
3454
+ ).catch(() => null);
3455
+ }
3456
+ function gitNetStrict(repo, args, timeoutMs) {
3457
+ return withHardTimeout($2({ timeout: timeoutMs })`git -C ${repo} ${args}`, "git", gitCallLabel(repo, args), timeoutMs);
3458
+ }
3312
3459
  function isUnrecoverableRefError(e) {
3313
3460
  if (e instanceof MissingRefError) return true;
3314
3461
  const msg = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
@@ -3533,26 +3680,27 @@ async function remoteMainRef(repo) {
3533
3680
  throw new Error(`\u043D\u0435 \u043D\u0430\u0448\u0451\u043B \u0434\u0435\u0444\u043E\u043B\u0442-\u0432\u0435\u0442\u043A\u0443 origin \u0432 ${repo} (\u043D\u0435\u0442 origin/HEAD, origin/main, origin/master)`);
3534
3681
  }
3535
3682
  async function remoteBranchExists(repo, branch) {
3536
- const res = await $2({ reject: false })`git -C ${repo} ls-remote --heads origin ${`refs/heads/${branch}`}`;
3537
- if (res.exitCode !== 0) return null;
3683
+ const res = await gitNet(repo, ["ls-remote", "--heads", "origin", `refs/heads/${branch}`], 5e3);
3684
+ if (!res || res.exitCode !== 0) return null;
3538
3685
  return res.stdout.trim().length > 0;
3539
3686
  }
3540
- async function remoteBaseRef(repo, branch) {
3541
- await $2({ reject: false })`git -C ${repo} fetch --quiet origin ${branch}`;
3687
+ async function remoteBaseRef(repo, branch, timeoutMs = GIT_NET_TIMEOUT_MS) {
3688
+ await gitNet(repo, ["fetch", "--quiet", "origin", branch], timeoutMs);
3542
3689
  return `origin/${branch}`;
3543
3690
  }
3544
- async function remoteRepoFullName(repo) {
3545
- const url = (await $2({ reject: false })`git -C ${repo} remote get-url origin`).stdout.trim();
3691
+ async function remoteRepoFullName(repo, timeoutMs = GIT_LOCAL_TIMEOUT_MS) {
3692
+ const res = await gitNet(repo, ["remote", "get-url", "origin"], timeoutMs);
3693
+ const url = res?.stdout.trim() ?? "";
3546
3694
  const m = url.match(GITHUB_REMOTE_RE);
3547
3695
  return m ? m[1] : null;
3548
3696
  }
3549
3697
  async function journalForBranch(repo, branch, slug) {
3550
- await $2({ reject: false })`git -C ${repo} fetch --quiet origin ${branch}`;
3698
+ await gitNet(repo, ["fetch", "--quiet", "origin", branch], GIT_NET_TIMEOUT_MS);
3551
3699
  const out = await $2({ reject: false })`git -C ${repo} show ${`origin/${branch}:journal/${slug}.md`}`;
3552
3700
  return out.exitCode === 0 && out.stdout ? out.stdout : null;
3553
3701
  }
3554
3702
  async function prepareDetachedWorktree(repo, branch, worktree) {
3555
- await $2`git -C ${repo} fetch --quiet origin`;
3703
+ await gitNetStrict(repo, ["fetch", "--quiet", "origin"], GIT_HEAVY_NET_TIMEOUT_MS);
3556
3704
  const ref = `origin/${branch}`;
3557
3705
  if (!await revParse(repo, `${ref}^{commit}`)) throw new MissingRefError(ref, repo);
3558
3706
  await $2`git -C ${repo} worktree add --detach --force ${worktree} ${ref}`;
@@ -3569,7 +3717,7 @@ async function mergeRef(worktree, ref) {
3569
3717
  return { clean: false, conflicts: u ? u.split("\n") : [], changed: false };
3570
3718
  }
3571
3719
  async function pushHeadTo(worktree, branch) {
3572
- await $2`git -C ${worktree} push origin ${`HEAD:${branch}`}`;
3720
+ await gitNetStrict(worktree, ["push", "origin", `HEAD:${branch}`], GIT_HEAVY_NET_TIMEOUT_MS);
3573
3721
  }
3574
3722
  async function snapshotWorktree(worktree, message) {
3575
3723
  try {
@@ -3578,7 +3726,7 @@ async function snapshotWorktree(worktree, message) {
3578
3726
  await $2({ reject: false })`git -C ${worktree} add -A`;
3579
3727
  const commit = await $2({ reject: false })`git -C ${worktree} commit --no-verify -m ${message}`;
3580
3728
  if (commit.exitCode !== 0) return false;
3581
- await $2({ reject: false })`git -C ${worktree} push`;
3729
+ await gitNet(worktree, ["push"], GIT_NET_TIMEOUT_MS);
3582
3730
  return true;
3583
3731
  } catch {
3584
3732
  return false;
@@ -3594,6 +3742,7 @@ var init_git = __esm({
3594
3742
  "src/git.ts"() {
3595
3743
  "use strict";
3596
3744
  init_procCleanup();
3745
+ init_netTimeout();
3597
3746
  init_swarmFiles();
3598
3747
  WorktreeBusyError = class extends Error {
3599
3748
  constructor(branch, worktreePath) {
@@ -4281,7 +4430,7 @@ var init_attachments = __esm({
4281
4430
 
4282
4431
  // src/shotUpload.ts
4283
4432
  import { readFile as fsReadFile } from "fs/promises";
4284
- import { basename as basename2, isAbsolute as isAbsolute3, join as join21 } from "path";
4433
+ import { basename as basename4, isAbsolute as isAbsolute3, join as join21 } from "path";
4285
4434
  function shotMimetype(path) {
4286
4435
  const dot = path.lastIndexOf(".");
4287
4436
  if (dot < 0) return null;
@@ -4320,7 +4469,7 @@ async function readShots(paths, opts) {
4320
4469
  skipped.push({ path, reason: `${(bytes.byteLength / 1024 / 1024).toFixed(1)} \u041C\u0411 \u2014 \u0431\u043E\u043B\u044C\u0448\u0435 \u043B\u0438\u043C\u0438\u0442\u0430 ${maxBytes / 1024 / 1024} \u041C\u0411` });
4321
4470
  continue;
4322
4471
  }
4323
- files.push({ path, fileName: basename2(path), mimetype, bytes });
4472
+ files.push({ path, fileName: basename4(path), mimetype, bytes });
4324
4473
  }
4325
4474
  return { files, skipped };
4326
4475
  }
@@ -6217,7 +6366,7 @@ var init_droneStore = __esm({
6217
6366
  // src/overmind.ts
6218
6367
  import { homedir as homedir5 } from "os";
6219
6368
  import { existsSync as existsSync17, readFileSync as readFileSync20, readdirSync as readdirSync13, renameSync as renameSync11 } from "fs";
6220
- import { basename as basename4, dirname as dirname4, join as join30 } from "path";
6369
+ import { basename as basename6, dirname as dirname4, join as join30 } from "path";
6221
6370
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
6222
6371
  import { spawn as spawn3 } from "child_process";
6223
6372
 
@@ -8881,6 +9030,49 @@ function rotateDaemonLog(path, opts, io = realIo) {
8881
9030
  return "rotated";
8882
9031
  }
8883
9032
 
9033
+ // src/logStamp.ts
9034
+ import { format } from "util";
9035
+ function logStamp(at) {
9036
+ return at.toISOString();
9037
+ }
9038
+ function stampMessage(text, at) {
9039
+ const ts = logStamp(at);
9040
+ return text.split("\n").map((line) => line.length ? `${ts} ${line}` : ts).join("\n");
9041
+ }
9042
+ function utcOffsetText(at) {
9043
+ const minutes = -at.getTimezoneOffset();
9044
+ const sign2 = minutes < 0 ? "-" : "+";
9045
+ const abs = Math.abs(minutes);
9046
+ const pad = (n) => String(n).padStart(2, "0");
9047
+ return `UTC${sign2}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
9048
+ }
9049
+ function localZoneLine(at, zone) {
9050
+ const tz = zone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
9051
+ return `[overmind] \u0432\u0440\u0435\u043C\u044F \u0432 \u043B\u043E\u0433\u0435 \u2014 UTC, ISO-8601 \u0441 \u043C\u0438\u043B\u043B\u0438\u0441\u0435\u043A\u0443\u043D\u0434\u0430\u043C\u0438 (${logStamp(at)}); \u043C\u0435\u0441\u0442\u043D\u043E\u0435 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 ${utcOffsetText(at)}${tz ? ` (${tz})` : ""}`;
9052
+ }
9053
+ var LEVELS = ["log", "info", "warn", "error"];
9054
+ var installed = false;
9055
+ function installLogStamps(now = () => /* @__PURE__ */ new Date()) {
9056
+ if (installed) return () => {
9057
+ };
9058
+ installed = true;
9059
+ const original = LEVELS.map((level) => [level, console[level]]);
9060
+ for (const [level, orig] of original) {
9061
+ const write = orig.bind(console);
9062
+ console[level] = (...args) => {
9063
+ write(stampMessage(format(...args), now()));
9064
+ };
9065
+ }
9066
+ return () => {
9067
+ for (const [level, orig] of original) console[level] = orig;
9068
+ installed = false;
9069
+ };
9070
+ }
9071
+
9072
+ // src/overmind.ts
9073
+ init_logNoise();
9074
+ init_linkFailure();
9075
+
8884
9076
  // src/repoRegistry.ts
8885
9077
  init_config();
8886
9078
  import { $ } from "execa";
@@ -8919,13 +9111,13 @@ async function buildRepoRegistry(paths) {
8919
9111
  }
8920
9112
 
8921
9113
  // src/repoMemory.ts
8922
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
8923
- import { join as join8 } from "path";
9114
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
9115
+ import { join as join9 } from "path";
8924
9116
  var FILE = "repos.json";
8925
9117
  function loadRememberedRepos(home) {
8926
9118
  let raw;
8927
9119
  try {
8928
- raw = readFileSync6(join8(home, FILE), "utf8");
9120
+ raw = readFileSync7(join9(home, FILE), "utf8");
8929
9121
  } catch {
8930
9122
  return [];
8931
9123
  }
@@ -8940,7 +9132,7 @@ function loadRememberedRepos(home) {
8940
9132
  function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
8941
9133
  const clean = paths.filter((p) => p.trim().length > 0);
8942
9134
  if (clean.length === 0) return;
8943
- writeFileSync6(join8(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
9135
+ writeFileSync7(join9(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
8944
9136
  `);
8945
9137
  }
8946
9138
  function pickRepoPaths(args) {
@@ -9185,9 +9377,13 @@ async function refreshRepoMap(opts) {
9185
9377
  added++;
9186
9378
  }
9187
9379
  scannedAt = stamp();
9188
- log(
9189
- `[overmind] \u043F\u0440\u043E\u0448\u0451\u043B \u043F\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C (${SCAN_REASON_TEXT[decision.reason]}): ${added > 0 ? `\u043D\u0430\u0448\u0451\u043B \u0435\u0449\u0451 ${added}` : "\u043D\u043E\u0432\u043E\u0433\u043E \u043D\u0435 \u043D\u0430\u0448\u0451\u043B"}, \u0432\u0441\u0435\u0433\u043E \u043A\u043B\u043E\u043D\u043E\u0432 ${clones.size}, \u0437\u0430 ${Date.now() - startedAt}\u043C\u0441. \u0418\u0441\u043A\u0430\u043B \u0432: ${opts.roots.length ? opts.roots.slice(0, 6).join(", ") : "\u043D\u0438\u0433\u0434\u0435 \u2014 \u043F\u043E\u0438\u0441\u043A \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D (SWARM_SCAN_ROOTS)"}`
9190
- );
9380
+ const outcome = `[overmind] \u043F\u0440\u043E\u0448\u0451\u043B \u043F\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C (${SCAN_REASON_TEXT[decision.reason]}): ${added > 0 ? `\u043D\u0430\u0448\u0451\u043B \u0435\u0449\u0451 ${added}` : "\u043D\u043E\u0432\u043E\u0433\u043E \u043D\u0435 \u043D\u0430\u0448\u0451\u043B"}, \u0432\u0441\u0435\u0433\u043E \u043A\u043B\u043E\u043D\u043E\u0432 ${clones.size}`;
9381
+ if (added > 0 || opts.state.announcedScan !== outcome) {
9382
+ opts.state.announcedScan = outcome;
9383
+ log(
9384
+ `${outcome}, \u0437\u0430 ${Date.now() - startedAt}\u043C\u0441. \u0418\u0441\u043A\u0430\u043B \u0432: ${opts.roots.length ? opts.roots.slice(0, 6).join(", ") : "\u043D\u0438\u0433\u0434\u0435 \u2014 \u043F\u043E\u0438\u0441\u043A \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D (SWARM_SCAN_ROOTS)"}`
9385
+ );
9386
+ }
9191
9387
  }
9192
9388
  const registry = [...clones.values()].map((c) => ({
9193
9389
  repoFullName: c.repoFullName,
@@ -9199,8 +9395,10 @@ async function refreshRepoMap(opts) {
9199
9395
  scannedAt
9200
9396
  );
9201
9397
  if (!answer.ok) {
9202
- log(describeReposFailure(answer.kind, answer.detail));
9203
- log(`[overmind] \u0438\u0442\u043E\u0433: \u043A\u043B\u043E\u043D\u043E\u0432 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E ${clones.size}, \u0441\u043F\u0438\u0441\u043E\u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0440\u043E\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D (\u0441\u043C. \u0441\u0442\u0440\u043E\u043A\u0443 \u0432\u044B\u0448\u0435)`);
9398
+ announceMap(opts.state, log, [
9399
+ describeReposFailure(answer.kind, answer.detail),
9400
+ `[overmind] \u0438\u0442\u043E\u0433: \u043A\u043B\u043E\u043D\u043E\u0432 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E ${clones.size}, \u0441\u043F\u0438\u0441\u043E\u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0440\u043E\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D (\u0441\u043C. \u0441\u0442\u0440\u043E\u043A\u0443 \u0432\u044B\u0448\u0435)`
9401
+ ]);
9204
9402
  return registry;
9205
9403
  }
9206
9404
  opts.state.unknown = false;
@@ -9209,18 +9407,39 @@ async function refreshRepoMap(opts) {
9209
9407
  opts.state.roots = [...opts.roots];
9210
9408
  opts.state.at = stamp();
9211
9409
  if (wantedNames.length === 0) {
9212
- log("[overmind] \u0443 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043D\u0435\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 \u0441 \u0432\u043A\u043B\u044E\u0447\u0451\u043D\u043D\u044B\u043C \u0440\u043E\u0435\u043C \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u043F\u043E \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u043C\u0443 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435");
9410
+ announceMap(opts.state, log, ["[overmind] \u0443 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043D\u0435\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 \u0441 \u0432\u043A\u043B\u044E\u0447\u0451\u043D\u043D\u044B\u043C \u0440\u043E\u0435\u043C \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u043F\u043E \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u043C\u0443 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435"]);
9213
9411
  return registry;
9214
9412
  }
9215
- log(`[overmind] ${describeMapSummary(opts.state, opts.roots)}`);
9216
- for (const c of opts.state.clones) log(`[overmind] ${describeClone(c)}`);
9217
9413
  const prefixOf = new Map(answer.repos.map((r) => [r.repoFullName, r.taskPrefix]));
9218
- for (const name of opts.state.missing) {
9219
- const prefix = prefixOf.get(name);
9220
- log(`[overmind] ${describeMissing(prefix ? `${name} (\u043F\u0440\u043E\u0435\u043A\u0442 ${prefix})` : name, opts.roots)}`);
9221
- }
9414
+ announceMap(
9415
+ opts.state,
9416
+ log,
9417
+ [
9418
+ `[overmind] ${describeMapSummary(opts.state, opts.roots)}`,
9419
+ ...opts.state.clones.map((c) => `[overmind] ${describeClone(c)}`),
9420
+ ...opts.state.missing.map((name) => {
9421
+ const prefix = prefixOf.get(name);
9422
+ return `[overmind] ${describeMissing(prefix ? `${name} (\u043F\u0440\u043E\u0435\u043A\u0442 ${prefix})` : name, opts.roots)}`;
9423
+ })
9424
+ ],
9425
+ mapSignature(opts.state, opts.roots)
9426
+ );
9222
9427
  return registry;
9223
9428
  }
9429
+ function announceMap(state, log, lines, signature = lines.join("\n")) {
9430
+ if (state.announced === signature) return;
9431
+ const first = state.announced === void 0;
9432
+ state.announced = signature;
9433
+ if (!first) log("[overmind] \u043A\u0430\u0440\u0442\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0438\u0437\u043C\u0435\u043D\u0438\u043B\u0430\u0441\u044C:");
9434
+ for (const line of lines) log(line);
9435
+ }
9436
+ function mapSignature(state, roots) {
9437
+ return [
9438
+ ...state.clones.map((c) => `${c.repoFullName}=${c.path}${c.alternatives.length ? `+${c.alternatives.join("+")}` : ""}`),
9439
+ `missing:${state.missing.join(",")}`,
9440
+ `roots:${roots.join(",")}`
9441
+ ].join("\n");
9442
+ }
9224
9443
  function describeMapSummary(state, roots) {
9225
9444
  const found = state.clones.length;
9226
9445
  const where = roots.length ? roots.slice(0, 6).join(", ") : "\u043F\u043E\u0438\u0441\u043A \u043F\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D (SWARM_SCAN_ROOTS)";
@@ -9917,17 +10136,20 @@ import { readdirSync as readdirSync10, readFileSync as readFileSync12 } from "fs
9917
10136
  import { join as join16 } from "path";
9918
10137
  init_executors();
9919
10138
  function endedItself(outcome) {
9920
- return outcome === "graceful" || outcome === "budget";
10139
+ return outcome === "graceful" || outcome === "budget" || outcome === "aborted";
10140
+ }
10141
+ function wasAborted(outcome) {
10142
+ return outcome === "aborted";
9921
10143
  }
9922
10144
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
9923
- function classifyExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
10145
+ function readLatestResultRaw(runLogsDir, runLogKeys) {
9924
10146
  const dir = findRunLogDir(runLogsDir, runLogKeys);
9925
- if (!dir) return "unknown";
10147
+ if (!dir) return null;
9926
10148
  let files;
9927
10149
  try {
9928
10150
  files = readdirSync10(dir);
9929
10151
  } catch {
9930
- return "unknown";
10152
+ return null;
9931
10153
  }
9932
10154
  let latest = null;
9933
10155
  for (const f of files) {
@@ -9936,21 +10158,35 @@ function classifyExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
9936
10158
  const idx = Number(m[1]);
9937
10159
  if (!latest || idx > latest.idx) latest = { idx, name: f };
9938
10160
  }
9939
- if (!latest) return "unknown";
9940
- let raw;
10161
+ if (!latest) return null;
9941
10162
  try {
9942
- raw = readFileSync12(join16(dir, latest.name), "utf8");
10163
+ return readFileSync12(join16(dir, latest.name), "utf8");
9943
10164
  } catch {
9944
- return "unknown";
10165
+ return null;
9945
10166
  }
10167
+ }
10168
+ function classifyExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
10169
+ const raw = readLatestResultRaw(runLogsDir, runLogKeys);
10170
+ if (raw == null) return "unknown";
9946
10171
  const result = executor.parseResult(raw, null);
9947
10172
  if (!result) return "crash";
9948
- return result.stoppedByBudget ? "budget" : "graceful";
10173
+ if (result.stoppedByBudget) return "budget";
10174
+ if (result.aborted) return "aborted";
10175
+ return "graceful";
10176
+ }
10177
+ function abortOfExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
10178
+ const raw = readLatestResultRaw(runLogsDir, runLogKeys);
10179
+ if (raw == null) return null;
10180
+ return executor.parseResult(raw, null)?.aborted ?? null;
9949
10181
  }
9950
10182
  function makeExitClassifier(runLogsDir, runLogKeys, executor) {
9951
10183
  const exec = executor ?? resolveExecutor();
9952
10184
  return (run) => classifyExit(runLogsDir, runLogKeys(run.id), exec);
9953
10185
  }
10186
+ function makeAbortReader(runLogsDir, runLogKeys, executor) {
10187
+ const exec = executor ?? resolveExecutor();
10188
+ return (run) => abortOfExit(runLogsDir, runLogKeys(run.id), exec);
10189
+ }
9954
10190
 
9955
10191
  // src/orchestrator.ts
9956
10192
  init_src();
@@ -10692,9 +10928,22 @@ init_git();
10692
10928
 
10693
10929
  // src/gh.ts
10694
10930
  init_src();
10931
+ init_netTimeout();
10695
10932
  import { $ as $3 } from "execa";
10696
- var realRunner = async (args) => (await $3`gh ${args}`).stdout;
10697
- var realRepoRunner = async (args, cwd) => (await $3({ cwd })`gh ${args}`).stdout;
10933
+ import { basename as basename3 } from "path";
10934
+ async function runGh(args, timeoutMs, cwd) {
10935
+ const label2 = callLabel("gh", args) + (cwd ? ` (repo ${basename3(cwd)})` : "");
10936
+ const child = cwd ? $3({ cwd, timeout: timeoutMs })`gh ${args}` : $3({ timeout: timeoutMs })`gh ${args}`;
10937
+ return (await withHardTimeout(child, "gh", label2, timeoutMs)).stdout;
10938
+ }
10939
+ function ghRunner(timeoutMs = GH_CALL_TIMEOUT_MS) {
10940
+ return (args) => runGh(args, timeoutMs);
10941
+ }
10942
+ function ghRepoRunner(timeoutMs = GH_CALL_TIMEOUT_MS) {
10943
+ return (args, cwd) => runGh(args, timeoutMs, cwd);
10944
+ }
10945
+ var realRunner = ghRunner();
10946
+ var realRepoRunner = ghRepoRunner();
10698
10947
  var GH_PR_RE = /^(?:https?:\/\/)?(?:www\.)?github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+(?:[/?#].*)?$/i;
10699
10948
  function isGithubPrUrl(url) {
10700
10949
  return GH_PR_RE.test(url.trim());
@@ -10784,7 +11033,7 @@ async function closePR(prUrl, run = realRunner) {
10784
11033
  async function postPrComment(prUrl, body, run = realRunner) {
10785
11034
  await run(["pr", "comment", prUrl, "--body", body]);
10786
11035
  }
10787
- var realCommentsRunner = async (prUrl) => (await $3`gh pr view ${prUrl} --json comments`).stdout;
11036
+ var realCommentsRunner = (prUrl) => runGh(["pr", "view", prUrl, "--json", "comments"], GH_CALL_TIMEOUT_MS);
10788
11037
  var VERDICT_RE = /^[^A-Za-z]*(APPROVED?|LGTM|(?:REQUEST(?:ED)?\s+)?CHANGES(?:\s+REQUESTED)?)\b/i;
10789
11038
  var APPROVE_RE = /^(APPROVE|LGTM)/i;
10790
11039
  function verdictLabel(verdict) {
@@ -10826,7 +11075,7 @@ async function scanPrComments(prUrl, sinceIso, run, pick3) {
10826
11075
  async function latestVerifyVerdictComment(prUrl, sinceIso, run = realCommentsRunner) {
10827
11076
  return scanPrComments(prUrl, sinceIso, run, (body) => parseVerifyVerdictComment(body));
10828
11077
  }
10829
- var realPrCommitsRunner = async (prUrl) => (await $3`gh pr view ${prUrl} --json commits`).stdout;
11078
+ var realPrCommitsRunner = (prUrl) => runGh(["pr", "view", prUrl, "--json", "commits"], GH_CALL_TIMEOUT_MS);
10830
11079
  async function prHasCommitsSince(prUrl, sinceIso, run = realPrCommitsRunner) {
10831
11080
  if (!isGithubPrUrl(prUrl)) return false;
10832
11081
  let out;
@@ -10850,7 +11099,7 @@ async function prHasCommitsSince(prUrl, sinceIso, run = realPrCommitsRunner) {
10850
11099
  return Number.isFinite(ms) && (!Number.isFinite(sinceMs) || ms >= sinceMs);
10851
11100
  });
10852
11101
  }
10853
- var realPrCommitTitlesRunner = async (prUrl) => (await $3`gh pr view ${prUrl} --json commits`).stdout;
11102
+ var realPrCommitTitlesRunner = (prUrl) => runGh(["pr", "view", prUrl, "--json", "commits"], GH_CALL_TIMEOUT_MS);
10854
11103
  async function prCommitTitles(prUrl, run = realPrCommitTitlesRunner) {
10855
11104
  if (!isGithubPrUrl(prUrl)) return [];
10856
11105
  let out;
@@ -10889,7 +11138,12 @@ function firstLine(s) {
10889
11138
  return s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "gh \u0432\u0435\u0440\u043D\u0443\u043B 403";
10890
11139
  }
10891
11140
  var realChecksRunner = async (pr) => {
10892
- const r = await $3({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
11141
+ const r = await withHardTimeout(
11142
+ $3({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`,
11143
+ "gh",
11144
+ callLabel("gh", ["pr", "checks", pr]),
11145
+ 15e3
11146
+ );
10893
11147
  const stdout = r.stdout ?? "";
10894
11148
  const stderr = r.stderr ?? "";
10895
11149
  if (!stdout.trim() && NO_CHECKS_STDERR.test(stderr)) return "[]";
@@ -10958,7 +11212,15 @@ async function prCiStatus(pr, run = realChecksRunner) {
10958
11212
  if (rows.some((r) => r.bucket === "pending")) return { bucket: "pending", failing: [], failingRunIds: [], ...counts };
10959
11213
  return { bucket: "pass", failing: [], failingRunIds: [], ...counts };
10960
11214
  }
10961
- var realRunLogRunner = async (runId) => (await $3({ reject: false, timeout: 3e4, maxBuffer: 10 * 1024 * 1024 })`gh run view ${runId} --log-failed`).stdout;
11215
+ var realRunLogRunner = async (runId) => {
11216
+ const r = await withHardTimeout(
11217
+ $3({ reject: false, timeout: 3e4, maxBuffer: 10 * 1024 * 1024 })`gh run view ${runId} --log-failed`,
11218
+ "gh",
11219
+ callLabel("gh", ["run", "view", runId]),
11220
+ 3e4
11221
+ );
11222
+ return r.stdout;
11223
+ };
10962
11224
  var CI_SUMMARY_LOG_CHARS = 4e3;
10963
11225
  var CI_SUMMARY_MAX_RUNS = 2;
10964
11226
  async function ciFailureSummary(failing, failingRunIds, logRun = realRunLogRunner) {
@@ -13233,7 +13495,7 @@ function makeEnvGate(opts) {
13233
13495
  // src/web.ts
13234
13496
  init_src();
13235
13497
  import { randomBytes as randomBytes3 } from "crypto";
13236
- import { basename as basename3 } from "path";
13498
+ import { basename as basename5 } from "path";
13237
13499
  import { serve as serve2 } from "@hono/node-server";
13238
13500
 
13239
13501
  // src/diff.ts
@@ -13439,7 +13701,7 @@ async function changedPaths(worktree, opts = {}) {
13439
13701
  // package.json
13440
13702
  var package_default = {
13441
13703
  name: "@taskless-app/swarm",
13442
- version: "0.6.21",
13704
+ version: "0.6.23",
13443
13705
  type: "module",
13444
13706
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
13445
13707
  keywords: [
@@ -13489,7 +13751,7 @@ var package_default = {
13489
13751
  execa: "^10.0.0",
13490
13752
  zod: "3.25.76"
13491
13753
  },
13492
- gitHead: "584411cbe69583a8727e89caa8ae7e626f8fd8ca"
13754
+ gitHead: "af6c4af0f523891b5db805d9b4583021688ed1f5"
13493
13755
  };
13494
13756
 
13495
13757
  // src/version.ts
@@ -13703,7 +13965,7 @@ function timelineState(db, now = Date.now(), overmindLog = []) {
13703
13965
  id: r.id,
13704
13966
  name: r.name,
13705
13967
  slug: r.task_slug,
13706
- repo: basename3(r.repo || "") || "?",
13968
+ repo: basename5(r.repo || "") || "?",
13707
13969
  status: r.status,
13708
13970
  area: r.area,
13709
13971
  start: sqliteIso(r.created_at),
@@ -15345,9 +15607,9 @@ async function runSelfUpdate(io) {
15345
15607
  if (!exists(join28(repo, "pnpm-lock.yaml"))) {
15346
15608
  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" };
15347
15609
  }
15348
- const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
15349
- if (installed.exitCode !== 0) {
15350
- return { ok: false, step: "install", message: `pnpm install \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B: ${tail(installed.stderr || installed.stdout)}` };
15610
+ const installed2 = await run("pnpm", ["install", "--frozen-lockfile"], repo);
15611
+ if (installed2.exitCode !== 0) {
15612
+ return { ok: false, step: "install", message: `pnpm install \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B: ${tail(installed2.stderr || installed2.stdout)}` };
15351
15613
  }
15352
15614
  await io.onStep("build", "\u0441\u043E\u0431\u0438\u0440\u0430\u044E \u043F\u0430\u043A\u0435\u0442\u044B \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430");
15353
15615
  const built = await run("pnpm", ["--filter", `${DAEMON_PACKAGE}...`, "build"], repo);
@@ -15970,9 +16232,24 @@ var executorHasSession = (p) => EXECUTOR.session.exists(p);
15970
16232
  var OVERMIND_ENTRY = THIS_FILE;
15971
16233
  var STALL_MS = Number(process.env.SWARM_STALL_MS) || 10 * 60 * 1e3;
15972
16234
  var STALL_FLOOR_MS = Number(process.env.SWARM_STALL_FLOOR_MS) || 30 * 60 * 1e3;
16235
+ var DEAD_HOLDER_SILENCE_MS = Number(process.env.SWARM_DEAD_HOLDER_SILENCE_MS) || 3 * 60 * 60 * 1e3;
15973
16236
  var RESPAWN_LIMIT = Number(process.env.SWARM_RESPAWN_LIMIT) || 2;
15974
16237
  var RESPAWN_YOUNG_MS = Number(process.env.SWARM_RESPAWN_YOUNG_MS) || 60 * 1e3;
15975
- var EXITED_NO_DONE_REASON = "\u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441\u0430\u043C\u0430, done() \u043D\u0435 \u0432\u044B\u0437\u0432\u0430\u043D \u2014 worktree \u0446\u0435\u043B, \u0440\u0430\u0431\u043E\u0442\u0430 \u0441\u043A\u043E\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043E \u0437\u0430\u043F\u0443\u0448\u0435\u043D\u0430";
16238
+ var EXITED_NO_DONE_REASON = "\u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441\u0430\u043C\u0430, done() \u043D\u0435 \u0432\u044B\u0437\u0432\u0430\u043D";
16239
+ var ABORTED_STOP_REASON = "\u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C, done() \u043D\u0435 \u0432\u044B\u0437\u0432\u0430\u043D";
16240
+ var EXIT_WORK_PUSHED = "\u0432\u0435\u0442\u043A\u0430 \u0435\u0441\u0442\u044C \u043D\u0430 origin \u2014 \u0435\u0441\u0442\u044C \u0447\u0442\u043E \u0441\u043F\u0430\u0441\u0430\u0442\u044C, PR \u043D\u0435 \u043E\u0442\u043A\u0440\u044B\u0442";
16241
+ var EXIT_WORK_LOST = "\u0432\u0435\u0442\u043A\u0438 \u043D\u0430 origin \u043D\u0435\u0442 \u2014 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E";
16242
+ var EXIT_WORK_UNKNOWN = "\u0441\u043F\u0440\u043E\u0441\u0438\u0442\u044C origin \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u2014 \u0446\u0435\u043B\u0430 \u043B\u0438 \u0440\u0430\u0431\u043E\u0442\u0430, \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E";
16243
+ var ABORT_TEXT = {
16244
+ tools: "\u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C \u043D\u0430 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0435 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u0430 \u2014 \u043E\u0431\u044B\u0447\u043D\u043E \u044D\u0442\u043E \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0441 \u0433\u0438\u0433\u0430\u043D\u0442\u0441\u043A\u0438\u043C \u0432\u044B\u0432\u043E\u0434\u043E\u043C (\u043F\u043E\u043B\u043D\u044B\u0439 \u0441\u044C\u044E\u0442 \u0442\u0435\u0441\u0442\u043E\u0432, \u043B\u043E\u0433 \u0431\u0435\u0437 \u043B\u0438\u043C\u0438\u0442\u0430): \u043E\u0442\u0432\u0435\u0442 \u0442\u0443\u043B\u0430 \u043D\u0435 \u0432\u043B\u0435\u0437\u0430\u0435\u0442 \u0438 \u043F\u0440\u0438\u0445\u043E\u0434\u0438\u0442 \u043F\u0443\u0441\u0442\u044B\u043C",
16245
+ link: "\u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C \u043D\u0430 \u0441\u0432\u044F\u0437\u0438 \u0441 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u043E\u043C (\u0441\u0442\u0440\u0438\u043C/\u043E\u0448\u0438\u0431\u043A\u0430 API) \u2014 \u043B\u0435\u0447\u0438\u0442\u0441\u044F \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C",
16246
+ other: "\u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C \u0441 \u043E\u0448\u0438\u0431\u043A\u043E\u0439 \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044F"
16247
+ };
16248
+ function abortReason(abort) {
16249
+ if (!abort) return "\u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C";
16250
+ return `${ABORT_TEXT[abort.kind]} (${abort.raw})`;
16251
+ }
16252
+ var ABORTED_CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u043E\u0448\u043B\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u041E\u0411\u041E\u0420\u0412\u0410\u041B\u0410\u0421\u042C \u043D\u0430 \u043F\u043E\u043B\u0443\u0441\u043B\u043E\u0432\u0435 (\u043D\u0435 \u0442\u044B \u0435\u0451 \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B). \u0422\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`: \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E \u0438 \u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043E \u2014 \u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439. \u0412\u0410\u0416\u041D\u041E \u043F\u0440\u043E \u043E\u0431\u0440\u044B\u0432\u044B: \u0440\u0432\u0451\u0442\u0441\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u043D\u0430 \u041E\u0422\u0412\u0415\u0422\u0415 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u0430, \u0447\u0435\u0439 \u0432\u044B\u0432\u043E\u0434 \u043E\u0433\u0440\u043E\u043C\u0435\u043D (\u043F\u043E\u043B\u043D\u044B\u0439 \u0441\u044C\u044E\u0442 \u0442\u0435\u0441\u0442\u043E\u0432, `git log` \u0431\u0435\u0437 \u043B\u0438\u043C\u0438\u0442\u0430, \u0434\u0430\u043C\u043F \u0444\u0430\u0439\u043B\u0430 \u0446\u0435\u043B\u0438\u043A\u043E\u043C). \u0413\u043D\u0430\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043D\u0430\u0434\u043E \u043F\u043E-\u043F\u0440\u0435\u0436\u043D\u0435\u043C\u0443 \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E \u2014 \u043D\u043E \u0432\u044B\u0432\u043E\u0434 \u043F\u0438\u0448\u0438 \u0432 \u0441\u0432\u043E\u0439 \u0444\u0430\u0439\u043B (`LOG=$(mktemp -t suite)`) \u0438 \u0431\u0435\u0440\u0438 \u0438\u0437 \u043D\u0435\u0433\u043E \u0445\u0432\u043E\u0441\u0442 \u0438 \u0438\u0442\u043E\u0433\u043E\u0432\u0443\u044E \u0441\u0442\u0440\u043E\u043A\u0443, \u0430 \u043D\u0435 \u043F\u0440\u043E\u0441\u0442\u044B\u043D\u044E. \u0414\u043E\u0432\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u043E \u043A\u043E\u043D\u0446\u0430, \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0438 \u041E\u0411\u042F\u0417\u0410\u0422\u0415\u041B\u042C\u041D\u041E \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url).";
15976
16253
  var EXITED_CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u043E\u0448\u043B\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0421\u0410\u041C\u0410, \u043D\u043E \u0442\u044B \u0442\u0430\u043A \u0438 \u043D\u0435 \u0432\u044B\u0437\u0432\u0430\u043B done(pr_url) \u2014 \u0440\u043E\u0439 \u043D\u0435 \u0443\u0432\u0438\u0434\u0435\u043B \u0441\u0434\u0430\u0447\u0438. \u0422\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`: \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E \u0438 \u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043E \u2014 \u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439. \u0414\u043E\u0432\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u043E \u043A\u043E\u043D\u0446\u0430, \u043E\u0442\u043A\u0440\u043E\u0439 PR (\u0435\u0441\u043B\u0438 \u0435\u0449\u0451 \u043D\u0435\u0442) \u0438 \u041E\u0411\u042F\u0417\u0410\u0422\u0415\u041B\u042C\u041D\u041E \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url) \u2014 \u0431\u0435\u0437 \u044D\u0442\u043E\u0433\u043E \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F \u0441\u0434\u0430\u043D\u043D\u043E\u0439. \u0423\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u0432 \u043B\u0438\u043C\u0438\u0442/\u043D\u0435 \u043C\u043E\u0436\u0435\u0448\u044C \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u0442\u044C \u2014 \u043D\u0435 \u043C\u043E\u043B\u043E\u0442\u0438 \u0432\u0441\u043B\u0435\u043F\u0443\u044E, \u0437\u0430\u0434\u0430\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u0447\u0435\u0440\u0435\u0437 MCP-\u0442\u0443\u043B ask. \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439.";
15977
16254
  var STORM_DEAD_LIMIT = Number(process.env.SWARM_STORM_DEAD_LIMIT) || 3;
15978
16255
  var CONFLICT_RESOLVE_COOLDOWN_MS = Number(process.env.SWARM_CONFLICT_COOLDOWN_MS) || 3 * 60 * 1e3;
@@ -16013,7 +16290,7 @@ var CHANGES_WAKE_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430
16013
16290
  var CHANGES_WAKE_ESCALATION_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u044F \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0430\u0441\u044C";
16014
16291
  var CHANGES_WAKE_FAILED_MARK = "\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C";
16015
16292
  var BUDGET_STOP_REASON = "\u043D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u0441\u044F \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442: \u0441\u0435\u0441\u0441\u0438\u044E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B \u0431\u044E\u0434\u0436\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430";
16016
- var NO_AUTO_WAKE_REASONS = [EXITED_NO_DONE_REASON, BUDGET_STOP_REASON];
16293
+ var NO_AUTO_WAKE_REASONS = [EXITED_NO_DONE_REASON, BUDGET_STOP_REASON, ABORTED_STOP_REASON];
16017
16294
  var REVIEW_BUDGET_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u043E\u0441\u044C \u0432 \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u0442\u0440\u0430\u0442";
16018
16295
  function reviewBudgetMark(cycle) {
16019
16296
  return `${REVIEW_BUDGET_MARK} (\u043A\u0440\u0443\u0433 ${cycle})`;
@@ -16806,6 +17083,11 @@ function droneLabel(run) {
16806
17083
  function signalTitle(run, what) {
16807
17084
  return `${run.task_slug || droneLabel(run)}: ${what}`;
16808
17085
  }
17086
+ var tickNoise = makeRepeatFilter();
17087
+ function warnRepeating(key, message, now) {
17088
+ const line = tickNoise.say(key, message, now);
17089
+ if (line) console.error(line);
17090
+ }
16809
17091
  function narrate(deps, text) {
16810
17092
  try {
16811
17093
  deps.narrate?.(text);
@@ -17577,14 +17859,17 @@ async function trySpawnQueuedRun(deps, run) {
17577
17859
  return await failBeforeStart(deps, run, String(e));
17578
17860
  }
17579
17861
  }
17862
+ function holderStillHolds(deps, holder, now) {
17863
+ if (holder.pid != null) return deps.isAlive(holder.pid);
17864
+ return now - lastActivityMs(deps.db, holder) < DEAD_HOLDER_SILENCE_MS;
17865
+ }
17580
17866
  async function healBusyWorktree(deps, run, e) {
17581
17867
  const path = e.worktreePath;
17582
17868
  if (!deps.releaseWorktree) return false;
17583
17869
  if (deps.liveWorktrees?.().has(path)) return false;
17870
+ const now = deps.now ? deps.now() : Date.now();
17584
17871
  const holder = deps.db.listRuns().find((r) => r.id !== run.id && r.worktree_path === path && !isTerminalStatus(r.status));
17585
- if (holder && (holder.pid == null || deps.isAlive(holder.pid))) {
17586
- return false;
17587
- }
17872
+ if (holder && holderStillHolds(deps, holder, now)) return false;
17588
17873
  if (deps.isDirty && await deps.isDirty(path).catch(() => true)) {
17589
17874
  deps.db.addEvent(
17590
17875
  run.id,
@@ -17594,10 +17879,12 @@ async function healBusyWorktree(deps, run, e) {
17594
17879
  narrate(deps, `${run.task_slug}: worktree \u0437\u0430\u043D\u044F\u0442 \u043C\u0451\u0440\u0442\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439, \u043D\u043E \u0432 \u043D\u0451\u043C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u2014 \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`);
17595
17880
  return false;
17596
17881
  }
17882
+ const silence = holder && holder.pid == null ? `\u043C\u043E\u043B\u0447\u0430\u043B ${humanMinutes(now - lastActivityMs(deps.db, holder))}` : null;
17883
+ const why = silence ? `\u0434\u0435\u0440\u0436\u0430\u0442\u0435\u043B\u044C #${holder.id} (${holder.status}) ${silence} \u0438 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430 \u043D\u0435 \u0438\u043C\u0435\u0435\u0442 \u2014 \u043D\u0435 \u0436\u0434\u0451\u0442 \u043C\u0435\u0440\u0436\u0430, \u0430 \u0443\u043C\u0435\u0440` : "\u0436\u0438\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0437\u0430 \u043D\u0438\u043C \u043D\u0435\u0442";
17597
17884
  await deps.releaseWorktree(run.repo, path).catch(() => {
17598
17885
  });
17599
- deps.db.addEvent(run.id, "step", `worktree ${path} \u0434\u0435\u0440\u0436\u0430\u043B\u0430 \u043C\u0451\u0440\u0442\u0432\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u2014 \u043F\u0440\u0438\u0431\u0440\u0430\u043B \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E \u0441\u043F\u0430\u0432\u043D (TASWM-476)`);
17600
- if (holder) deps.db.addEvent(holder.id, "step", `worktree ${path} \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0451\u043D: \u0436\u0438\u0432\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438 \u0437\u0430 \u043D\u0438\u043C \u043D\u0435\u0442, \u0441\u043B\u0430\u0433 \u043F\u043E\u043D\u0430\u0434\u043E\u0431\u0438\u043B\u0441\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0443 #${run.id}`);
17886
+ deps.db.addEvent(run.id, "step", `worktree ${path} \u0434\u0435\u0440\u0436\u0430\u043B\u0430 \u043C\u0451\u0440\u0442\u0432\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F (${why}) \u2014 \u043F\u0440\u0438\u0431\u0440\u0430\u043B \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u044E \u0441\u043F\u0430\u0432\u043D (TASWM-476)`);
17887
+ if (holder) deps.db.addEvent(holder.id, "step", `worktree ${path} \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0451\u043D: ${why}, \u0441\u043B\u0430\u0433 \u043F\u043E\u043D\u0430\u0434\u043E\u0431\u0438\u043B\u0441\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0443 #${run.id}`);
17601
17888
  return true;
17602
17889
  }
17603
17890
  async function failBeforeStart(deps, run, reason) {
@@ -17882,7 +18169,7 @@ async function buildReliefCostCandidatesFor(deps, now, droneCap) {
17882
18169
  if (deps.exclusiveProject && await deps.exclusiveProject(repo)) continue;
17883
18170
  } catch {
17884
18171
  }
17885
- exclusiveRepos.push(basename4(repo));
18172
+ exclusiveRepos.push(basename6(repo));
17886
18173
  }
17887
18174
  const otherHosts = [
17888
18175
  ...new Set(
@@ -17908,6 +18195,15 @@ async function buildReliefCostCandidatesFor(deps, now, droneCap) {
17908
18195
  ...hrefOf(worstRun?.task_url)
17909
18196
  }));
17910
18197
  }
18198
+ async function workFate(deps, run) {
18199
+ if (run.pr_url) return `PR \u043E\u0442\u043A\u0440\u044B\u0442: ${run.pr_url}`;
18200
+ if (!deps.branchOnOrigin) return "";
18201
+ const branch = run.branch || run.task_slug;
18202
+ const exists = await deps.branchOnOrigin(run.repo, branch).catch(() => null);
18203
+ if (exists === true) return `${EXIT_WORK_PUSHED} (\u0432\u0435\u0442\u043A\u0430 ${branch})`;
18204
+ if (exists === false) return `${EXIT_WORK_LOST} (\u0432\u0435\u0442\u043A\u0438 ${branch} \u043D\u0430 origin \u043D\u0435\u0442)`;
18205
+ return EXIT_WORK_UNKNOWN;
18206
+ }
17911
18207
  async function deliveredPrUrl(deps, run) {
17912
18208
  if (run.pr_url) return run.pr_url;
17913
18209
  if (!deps.openPrsForBranch) return null;
@@ -18299,7 +18595,11 @@ async function tickOnce(deps) {
18299
18595
  deps.db.addEvent(run.id, "step", `${SELF_REFERENCE_MARK}: PR \u0442\u0440\u043E\u0433\u0430\u0435\u0442 ${DAEMON_CODE_PATHS.join(", ")} \u2014 \u044D\u0441\u043A\u0430\u043B\u0438\u0440\u043E\u0432\u0430\u043D\u043E \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0443, \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E`);
18300
18596
  narrate(deps, `PR ${droneLabel(run)} \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043A\u043E\u0434 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0441\u0430\u043C \u0441\u0435\u0431\u044F \u043D\u0435 \u0430\u0442\u0442\u0435\u0441\u0442\u0443\u0435\u0442, \u044D\u0441\u043A\u0430\u043B\u0438\u0440\u0443\u044E \u0438 \u043D\u0435 \u0433\u043E\u043D\u044F\u044E \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E.`);
18301
18597
  } catch (e) {
18302
- console.error(`[overmind] \u0441\u0442\u043E\u0440\u043E\u0436: self-reference \u043D\u043E\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \xAB${signal.key}\xBB \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u0430, \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:`, e);
18598
+ warnRepeating(
18599
+ `self-reference:${signal.key}`,
18600
+ `[overmind] \u0441\u0442\u043E\u0440\u043E\u0436: self-reference \u043D\u043E\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \xAB${signal.key}\xBB \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u0430, \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C: ${describeError(e)}`,
18601
+ now
18602
+ );
18303
18603
  }
18304
18604
  }
18305
18605
  continue;
@@ -18505,7 +18805,10 @@ async function tickOnce(deps) {
18505
18805
  continue;
18506
18806
  }
18507
18807
  }
18508
- if (!alive && outcome === "graceful" && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && run.verifies_run_id == null && run.kind !== "hero") {
18808
+ if (!alive && (outcome === "graceful" || outcome === "aborted") && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && run.verifies_run_id == null && run.kind !== "hero") {
18809
+ const aborted = wasAborted(outcome);
18810
+ const abort = aborted ? deps.exitAbort?.(run) ?? null : null;
18811
+ const what = aborted ? abortReason(abort) : EXITED_NO_DONE_REASON;
18509
18812
  const wt = run.worktree_path;
18510
18813
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
18511
18814
  if (deps.respawn && canContinue && run.respawns < RESPAWN_LIMIT) {
@@ -18513,21 +18816,31 @@ async function tickOnce(deps) {
18513
18816
  deps.db.addEvent(
18514
18817
  run.id,
18515
18818
  "respawned",
18516
- `\u0430\u0432\u0442\u043E-\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u0435 #${n}: \u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441\u0430\u043C\u0430 \u0431\u0435\u0437 done() \u2014 --continue, \u0434\u043E\u0432\u043E\u0436\u0443 \u0434\u043E \u0441\u0434\u0430\u0447\u0438 (\u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u044E \u0441\u0434\u0435\u043B\u0430\u043D\u043D\u043E\u0435)`
18819
+ aborted ? `\u0430\u0432\u0442\u043E-\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u0435 #${n}: ${what} \u2014 --continue \u0441 \u043D\u0430\u043A\u0430\u0437\u043E\u043C \u043D\u0435 \u043F\u043E\u0432\u0442\u043E\u0440\u044F\u0442\u044C \u0442\u044F\u0436\u0451\u043B\u0443\u044E \u043A\u043E\u043C\u0430\u043D\u0434\u0443` : `\u0430\u0432\u0442\u043E-\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u0435 #${n}: \u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430\u0441\u044C \u0441\u0430\u043C\u0430 \u0431\u0435\u0437 done() \u2014 --continue, \u0434\u043E\u0432\u043E\u0436\u0443 \u0434\u043E \u0441\u0434\u0430\u0447\u0438 (\u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u044E \u0441\u0434\u0435\u043B\u0430\u043D\u043D\u043E\u0435)`
18820
+ );
18821
+ narrate(
18822
+ deps,
18823
+ aborted ? `${droneLabel(run)}: ${what}. \u041F\u043E\u0434\u043D\u0438\u043C\u0430\u044E --continue.` : `${droneLabel(run)} \u0432\u044B\u0448\u0435\u043B \u0441\u0430\u043C, \u043D\u0435 \u043F\u043E\u0437\u0432\u0430\u0432 done() \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E --continue, \u0447\u0442\u043E\u0431\u044B \u0434\u043E\u0432\u0451\u043B \u0434\u043E PR/done.`
18517
18824
  );
18518
- narrate(deps, `${droneLabel(run)} \u0432\u044B\u0448\u0435\u043B \u0441\u0430\u043C, \u043D\u0435 \u043F\u043E\u0437\u0432\u0430\u0432 done() \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E --continue, \u0447\u0442\u043E\u0431\u044B \u0434\u043E\u0432\u0451\u043B \u0434\u043E PR/done.`);
18519
18825
  try {
18520
- await deps.respawn(run, "continue", EXITED_CONTINUE_NUDGE);
18826
+ await deps.respawn(run, "continue", aborted ? ABORTED_CONTINUE_NUDGE : EXITED_CONTINUE_NUDGE);
18521
18827
  } catch (e) {
18522
- deps.db.updateRun(run.id, { status: "paused", stop_reason: EXITED_NO_DONE_REASON });
18523
- deps.db.addEvent(run.id, "paused", `${EXITED_NO_DONE_REASON}; \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u0435 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C: ${String(e)}`);
18828
+ const failed = `${aborted ? ABORTED_STOP_REASON : EXITED_NO_DONE_REASON}; \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u0435 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C: ${String(e)}`;
18829
+ deps.db.updateRun(run.id, { status: "paused", stop_reason: failed });
18830
+ deps.db.addEvent(run.id, "paused", failed);
18524
18831
  }
18525
18832
  continue;
18526
18833
  }
18527
- const reason2 = run.respawns >= RESPAWN_LIMIT ? `${EXITED_NO_DONE_REASON} \u2014 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u043B\u0438\u043C\u0438\u0442 \u0430\u0432\u0442\u043E-\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u0439 (${RESPAWN_LIMIT}), \u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440` : EXITED_NO_DONE_REASON;
18834
+ const marker = aborted ? ABORTED_STOP_REASON : EXITED_NO_DONE_REASON;
18835
+ const head = run.respawns >= RESPAWN_LIMIT ? `${marker} \u2014 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u043B\u0438\u043C\u0438\u0442 \u0430\u0432\u0442\u043E-\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u0439 (${RESPAWN_LIMIT}), \u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440` : marker;
18836
+ const fate = await workFate(deps, run);
18837
+ const reason2 = [head, aborted ? what : null, fate].filter(Boolean).join("; ");
18528
18838
  deps.db.updateRun(run.id, { status: "paused", stop_reason: reason2 });
18529
18839
  deps.db.addEvent(run.id, "paused", reason2);
18530
- narrate(deps, `${droneLabel(run)} \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B \u0441\u0435\u0441\u0441\u0438\u044E \u0441\u0430\u043C, \u043D\u0435 \u043F\u043E\u0437\u0432\u0430\u0432 done() \u2014 \u0441\u0442\u0430\u0442\u0443\u0441 \xAB\u0437\u0430\u043C\u0435\u0440\xBB, \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u0432 \u043B\u043E\u0433\u0435 (TASWM-229).`);
18840
+ narrate(
18841
+ deps,
18842
+ aborted ? `${droneLabel(run)}: ${what}. \u041F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u043C \u2014 \xAB\u0437\u0430\u043C\u0435\u0440\xBB, ${fate || "\u0441\u0443\u0434\u044C\u0431\u0430 \u0440\u0430\u0431\u043E\u0442\u044B \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430"}.` : `${droneLabel(run)} \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B \u0441\u0435\u0441\u0441\u0438\u044E \u0441\u0430\u043C, \u043D\u0435 \u043F\u043E\u0437\u0432\u0430\u0432 done() \u2014 \u0441\u0442\u0430\u0442\u0443\u0441 \xAB\u0437\u0430\u043C\u0435\u0440\xBB, \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u0432 \u043B\u043E\u0433\u0435 (TASWM-229).`
18843
+ );
18531
18844
  continue;
18532
18845
  }
18533
18846
  if (!alive && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && run.verifies_run_id == null && deps.respawn && run.respawns < RESPAWN_LIMIT) {
@@ -18703,8 +19016,13 @@ async function tickOnce(deps) {
18703
19016
  const signal = finalCall ? { ...item.signal, body: `${item.signal.body} ${LAST_REMINDER_SUFFIX}` } : item.signal;
18704
19017
  try {
18705
19018
  await az.notify(signal);
19019
+ tickNoise.forget(`notify:${signal.key}`);
18706
19020
  } catch (e) {
18707
- console.error(`[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u043D\u043E\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \xAB${signal.key}\xBB \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u0430, \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:`, e);
19021
+ warnRepeating(
19022
+ `notify:${signal.key}`,
19023
+ `[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u043D\u043E\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \xAB${signal.key}\xBB \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u0430, \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C: ${describeError(e)}`,
19024
+ now
19025
+ );
18708
19026
  continue;
18709
19027
  }
18710
19028
  for (const key of item.keys) az.tracker.commit(key, now);
@@ -18726,8 +19044,13 @@ async function tickOnce(deps) {
18726
19044
  try {
18727
19045
  await az.resolve(batch);
18728
19046
  az.tracker.forgetResolved(batch);
19047
+ tickNoise.forget("anomaly-resolve");
18729
19048
  } catch (e) {
18730
- console.error("[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0433\u0430\u0448\u0435\u043D\u0438\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0432\u0448\u0438\u0445\u0441\u044F \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u0439 \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u043E, \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:", e);
19049
+ warnRepeating(
19050
+ "anomaly-resolve",
19051
+ `[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0433\u0430\u0448\u0435\u043D\u0438\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0432\u0448\u0438\u0445\u0441\u044F \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u0439 \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u043E, \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C: ${describeError(e)}`,
19052
+ now
19053
+ );
18731
19054
  }
18732
19055
  }
18733
19056
  }
@@ -18817,7 +19140,7 @@ async function resolveConflict(deps, reviewRun) {
18817
19140
  model: reviewRun.model,
18818
19141
  review_model: reviewRun.review_model
18819
19142
  });
18820
- const wt = join30(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
19143
+ const wt = join30(deps.worktreesDir, basename6(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
18821
19144
  try {
18822
19145
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
18823
19146
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -18874,7 +19197,7 @@ async function spawnReviewer(deps, reviewRun, files) {
18874
19197
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
18875
19198
  workspace_id: reviewRun.workspace_id
18876
19199
  });
18877
- const wt = join30(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
19200
+ const wt = join30(deps.worktreesDir, basename6(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
18878
19201
  try {
18879
19202
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
18880
19203
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -18950,7 +19273,7 @@ async function spawnVerifier(deps, verifiedRun, files) {
18950
19273
  // штамп цикла: фикс-итерацию проверяет свежий верификатор
18951
19274
  workspace_id: verifiedRun.workspace_id
18952
19275
  });
18953
- const wt = join30(deps.worktreesDir, basename4(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
19276
+ const wt = join30(deps.worktreesDir, basename6(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
18954
19277
  try {
18955
19278
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
18956
19279
  await deps.git.setWorktreeIdentity(wt, verifier.name);
@@ -19200,6 +19523,8 @@ async function reportWorkspaceAccess(tl, db) {
19200
19523
  }
19201
19524
  async function runDaemon(opts) {
19202
19525
  installCrashGuards();
19526
+ installLogStamps();
19527
+ console.log(localZoneLine(/* @__PURE__ */ new Date()));
19203
19528
  const daemonStart = captureDaemonStart();
19204
19529
  const latestRelease = fetchLatestVersion();
19205
19530
  const worktreesDir = await migrateWorktreesAtStartup();
@@ -19780,6 +20105,11 @@ async function runDaemon(opts) {
19780
20105
  // Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
19781
20106
  // (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
19782
20107
  exitOutcome: makeExitClassifier(RUN_LOGS_DIR, (runId) => db.runLogKeys(runId), EXECUTOR),
20108
+ // TASWM-487: вид обрыва — из того же result-файла и теми же ключами каталога.
20109
+ exitAbort: makeAbortReader(RUN_LOGS_DIR, (runId) => db.runLogKeys(runId), EXECUTOR),
20110
+ // TASWM-487: жива ли работа на origin. Тот же фолбэк repo, что у removeWorktree выше:
20111
+ // у прогона в основном репозитории демона run.repo пуст.
20112
+ branchOnOrigin: (repo, branch) => remoteBranchExists(repo || opts.repo, branch),
19783
20113
  // Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
19784
20114
  livePpids,
19785
20115
  // Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
@@ -19811,7 +20141,7 @@ async function runDaemon(opts) {
19811
20141
  worktreesDir,
19812
20142
  activeWorktreePaths: new Set(runsWithWorktree.filter((r) => !isTerminalStatus(r.status)).map((r) => r.worktree_path)),
19813
20143
  containerRunPrefixes: runsWithWorktree.map((r) => ({
19814
- prefix: composeProjectPrefix(basename4(r.worktree_path)),
20144
+ prefix: composeProjectPrefix(basename6(r.worktree_path)),
19815
20145
  active: !isTerminalStatus(r.status)
19816
20146
  }))
19817
20147
  }).then(() => {
@@ -20381,6 +20711,9 @@ if (isEntrypoint) {
20381
20711
  }
20382
20712
  }
20383
20713
  export {
20714
+ ABORTED_CONTINUE_NUDGE,
20715
+ ABORTED_STOP_REASON,
20716
+ ABORT_TEXT,
20384
20717
  AWAITING_CI_REASON,
20385
20718
  BUDGET_STOP_REASON,
20386
20719
  CHANGES_FRESH_WAKE_AFTER_MS,
@@ -20405,8 +20738,12 @@ export {
20405
20738
  CI_WAKE_LIMIT,
20406
20739
  CONFLICT_RESOLVE_COOLDOWN_MS,
20407
20740
  CONTINUE_NUDGE,
20741
+ DEAD_HOLDER_SILENCE_MS,
20408
20742
  EXITED_CONTINUE_NUDGE,
20409
20743
  EXITED_NO_DONE_REASON,
20744
+ EXIT_WORK_LOST,
20745
+ EXIT_WORK_PUSHED,
20746
+ EXIT_WORK_UNKNOWN,
20410
20747
  GATE_LOG_HEARTBEAT_TICKS,
20411
20748
  HEADLESS_BLOCK,
20412
20749
  HERO_CONTINUE_NUDGE,
@@ -20429,6 +20766,7 @@ export {
20429
20766
  STORM_DEAD_LIMIT,
20430
20767
  VERIFY_SPAWN_LIMIT,
20431
20768
  WATCHDOG_PARK_REASON,
20769
+ abortReason,
20432
20770
  buildChangesWakeNudge,
20433
20771
  buildCiReadyNudge,
20434
20772
  buildCiWakeNudge,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.21",
3
+ "version": "0.6.23",
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": [
@@ -38,7 +38,7 @@
38
38
  "execa": "^10.0.0",
39
39
  "zod": "3.25.76"
40
40
  },
41
- "gitHead": "584411cbe69583a8727e89caa8ae7e626f8fd8ca",
41
+ "gitHead": "af6c4af0f523891b5db805d9b4583021688ed1f5",
42
42
  "scripts": {
43
43
  "overmind": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/overmind.ts",
44
44
  "swarm": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/cli.ts",
@@ -46,6 +46,39 @@ CONTRIBUTING): КАКИМИ командами это зелёное получ
46
46
  разрешение молча сдать: скажи прямо, чем проверял вместо них. Инфраструктуру
47
47
  проекта под гейт роя НЕ достраивай.
48
48
 
49
+ ## Тяжёлый прогон — в файл, а не в контекст
50
+
51
+ Вывод на тысячи строк не влезает в один результат инструмента, приходит пустым — и
52
+ сессия рвётся на полуслове, посреди твоей работы. В run-логах роя за июль–август это
53
+ **215 обрывов из 1104 выходов**: каждый пятый дрон умирает не от бага, а от
54
+ собственного `pnpm test`.
55
+
56
+ Порядок такой:
57
+
58
+ 1. **Сначала спроси, надо ли гнать сьют целиком здесь.** Если рой работает в режиме
59
+ `verifyMode=ci`, общий прогон уже делает CI на этом же коммите — дублировать его на
60
+ машине владельца не надо (тебе про это скажут в задании). Твоё дело тогда — прицельно
61
+ те файлы, которых касается правка.
62
+ 2. **Гонишь тяжёлое — пиши в файл, в контекст бери хвост.**
63
+
64
+ ```bash
65
+ LOG=$(mktemp -t suite) # свой файл: на машине не ты один
66
+ pnpm test > "$LOG" 2>&1; echo "exit=$?" # код — от ТЕСТОВ, не от tail
67
+ tail -40 "$LOG" # итог: сколько файлов, сколько тестов
68
+ grep -nE "FAIL|✗|✘|●" "$LOG" | head -40 # упавшие — точечно
69
+ ```
70
+
71
+ - Код возврата бери у самой команды (`echo "exit=$?"` сразу после неё). `cmd | tail`
72
+ всегда вернёт 0 — это код `tail`, и красный сьют так маскируется.
73
+ - Свой файл на прогон (`mktemp`), не общий `/tmp/suite.log`: дронов на машине несколько,
74
+ и они затрут лог друг другу — а ты прочитаешь чужой результат как свой.
75
+ - Доказательство в сдачу — итоговые строки из файла (`Test Files … Tests …`, `exit=0`),
76
+ а не простыня целиком. Полнота доказательства — в цифрах итога, не в объёме.
77
+ - То же касается `git log`, `git diff` без пути, дампа большого файла, `docker build`:
78
+ всё, что печатает бесконечно, идёт через файл.
79
+ - Это не «частичный прогон» из п. 2: прогон полный, обрезан только вывод. Экстраполяция
80
+ запрещена по-прежнему — итог читай, а не первые строки.
81
+
49
82
  ## Red flags — stop and go get evidence
50
83
 
51
84
  - «should», «probably», «seems to», «кажется, работает»