@taskless-app/swarm 0.6.21 → 0.6.22

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
@@ -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.22",
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: "b8eaac4a5562ffcf45a02af338267620e0a7843f"
2427
2447
  };
2428
2448
  }
2429
2449
  });
package/dist/overmind.js CHANGED
@@ -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: {
@@ -2888,11 +2908,11 @@ var init_heartbeat = __esm({
2888
2908
  });
2889
2909
 
2890
2910
  // src/agentToken.ts
2891
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
2892
- import { join as join9 } from "path";
2911
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3 } from "fs";
2912
+ import { join as join8 } from "path";
2893
2913
  function loadAgentToken() {
2894
2914
  try {
2895
- const s = JSON.parse(readFileSync7(STORE_PATH, "utf8"));
2915
+ const s = JSON.parse(readFileSync6(STORE_PATH, "utf8"));
2896
2916
  return s.token ? s : null;
2897
2917
  } catch {
2898
2918
  return null;
@@ -2945,7 +2965,7 @@ var init_agentToken = __esm({
2945
2965
  "src/agentToken.ts"() {
2946
2966
  "use strict";
2947
2967
  init_config();
2948
- STORE_PATH = join9(SWARM_HOME, "agent.json");
2968
+ STORE_PATH = join8(SWARM_HOME, "agent.json");
2949
2969
  base = (api) => api.replace(/\/$/, "");
2950
2970
  TOKEN_PROBE_TIMEOUT_MS = 8e3;
2951
2971
  }
@@ -3533,7 +3553,7 @@ async function remoteMainRef(repo) {
3533
3553
  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
3554
  }
3535
3555
  async function remoteBranchExists(repo, branch) {
3536
- const res = await $2({ reject: false })`git -C ${repo} ls-remote --heads origin ${`refs/heads/${branch}`}`;
3556
+ const res = await $2({ reject: false, timeout: 5e3 })`git -C ${repo} ls-remote --heads origin ${`refs/heads/${branch}`}`;
3537
3557
  if (res.exitCode !== 0) return null;
3538
3558
  return res.stdout.trim().length > 0;
3539
3559
  }
@@ -8881,6 +8901,80 @@ function rotateDaemonLog(path, opts, io = realIo) {
8881
8901
  return "rotated";
8882
8902
  }
8883
8903
 
8904
+ // src/logStamp.ts
8905
+ import { format } from "util";
8906
+ function logStamp(at) {
8907
+ return at.toISOString();
8908
+ }
8909
+ function stampMessage(text, at) {
8910
+ const ts = logStamp(at);
8911
+ return text.split("\n").map((line) => line.length ? `${ts} ${line}` : ts).join("\n");
8912
+ }
8913
+ function utcOffsetText(at) {
8914
+ const minutes = -at.getTimezoneOffset();
8915
+ const sign2 = minutes < 0 ? "-" : "+";
8916
+ const abs = Math.abs(minutes);
8917
+ const pad = (n) => String(n).padStart(2, "0");
8918
+ return `UTC${sign2}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
8919
+ }
8920
+ function localZoneLine(at, zone) {
8921
+ const tz = zone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
8922
+ 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})` : ""}`;
8923
+ }
8924
+ var LEVELS = ["log", "info", "warn", "error"];
8925
+ var installed = false;
8926
+ function installLogStamps(now = () => /* @__PURE__ */ new Date()) {
8927
+ if (installed) return () => {
8928
+ };
8929
+ installed = true;
8930
+ const original = LEVELS.map((level) => [level, console[level]]);
8931
+ for (const [level, orig] of original) {
8932
+ const write = orig.bind(console);
8933
+ console[level] = (...args) => {
8934
+ write(stampMessage(format(...args), now()));
8935
+ };
8936
+ }
8937
+ return () => {
8938
+ for (const [level, orig] of original) console[level] = orig;
8939
+ installed = false;
8940
+ };
8941
+ }
8942
+
8943
+ // src/logNoise.ts
8944
+ var DEFAULT_REMIND_MS = 10 * 6e4;
8945
+ function timesWord(n) {
8946
+ const tens = n % 100;
8947
+ if (tens >= 11 && tens <= 14) return "\u0440\u0430\u0437";
8948
+ return n % 10 >= 2 && n % 10 <= 4 ? "\u0440\u0430\u0437\u0430" : "\u0440\u0430\u0437";
8949
+ }
8950
+ function makeRepeatFilter(opts = {}) {
8951
+ const remindAfterMs = opts.remindAfterMs ?? DEFAULT_REMIND_MS;
8952
+ const streaks = /* @__PURE__ */ new Map();
8953
+ return {
8954
+ say(key, message, now) {
8955
+ const prev = streaks.get(key);
8956
+ if (!prev || prev.message !== message) {
8957
+ 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)})` : "";
8958
+ streaks.set(key, { message, since: now, saidAt: now, hidden: 0 });
8959
+ return `${message}${tail2}`;
8960
+ }
8961
+ prev.hidden += 1;
8962
+ if (now - prev.saidAt < remindAfterMs) return null;
8963
+ const hidden = prev.hidden;
8964
+ const minutes = Math.max(1, Math.round((now - prev.since) / 6e4));
8965
+ prev.saidAt = now;
8966
+ prev.hidden = 0;
8967
+ 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`;
8968
+ },
8969
+ forget(key) {
8970
+ streaks.delete(key);
8971
+ }
8972
+ };
8973
+ }
8974
+
8975
+ // src/overmind.ts
8976
+ init_linkFailure();
8977
+
8884
8978
  // src/repoRegistry.ts
8885
8979
  init_config();
8886
8980
  import { $ } from "execa";
@@ -8919,13 +9013,13 @@ async function buildRepoRegistry(paths) {
8919
9013
  }
8920
9014
 
8921
9015
  // src/repoMemory.ts
8922
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
8923
- import { join as join8 } from "path";
9016
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
9017
+ import { join as join9 } from "path";
8924
9018
  var FILE = "repos.json";
8925
9019
  function loadRememberedRepos(home) {
8926
9020
  let raw;
8927
9021
  try {
8928
- raw = readFileSync6(join8(home, FILE), "utf8");
9022
+ raw = readFileSync7(join9(home, FILE), "utf8");
8929
9023
  } catch {
8930
9024
  return [];
8931
9025
  }
@@ -8940,7 +9034,7 @@ function loadRememberedRepos(home) {
8940
9034
  function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
8941
9035
  const clean = paths.filter((p) => p.trim().length > 0);
8942
9036
  if (clean.length === 0) return;
8943
- writeFileSync6(join8(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
9037
+ writeFileSync7(join9(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
8944
9038
  `);
8945
9039
  }
8946
9040
  function pickRepoPaths(args) {
@@ -9185,9 +9279,13 @@ async function refreshRepoMap(opts) {
9185
9279
  added++;
9186
9280
  }
9187
9281
  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
- );
9282
+ 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}`;
9283
+ if (added > 0 || opts.state.announcedScan !== outcome) {
9284
+ opts.state.announcedScan = outcome;
9285
+ log(
9286
+ `${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)"}`
9287
+ );
9288
+ }
9191
9289
  }
9192
9290
  const registry = [...clones.values()].map((c) => ({
9193
9291
  repoFullName: c.repoFullName,
@@ -9199,8 +9297,10 @@ async function refreshRepoMap(opts) {
9199
9297
  scannedAt
9200
9298
  );
9201
9299
  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)`);
9300
+ announceMap(opts.state, log, [
9301
+ describeReposFailure(answer.kind, answer.detail),
9302
+ `[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)`
9303
+ ]);
9204
9304
  return registry;
9205
9305
  }
9206
9306
  opts.state.unknown = false;
@@ -9209,18 +9309,39 @@ async function refreshRepoMap(opts) {
9209
9309
  opts.state.roots = [...opts.roots];
9210
9310
  opts.state.at = stamp();
9211
9311
  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");
9312
+ 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
9313
  return registry;
9214
9314
  }
9215
- log(`[overmind] ${describeMapSummary(opts.state, opts.roots)}`);
9216
- for (const c of opts.state.clones) log(`[overmind] ${describeClone(c)}`);
9217
9315
  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
- }
9316
+ announceMap(
9317
+ opts.state,
9318
+ log,
9319
+ [
9320
+ `[overmind] ${describeMapSummary(opts.state, opts.roots)}`,
9321
+ ...opts.state.clones.map((c) => `[overmind] ${describeClone(c)}`),
9322
+ ...opts.state.missing.map((name) => {
9323
+ const prefix = prefixOf.get(name);
9324
+ return `[overmind] ${describeMissing(prefix ? `${name} (\u043F\u0440\u043E\u0435\u043A\u0442 ${prefix})` : name, opts.roots)}`;
9325
+ })
9326
+ ],
9327
+ mapSignature(opts.state, opts.roots)
9328
+ );
9222
9329
  return registry;
9223
9330
  }
9331
+ function announceMap(state, log, lines, signature = lines.join("\n")) {
9332
+ if (state.announced === signature) return;
9333
+ const first = state.announced === void 0;
9334
+ state.announced = signature;
9335
+ 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:");
9336
+ for (const line of lines) log(line);
9337
+ }
9338
+ function mapSignature(state, roots) {
9339
+ return [
9340
+ ...state.clones.map((c) => `${c.repoFullName}=${c.path}${c.alternatives.length ? `+${c.alternatives.join("+")}` : ""}`),
9341
+ `missing:${state.missing.join(",")}`,
9342
+ `roots:${roots.join(",")}`
9343
+ ].join("\n");
9344
+ }
9224
9345
  function describeMapSummary(state, roots) {
9225
9346
  const found = state.clones.length;
9226
9347
  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 +10038,20 @@ import { readdirSync as readdirSync10, readFileSync as readFileSync12 } from "fs
9917
10038
  import { join as join16 } from "path";
9918
10039
  init_executors();
9919
10040
  function endedItself(outcome) {
9920
- return outcome === "graceful" || outcome === "budget";
10041
+ return outcome === "graceful" || outcome === "budget" || outcome === "aborted";
10042
+ }
10043
+ function wasAborted(outcome) {
10044
+ return outcome === "aborted";
9921
10045
  }
9922
10046
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
9923
- function classifyExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
10047
+ function readLatestResultRaw(runLogsDir, runLogKeys) {
9924
10048
  const dir = findRunLogDir(runLogsDir, runLogKeys);
9925
- if (!dir) return "unknown";
10049
+ if (!dir) return null;
9926
10050
  let files;
9927
10051
  try {
9928
10052
  files = readdirSync10(dir);
9929
10053
  } catch {
9930
- return "unknown";
10054
+ return null;
9931
10055
  }
9932
10056
  let latest = null;
9933
10057
  for (const f of files) {
@@ -9936,21 +10060,35 @@ function classifyExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
9936
10060
  const idx = Number(m[1]);
9937
10061
  if (!latest || idx > latest.idx) latest = { idx, name: f };
9938
10062
  }
9939
- if (!latest) return "unknown";
9940
- let raw;
10063
+ if (!latest) return null;
9941
10064
  try {
9942
- raw = readFileSync12(join16(dir, latest.name), "utf8");
10065
+ return readFileSync12(join16(dir, latest.name), "utf8");
9943
10066
  } catch {
9944
- return "unknown";
10067
+ return null;
9945
10068
  }
10069
+ }
10070
+ function classifyExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
10071
+ const raw = readLatestResultRaw(runLogsDir, runLogKeys);
10072
+ if (raw == null) return "unknown";
9946
10073
  const result = executor.parseResult(raw, null);
9947
10074
  if (!result) return "crash";
9948
- return result.stoppedByBudget ? "budget" : "graceful";
10075
+ if (result.stoppedByBudget) return "budget";
10076
+ if (result.aborted) return "aborted";
10077
+ return "graceful";
10078
+ }
10079
+ function abortOfExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
10080
+ const raw = readLatestResultRaw(runLogsDir, runLogKeys);
10081
+ if (raw == null) return null;
10082
+ return executor.parseResult(raw, null)?.aborted ?? null;
9949
10083
  }
9950
10084
  function makeExitClassifier(runLogsDir, runLogKeys, executor) {
9951
10085
  const exec = executor ?? resolveExecutor();
9952
10086
  return (run) => classifyExit(runLogsDir, runLogKeys(run.id), exec);
9953
10087
  }
10088
+ function makeAbortReader(runLogsDir, runLogKeys, executor) {
10089
+ const exec = executor ?? resolveExecutor();
10090
+ return (run) => abortOfExit(runLogsDir, runLogKeys(run.id), exec);
10091
+ }
9954
10092
 
9955
10093
  // src/orchestrator.ts
9956
10094
  init_src();
@@ -13439,7 +13577,7 @@ async function changedPaths(worktree, opts = {}) {
13439
13577
  // package.json
13440
13578
  var package_default = {
13441
13579
  name: "@taskless-app/swarm",
13442
- version: "0.6.21",
13580
+ version: "0.6.22",
13443
13581
  type: "module",
13444
13582
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
13445
13583
  keywords: [
@@ -13489,7 +13627,7 @@ var package_default = {
13489
13627
  execa: "^10.0.0",
13490
13628
  zod: "3.25.76"
13491
13629
  },
13492
- gitHead: "584411cbe69583a8727e89caa8ae7e626f8fd8ca"
13630
+ gitHead: "b8eaac4a5562ffcf45a02af338267620e0a7843f"
13493
13631
  };
13494
13632
 
13495
13633
  // src/version.ts
@@ -15345,9 +15483,9 @@ async function runSelfUpdate(io) {
15345
15483
  if (!exists(join28(repo, "pnpm-lock.yaml"))) {
15346
15484
  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
15485
  }
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)}` };
15486
+ const installed2 = await run("pnpm", ["install", "--frozen-lockfile"], repo);
15487
+ if (installed2.exitCode !== 0) {
15488
+ return { ok: false, step: "install", message: `pnpm install \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B: ${tail(installed2.stderr || installed2.stdout)}` };
15351
15489
  }
15352
15490
  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
15491
  const built = await run("pnpm", ["--filter", `${DAEMON_PACKAGE}...`, "build"], repo);
@@ -15970,9 +16108,24 @@ var executorHasSession = (p) => EXECUTOR.session.exists(p);
15970
16108
  var OVERMIND_ENTRY = THIS_FILE;
15971
16109
  var STALL_MS = Number(process.env.SWARM_STALL_MS) || 10 * 60 * 1e3;
15972
16110
  var STALL_FLOOR_MS = Number(process.env.SWARM_STALL_FLOOR_MS) || 30 * 60 * 1e3;
16111
+ var DEAD_HOLDER_SILENCE_MS = Number(process.env.SWARM_DEAD_HOLDER_SILENCE_MS) || 3 * 60 * 60 * 1e3;
15973
16112
  var RESPAWN_LIMIT = Number(process.env.SWARM_RESPAWN_LIMIT) || 2;
15974
16113
  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";
16114
+ 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";
16115
+ 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";
16116
+ 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";
16117
+ 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";
16118
+ 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";
16119
+ var ABORT_TEXT = {
16120
+ 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",
16121
+ 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",
16122
+ 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"
16123
+ };
16124
+ function abortReason(abort) {
16125
+ if (!abort) return "\u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C";
16126
+ return `${ABORT_TEXT[abort.kind]} (${abort.raw})`;
16127
+ }
16128
+ 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
16129
  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
16130
  var STORM_DEAD_LIMIT = Number(process.env.SWARM_STORM_DEAD_LIMIT) || 3;
15978
16131
  var CONFLICT_RESOLVE_COOLDOWN_MS = Number(process.env.SWARM_CONFLICT_COOLDOWN_MS) || 3 * 60 * 1e3;
@@ -16013,7 +16166,7 @@ var CHANGES_WAKE_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430
16013
16166
  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
16167
  var CHANGES_WAKE_FAILED_MARK = "\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C";
16015
16168
  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];
16169
+ var NO_AUTO_WAKE_REASONS = [EXITED_NO_DONE_REASON, BUDGET_STOP_REASON, ABORTED_STOP_REASON];
16017
16170
  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
16171
  function reviewBudgetMark(cycle) {
16019
16172
  return `${REVIEW_BUDGET_MARK} (\u043A\u0440\u0443\u0433 ${cycle})`;
@@ -16806,6 +16959,11 @@ function droneLabel(run) {
16806
16959
  function signalTitle(run, what) {
16807
16960
  return `${run.task_slug || droneLabel(run)}: ${what}`;
16808
16961
  }
16962
+ var tickNoise = makeRepeatFilter();
16963
+ function warnRepeating(key, message, now) {
16964
+ const line = tickNoise.say(key, message, now);
16965
+ if (line) console.error(line);
16966
+ }
16809
16967
  function narrate(deps, text) {
16810
16968
  try {
16811
16969
  deps.narrate?.(text);
@@ -17577,14 +17735,17 @@ async function trySpawnQueuedRun(deps, run) {
17577
17735
  return await failBeforeStart(deps, run, String(e));
17578
17736
  }
17579
17737
  }
17738
+ function holderStillHolds(deps, holder, now) {
17739
+ if (holder.pid != null) return deps.isAlive(holder.pid);
17740
+ return now - lastActivityMs(deps.db, holder) < DEAD_HOLDER_SILENCE_MS;
17741
+ }
17580
17742
  async function healBusyWorktree(deps, run, e) {
17581
17743
  const path = e.worktreePath;
17582
17744
  if (!deps.releaseWorktree) return false;
17583
17745
  if (deps.liveWorktrees?.().has(path)) return false;
17746
+ const now = deps.now ? deps.now() : Date.now();
17584
17747
  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
- }
17748
+ if (holder && holderStillHolds(deps, holder, now)) return false;
17588
17749
  if (deps.isDirty && await deps.isDirty(path).catch(() => true)) {
17589
17750
  deps.db.addEvent(
17590
17751
  run.id,
@@ -17594,10 +17755,12 @@ async function healBusyWorktree(deps, run, e) {
17594
17755
  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
17756
  return false;
17596
17757
  }
17758
+ const silence = holder && holder.pid == null ? `\u043C\u043E\u043B\u0447\u0430\u043B ${humanMinutes(now - lastActivityMs(deps.db, holder))}` : null;
17759
+ 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
17760
  await deps.releaseWorktree(run.repo, path).catch(() => {
17598
17761
  });
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}`);
17762
+ 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)`);
17763
+ 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
17764
  return true;
17602
17765
  }
17603
17766
  async function failBeforeStart(deps, run, reason) {
@@ -17908,6 +18071,15 @@ async function buildReliefCostCandidatesFor(deps, now, droneCap) {
17908
18071
  ...hrefOf(worstRun?.task_url)
17909
18072
  }));
17910
18073
  }
18074
+ async function workFate(deps, run) {
18075
+ if (run.pr_url) return `PR \u043E\u0442\u043A\u0440\u044B\u0442: ${run.pr_url}`;
18076
+ if (!deps.branchOnOrigin) return "";
18077
+ const branch = run.branch || run.task_slug;
18078
+ const exists = await deps.branchOnOrigin(run.repo, branch).catch(() => null);
18079
+ if (exists === true) return `${EXIT_WORK_PUSHED} (\u0432\u0435\u0442\u043A\u0430 ${branch})`;
18080
+ if (exists === false) return `${EXIT_WORK_LOST} (\u0432\u0435\u0442\u043A\u0438 ${branch} \u043D\u0430 origin \u043D\u0435\u0442)`;
18081
+ return EXIT_WORK_UNKNOWN;
18082
+ }
17911
18083
  async function deliveredPrUrl(deps, run) {
17912
18084
  if (run.pr_url) return run.pr_url;
17913
18085
  if (!deps.openPrsForBranch) return null;
@@ -18299,7 +18471,11 @@ async function tickOnce(deps) {
18299
18471
  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
18472
  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
18473
  } 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);
18474
+ warnRepeating(
18475
+ `self-reference:${signal.key}`,
18476
+ `[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)}`,
18477
+ now
18478
+ );
18303
18479
  }
18304
18480
  }
18305
18481
  continue;
@@ -18505,7 +18681,10 @@ async function tickOnce(deps) {
18505
18681
  continue;
18506
18682
  }
18507
18683
  }
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") {
18684
+ 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") {
18685
+ const aborted = wasAborted(outcome);
18686
+ const abort = aborted ? deps.exitAbort?.(run) ?? null : null;
18687
+ const what = aborted ? abortReason(abort) : EXITED_NO_DONE_REASON;
18509
18688
  const wt = run.worktree_path;
18510
18689
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
18511
18690
  if (deps.respawn && canContinue && run.respawns < RESPAWN_LIMIT) {
@@ -18513,21 +18692,31 @@ async function tickOnce(deps) {
18513
18692
  deps.db.addEvent(
18514
18693
  run.id,
18515
18694
  "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)`
18695
+ 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)`
18696
+ );
18697
+ narrate(
18698
+ deps,
18699
+ 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
18700
  );
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
18701
  try {
18520
- await deps.respawn(run, "continue", EXITED_CONTINUE_NUDGE);
18702
+ await deps.respawn(run, "continue", aborted ? ABORTED_CONTINUE_NUDGE : EXITED_CONTINUE_NUDGE);
18521
18703
  } 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)}`);
18704
+ 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)}`;
18705
+ deps.db.updateRun(run.id, { status: "paused", stop_reason: failed });
18706
+ deps.db.addEvent(run.id, "paused", failed);
18524
18707
  }
18525
18708
  continue;
18526
18709
  }
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;
18710
+ const marker = aborted ? ABORTED_STOP_REASON : EXITED_NO_DONE_REASON;
18711
+ 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;
18712
+ const fate = await workFate(deps, run);
18713
+ const reason2 = [head, aborted ? what : null, fate].filter(Boolean).join("; ");
18528
18714
  deps.db.updateRun(run.id, { status: "paused", stop_reason: reason2 });
18529
18715
  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).`);
18716
+ narrate(
18717
+ deps,
18718
+ 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).`
18719
+ );
18531
18720
  continue;
18532
18721
  }
18533
18722
  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 +18892,13 @@ async function tickOnce(deps) {
18703
18892
  const signal = finalCall ? { ...item.signal, body: `${item.signal.body} ${LAST_REMINDER_SUFFIX}` } : item.signal;
18704
18893
  try {
18705
18894
  await az.notify(signal);
18895
+ tickNoise.forget(`notify:${signal.key}`);
18706
18896
  } 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);
18897
+ warnRepeating(
18898
+ `notify:${signal.key}`,
18899
+ `[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)}`,
18900
+ now
18901
+ );
18708
18902
  continue;
18709
18903
  }
18710
18904
  for (const key of item.keys) az.tracker.commit(key, now);
@@ -18726,8 +18920,13 @@ async function tickOnce(deps) {
18726
18920
  try {
18727
18921
  await az.resolve(batch);
18728
18922
  az.tracker.forgetResolved(batch);
18923
+ tickNoise.forget("anomaly-resolve");
18729
18924
  } 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);
18925
+ warnRepeating(
18926
+ "anomaly-resolve",
18927
+ `[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)}`,
18928
+ now
18929
+ );
18731
18930
  }
18732
18931
  }
18733
18932
  }
@@ -19200,6 +19399,8 @@ async function reportWorkspaceAccess(tl, db) {
19200
19399
  }
19201
19400
  async function runDaemon(opts) {
19202
19401
  installCrashGuards();
19402
+ installLogStamps();
19403
+ console.log(localZoneLine(/* @__PURE__ */ new Date()));
19203
19404
  const daemonStart = captureDaemonStart();
19204
19405
  const latestRelease = fetchLatestVersion();
19205
19406
  const worktreesDir = await migrateWorktreesAtStartup();
@@ -19780,6 +19981,11 @@ async function runDaemon(opts) {
19780
19981
  // Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
19781
19982
  // (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
19782
19983
  exitOutcome: makeExitClassifier(RUN_LOGS_DIR, (runId) => db.runLogKeys(runId), EXECUTOR),
19984
+ // TASWM-487: вид обрыва — из того же result-файла и теми же ключами каталога.
19985
+ exitAbort: makeAbortReader(RUN_LOGS_DIR, (runId) => db.runLogKeys(runId), EXECUTOR),
19986
+ // TASWM-487: жива ли работа на origin. Тот же фолбэк repo, что у removeWorktree выше:
19987
+ // у прогона в основном репозитории демона run.repo пуст.
19988
+ branchOnOrigin: (repo, branch) => remoteBranchExists(repo || opts.repo, branch),
19783
19989
  // Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
19784
19990
  livePpids,
19785
19991
  // Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
@@ -20381,6 +20587,9 @@ if (isEntrypoint) {
20381
20587
  }
20382
20588
  }
20383
20589
  export {
20590
+ ABORTED_CONTINUE_NUDGE,
20591
+ ABORTED_STOP_REASON,
20592
+ ABORT_TEXT,
20384
20593
  AWAITING_CI_REASON,
20385
20594
  BUDGET_STOP_REASON,
20386
20595
  CHANGES_FRESH_WAKE_AFTER_MS,
@@ -20405,8 +20614,12 @@ export {
20405
20614
  CI_WAKE_LIMIT,
20406
20615
  CONFLICT_RESOLVE_COOLDOWN_MS,
20407
20616
  CONTINUE_NUDGE,
20617
+ DEAD_HOLDER_SILENCE_MS,
20408
20618
  EXITED_CONTINUE_NUDGE,
20409
20619
  EXITED_NO_DONE_REASON,
20620
+ EXIT_WORK_LOST,
20621
+ EXIT_WORK_PUSHED,
20622
+ EXIT_WORK_UNKNOWN,
20410
20623
  GATE_LOG_HEARTBEAT_TICKS,
20411
20624
  HEADLESS_BLOCK,
20412
20625
  HERO_CONTINUE_NUDGE,
@@ -20429,6 +20642,7 @@ export {
20429
20642
  STORM_DEAD_LIMIT,
20430
20643
  VERIFY_SPAWN_LIMIT,
20431
20644
  WATCHDOG_PARK_REASON,
20645
+ abortReason,
20432
20646
  buildChangesWakeNudge,
20433
20647
  buildCiReadyNudge,
20434
20648
  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.22",
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": "b8eaac4a5562ffcf45a02af338267620e0a7843f",
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», «кажется, работает»