@taskless-app/swarm 0.6.27 → 0.6.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +238 -198
  2. package/dist/overmind.js +195 -164
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -2770,6 +2770,192 @@ var init_storm = __esm({
2770
2770
  }
2771
2771
  });
2772
2772
 
2773
+ // src/agentToken.ts
2774
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync } from "fs";
2775
+ import { join as join6 } from "path";
2776
+ function loadAgentToken() {
2777
+ try {
2778
+ const s = JSON.parse(readFileSync4(STORE_PATH, "utf8"));
2779
+ return s.token ? s : null;
2780
+ } catch {
2781
+ return null;
2782
+ }
2783
+ }
2784
+ function saveAgentToken(s) {
2785
+ writeFileSync4(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
2786
+ }
2787
+ function clearAgentToken() {
2788
+ try {
2789
+ unlinkSync(STORE_PATH);
2790
+ } catch {
2791
+ }
2792
+ }
2793
+ async function mintAgentToken(args, fetchFn = fetch) {
2794
+ const res = await fetchFn(`${base(args.api)}/api/swarm/agent/token`, {
2795
+ method: "POST",
2796
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${args.bearer}` },
2797
+ body: JSON.stringify({
2798
+ ...args.label ? { label: args.label } : {},
2799
+ ...args.hostId ? { hostId: args.hostId } : {},
2800
+ ...args.hostName ? { hostName: args.hostName } : {}
2801
+ })
2802
+ });
2803
+ const text = await res.text();
2804
+ if (!res.ok) throw new Error(`agent token: HTTP ${res.status} ${text.slice(0, 200)}`);
2805
+ const j = JSON.parse(text);
2806
+ if (!j.token) throw new Error("agent token: \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442 token");
2807
+ return { token: j.token, ...j.tokenId ? { tokenId: j.tokenId } : {} };
2808
+ }
2809
+ async function probeAgentToken(args, fetchFn = fetch) {
2810
+ try {
2811
+ const res = await fetchFn(`${base(args.api)}/api/auth/me`, {
2812
+ headers: { Authorization: `Bearer ${args.token}` },
2813
+ // Оборвётся по таймауту — попадём в catch ниже, то есть в "unreachable": это ровно
2814
+ // то, чем висящая сеть и является. Соседи по CLI (daemonLink) живут так же.
2815
+ signal: AbortSignal.timeout(args.timeoutMs ?? TOKEN_PROBE_TIMEOUT_MS)
2816
+ });
2817
+ if (res.status === 401 || res.status === 403) return "revoked";
2818
+ return res.ok ? "ok" : "unreachable";
2819
+ } catch {
2820
+ return "unreachable";
2821
+ }
2822
+ }
2823
+ function isRevokedTokenError(e) {
2824
+ if (e && typeof e === "object" && e.code === 401) return true;
2825
+ const text = e instanceof Error ? e.message : typeof e === "string" ? e : "";
2826
+ return /invalid_token|(?:http|status|code)[ =:]*401\b|\(401\)|401 unauthorized/i.test(text);
2827
+ }
2828
+ function revokedTokenLine() {
2829
+ return "[taskless] \u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u043E\u0437\u0432\u0430\u043D (401 invalid_token) \u2014 \u0440\u043E\u0439 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442 \u0440\u0430\u0431\u043E\u0442\u0443. \u041F\u043E\u0447\u0438\u043D\u0438 \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: swarm login";
2830
+ }
2831
+ function revokedTokenComplaint() {
2832
+ return [
2833
+ "",
2834
+ "\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E",
2835
+ "\u2502 \u0422\u041E\u041A\u0415\u041D \u0420\u041E\u042F \u041D\u0415 \u041F\u0420\u0418\u041D\u042F\u0422 (401). \u0414\u043E\u0441\u0442\u0443\u043F \u043A Taskless \u0440\u0430\u0437\u043E\u0440\u0432\u0430\u043D. \u2502",
2836
+ "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F",
2837
+ "\u041F\u0440\u0438\u0447\u0438\u043D\u0430 \u2014 \u043E\u0434\u043D\u0430 \u0438\u0437 \u0442\u0440\u0451\u0445, \u0432\u0441\u0435 \u0442\u0440\u0438 \u0440\u0435\u0448\u0430\u044E\u0442\u0441\u044F \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0435\u043C:",
2838
+ " \u2022 \xAB\u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u044D\u0442\u043E\u0442 \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\xBB \u0432 \u043F\u0430\u0440\u043A\u0435 \u043C\u0430\u0448\u0438\u043D \u2014 \u043E\u0442\u043E\u0437\u0432\u0430\u043D \u0442\u043E\u043A\u0435\u043D \u042D\u0422\u041E\u0419 \u043C\u0430\u0448\u0438\u043D\u044B;",
2839
+ " \u2022 \xAB\u0441\u0431\u0440\u043E\u0441\u0438\u0442\u044C \u0434\u043E\u0441\u0442\u0443\u043F \u0440\u043E\u044F\xBB \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 (\u041F\u0440\u043E\u0444\u0438\u043B\u044C \u2192 \u0420\u043E\u0439) \u2014 \u043E\u0442\u043E\u0437\u0432\u0430\u043D\u044B \u0432\u0441\u0435;",
2840
+ " \u2022 \u0440\u043E\u0439 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D \u043C\u0430\u0441\u0442\u0435\u0440-\u0442\u0443\u043C\u0431\u043B\u0435\u0440\u043E\u043C \u2014 \u0442\u043E\u043A\u0435\u043D \u0430\u0433\u0435\u043D\u0442\u0430 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u0451\u0442 \u0434\u0435\u0439\u0441\u0442\u0432\u043E\u0432\u0430\u0442\u044C.",
2841
+ "",
2842
+ "\u0412\u0445\u043E\u0434 \u043D\u0430 \u0414\u0420\u0423\u0413\u041E\u0419 \u043C\u0430\u0448\u0438\u043D\u0435 \u044D\u0442\u043E\u0442 \u0442\u043E\u043A\u0435\u043D \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0433\u0430\u0441\u0438\u0442 (TASWM-324): \u0443 \u043A\u0430\u0436\u0434\u043E\u0439",
2843
+ "\u043C\u0430\u0448\u0438\u043D\u044B \u0441\u0432\u043E\u0439 \u0442\u043E\u043A\u0435\u043D, \u043F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0439 \u043A \u0435\u0451 hostId.",
2844
+ "",
2845
+ "\u0414\u0435\u043C\u043E\u043D \u041D\u0415 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432\u0441\u043B\u0435\u043F\u0443\u044E: \u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442, \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043D\u0435 \u0431\u0435\u0440\u0443\u0442\u0441\u044F \u0438 \u043E\u0442\u0447\u0451\u0442\u044B",
2846
+ "\u043D\u0435 \u043F\u0438\u0448\u0443\u0442\u0441\u044F. \u041F\u043E\u0447\u0438\u043D\u0438 \u0432\u0445\u043E\u0434\u043E\u043C \u0437\u0430\u043D\u043E\u0432\u043E: swarm login",
2847
+ "\u041F\u0440\u043E\u0431\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435 \u043F\u0440\u043E\u0445\u043E\u0434\u0438\u0442, \u0430 \u0432\u043E\u0439\u0442\u0438 \u043C\u0435\u0448\u0430\u0435\u0442 \u0441\u0442\u0430\u0440\u044B\u0439 \u0444\u0430\u0439\u043B: swarm login --force",
2848
+ ""
2849
+ ].join("\n");
2850
+ }
2851
+ var STORE_PATH, base, TOKEN_PROBE_TIMEOUT_MS;
2852
+ var init_agentToken = __esm({
2853
+ "src/agentToken.ts"() {
2854
+ "use strict";
2855
+ init_config();
2856
+ STORE_PATH = join6(SWARM_HOME, "agent.json");
2857
+ base = (api) => api.replace(/\/$/, "");
2858
+ TOKEN_PROBE_TIMEOUT_MS = 8e3;
2859
+ }
2860
+ });
2861
+
2862
+ // src/linkFailure.ts
2863
+ function describeError(e, maxDepth = 4) {
2864
+ const parts = [];
2865
+ const seen = /* @__PURE__ */ new Set();
2866
+ let cur = e;
2867
+ for (let depth = 0; depth < maxDepth && cur != null && !seen.has(cur); depth++) {
2868
+ seen.add(cur);
2869
+ const msg = (cur instanceof Error ? cur.message : typeof cur === "string" ? cur : String(cur)).trim();
2870
+ const code = cur.code;
2871
+ const codeText = code === void 0 || code === null ? "" : String(code);
2872
+ const label2 = codeText && !msg.includes(codeText) ? `${msg} [code=${codeText}]` : msg;
2873
+ if (label2 && !parts.includes(label2)) parts.push(label2);
2874
+ cur = cur.cause;
2875
+ }
2876
+ return parts.join(" \u2190 ") || String(e);
2877
+ }
2878
+ function explainLinkFailure(e) {
2879
+ const detail = describeError(e);
2880
+ if (detail.includes(UNKNOWN_LINK_REASON)) return { kind: "unknown", detail };
2881
+ if (isRevokedTokenError(e) || isRevokedTokenError(detail)) return { kind: "unauthorized", detail };
2882
+ if (MISSING_TOOL_RE.test(detail)) return { kind: "missing_tool", detail };
2883
+ if (OFFLINE_RE.test(detail)) return { kind: "offline", detail };
2884
+ return { kind: "rejected", detail };
2885
+ }
2886
+ var UNKNOWN_LINK_REASON, OFFLINE_RE, MISSING_TOOL_RE;
2887
+ var init_linkFailure = __esm({
2888
+ "src/linkFailure.ts"() {
2889
+ "use strict";
2890
+ init_agentToken();
2891
+ UNKNOWN_LINK_REASON = "\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F \u043D\u0435\u0442, \u0430 \u043F\u0440\u0438\u0447\u0438\u043D\u0443 \u043A\u043E\u043D\u043D\u0435\u043A\u0442\u043E\u0440 \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043B";
2892
+ OFFLINE_RE = /fetch failed|econnreset|econnrefused|econnaborted|enotfound|eai_again|etimedout|ehostunreach|enetunreach|epipe|socket hang up|und_err|network error|connection (?:closed|refused|reset|error)|connect timeout/i;
2893
+ MISSING_TOOL_RE = /tool[^.]{0,40}not found|unknown tool|tool_not_found|method not found|-32601\b/i;
2894
+ }
2895
+ });
2896
+
2897
+ // src/toolFailure.ts
2898
+ function classifyToolFailure(raw) {
2899
+ if (MISMATCH_RE.test(raw)) return TOOL_FAILURE.WORKSPACE_MISMATCH;
2900
+ if (FORBIDDEN_RE.test(raw)) return TOOL_FAILURE.FORBIDDEN;
2901
+ if (NOT_FOUND_RE.test(raw)) return TOOL_FAILURE.NOT_FOUND;
2902
+ return TOOL_FAILURE.TOOL_ERROR;
2903
+ }
2904
+ function classifyThrown(e) {
2905
+ const { kind, detail } = explainLinkFailure(e);
2906
+ const code = kind === "offline" ? TOOL_FAILURE.UNREACHABLE : kind === "unauthorized" ? TOOL_FAILURE.UNAUTHORIZED : (
2907
+ // «ручки нет» и «сервер отказал по существу» разбираем доменно: там может быть
2908
+ // и task_not_found, и отказ по правам — то есть наши коды, а не транспортные.
2909
+ classifyToolFailure(detail)
2910
+ );
2911
+ return { code, detail };
2912
+ }
2913
+ var TOOL_FAILURE, TOOL_FAILURE_TEXT, TOOL_FAILURE_RETRIABLE, NOT_FOUND_RE, FORBIDDEN_RE, MISMATCH_RE;
2914
+ var init_toolFailure = __esm({
2915
+ "src/toolFailure.ts"() {
2916
+ "use strict";
2917
+ init_linkFailure();
2918
+ TOOL_FAILURE = {
2919
+ /** Тул отработал, но искомого в ответе нет: задачи с таким слагом в воркспейсе не существует. */
2920
+ NOT_FOUND: "not_found",
2921
+ /** Объект есть, но токену роя не виден (приватный проект, спейс, чужой воркспейс). */
2922
+ FORBIDDEN: "forbidden",
2923
+ /** Токен роя не состоит в воркспейсе, которым искали. */
2924
+ WORKSPACE_MISMATCH: "workspace_mismatch",
2925
+ /** До прода не достучались: сеть, DNS, таймаут, оборванный сокет. */
2926
+ UNREACHABLE: "unreachable",
2927
+ /** Прод ответил, но токен роя отвергнут (отозван, протух). Сетью это не лечится. */
2928
+ UNAUTHORIZED: "unauthorized",
2929
+ /** Тул ответил ошибкой, которую мы не узнали, — текст кладём в detail как есть. */
2930
+ TOOL_ERROR: "tool_error",
2931
+ /** Успех, но полезной нагрузки в ответе нет (форма ответа разошлась с ожиданием). */
2932
+ EMPTY: "empty"
2933
+ };
2934
+ TOOL_FAILURE_TEXT = {
2935
+ [TOOL_FAILURE.NOT_FOUND]: "\u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435\u0442 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043B\u0430\u0433 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
2936
+ [TOOL_FAILURE.FORBIDDEN]: "\u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0435\u0442: \u043E\u0431\u044A\u0435\u043A\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0440\u043E\u044F \u043D\u0435 \u0432\u0438\u0434\u0435\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
2937
+ [TOOL_FAILURE.WORKSPACE_MISMATCH]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435",
2938
+ [TOOL_FAILURE.UNREACHABLE]: "Taskless \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u2014 \u0441\u0435\u0442\u044C, DNS \u0438\u043B\u0438 \u0442\u0430\u0439\u043C\u0430\u0443\u0442",
2939
+ [TOOL_FAILURE.UNAUTHORIZED]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442 \u043F\u0440\u043E\u0434\u043E\u043C \u2014 \u043D\u0443\u0436\u0435\u043D swarm login \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435",
2940
+ [TOOL_FAILURE.TOOL_ERROR]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0448\u0438\u0431\u043A\u043E\u0439",
2941
+ [TOOL_FAILURE.EMPTY]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0443\u0441\u043F\u0435\u0445\u043E\u043C, \u043D\u043E \u0434\u0430\u043D\u043D\u044B\u0445 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442"
2942
+ };
2943
+ TOOL_FAILURE_RETRIABLE = {
2944
+ [TOOL_FAILURE.NOT_FOUND]: false,
2945
+ [TOOL_FAILURE.FORBIDDEN]: false,
2946
+ [TOOL_FAILURE.WORKSPACE_MISMATCH]: false,
2947
+ [TOOL_FAILURE.UNREACHABLE]: true,
2948
+ // Отозванный токен повтором не чинится — его меняет человек (`swarm login`).
2949
+ [TOOL_FAILURE.UNAUTHORIZED]: false,
2950
+ [TOOL_FAILURE.TOOL_ERROR]: false,
2951
+ [TOOL_FAILURE.EMPTY]: false
2952
+ };
2953
+ NOT_FOUND_RE = /task_not_found|not_found|не найдена|не найден/i;
2954
+ FORBIDDEN_RE = /forbidden|not visible|no access|нет доступа|(?:http|status|code)[ =:]*403\b|\(403\)|403 forbidden/i;
2955
+ MISMATCH_RE = /workspace_mismatch|not a member of that workspace/i;
2956
+ }
2957
+ });
2958
+
2773
2959
  // src/procList.ts
2774
2960
  function parsePsRows(out) {
2775
2961
  const rows = [];
@@ -3053,7 +3239,7 @@ var init_package = __esm({
3053
3239
  "package.json"() {
3054
3240
  package_default = {
3055
3241
  name: "@taskless-app/swarm",
3056
- version: "0.6.27",
3242
+ version: "0.6.29",
3057
3243
  type: "module",
3058
3244
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
3059
3245
  keywords: [
@@ -3104,7 +3290,7 @@ var init_package = __esm({
3104
3290
  execa: "^10.0.0",
3105
3291
  zod: "3.25.76"
3106
3292
  },
3107
- gitHead: "2e1ed83014422ae577be60271adc79e291b9e2ab"
3293
+ gitHead: "d8072f68198b68b4252687b01e7e82e2b4769346"
3108
3294
  };
3109
3295
  }
3110
3296
  });
@@ -3236,18 +3422,18 @@ var init_netTimeout = __esm({
3236
3422
 
3237
3423
  // src/droneChannel.ts
3238
3424
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
3239
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
3425
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
3240
3426
  import { dirname as dirname3 } from "path";
3241
3427
  import { serve } from "@hono/node-server";
3242
3428
  function loadChannelSecret(path) {
3243
3429
  try {
3244
- const s = readFileSync7(path, "utf8").trim();
3430
+ const s = readFileSync8(path, "utf8").trim();
3245
3431
  if (s.length >= 32) return s;
3246
3432
  } catch {
3247
3433
  }
3248
3434
  const secret = randomBytes(32).toString("hex");
3249
3435
  mkdirSync7(dirname3(path), { recursive: true });
3250
- writeFileSync6(path, secret, { mode: 384 });
3436
+ writeFileSync7(path, secret, { mode: 384 });
3251
3437
  try {
3252
3438
  chmodSync2(path, 384);
3253
3439
  } catch {
@@ -3286,95 +3472,6 @@ var init_droneChannel = __esm({
3286
3472
  }
3287
3473
  });
3288
3474
 
3289
- // src/agentToken.ts
3290
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync2 } from "fs";
3291
- import { join as join10 } from "path";
3292
- function loadAgentToken() {
3293
- try {
3294
- const s = JSON.parse(readFileSync8(STORE_PATH, "utf8"));
3295
- return s.token ? s : null;
3296
- } catch {
3297
- return null;
3298
- }
3299
- }
3300
- function saveAgentToken(s) {
3301
- writeFileSync7(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
3302
- }
3303
- function clearAgentToken() {
3304
- try {
3305
- unlinkSync2(STORE_PATH);
3306
- } catch {
3307
- }
3308
- }
3309
- async function mintAgentToken(args, fetchFn = fetch) {
3310
- const res = await fetchFn(`${base(args.api)}/api/swarm/agent/token`, {
3311
- method: "POST",
3312
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${args.bearer}` },
3313
- body: JSON.stringify({
3314
- ...args.label ? { label: args.label } : {},
3315
- ...args.hostId ? { hostId: args.hostId } : {},
3316
- ...args.hostName ? { hostName: args.hostName } : {}
3317
- })
3318
- });
3319
- const text = await res.text();
3320
- if (!res.ok) throw new Error(`agent token: HTTP ${res.status} ${text.slice(0, 200)}`);
3321
- const j = JSON.parse(text);
3322
- if (!j.token) throw new Error("agent token: \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442 token");
3323
- return { token: j.token, ...j.tokenId ? { tokenId: j.tokenId } : {} };
3324
- }
3325
- async function probeAgentToken(args, fetchFn = fetch) {
3326
- try {
3327
- const res = await fetchFn(`${base(args.api)}/api/auth/me`, {
3328
- headers: { Authorization: `Bearer ${args.token}` },
3329
- // Оборвётся по таймауту — попадём в catch ниже, то есть в "unreachable": это ровно
3330
- // то, чем висящая сеть и является. Соседи по CLI (daemonLink) живут так же.
3331
- signal: AbortSignal.timeout(args.timeoutMs ?? TOKEN_PROBE_TIMEOUT_MS)
3332
- });
3333
- if (res.status === 401 || res.status === 403) return "revoked";
3334
- return res.ok ? "ok" : "unreachable";
3335
- } catch {
3336
- return "unreachable";
3337
- }
3338
- }
3339
- function isRevokedTokenError(e) {
3340
- if (e && typeof e === "object" && e.code === 401) return true;
3341
- const text = e instanceof Error ? e.message : typeof e === "string" ? e : "";
3342
- return /invalid_token|(?:http|status|code)[ =:]*401\b|\(401\)|401 unauthorized/i.test(text);
3343
- }
3344
- function revokedTokenLine() {
3345
- return "[taskless] \u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u043E\u0437\u0432\u0430\u043D (401 invalid_token) \u2014 \u0440\u043E\u0439 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442 \u0440\u0430\u0431\u043E\u0442\u0443. \u041F\u043E\u0447\u0438\u043D\u0438 \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: swarm login";
3346
- }
3347
- function revokedTokenComplaint() {
3348
- return [
3349
- "",
3350
- "\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E",
3351
- "\u2502 \u0422\u041E\u041A\u0415\u041D \u0420\u041E\u042F \u041D\u0415 \u041F\u0420\u0418\u041D\u042F\u0422 (401). \u0414\u043E\u0441\u0442\u0443\u043F \u043A Taskless \u0440\u0430\u0437\u043E\u0440\u0432\u0430\u043D. \u2502",
3352
- "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F",
3353
- "\u041F\u0440\u0438\u0447\u0438\u043D\u0430 \u2014 \u043E\u0434\u043D\u0430 \u0438\u0437 \u0442\u0440\u0451\u0445, \u0432\u0441\u0435 \u0442\u0440\u0438 \u0440\u0435\u0448\u0430\u044E\u0442\u0441\u044F \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0435\u043C:",
3354
- " \u2022 \xAB\u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u044D\u0442\u043E\u0442 \u043A\u043E\u043C\u043F\u044C\u044E\u0442\u0435\u0440\xBB \u0432 \u043F\u0430\u0440\u043A\u0435 \u043C\u0430\u0448\u0438\u043D \u2014 \u043E\u0442\u043E\u0437\u0432\u0430\u043D \u0442\u043E\u043A\u0435\u043D \u042D\u0422\u041E\u0419 \u043C\u0430\u0448\u0438\u043D\u044B;",
3355
- " \u2022 \xAB\u0441\u0431\u0440\u043E\u0441\u0438\u0442\u044C \u0434\u043E\u0441\u0442\u0443\u043F \u0440\u043E\u044F\xBB \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 (\u041F\u0440\u043E\u0444\u0438\u043B\u044C \u2192 \u0420\u043E\u0439) \u2014 \u043E\u0442\u043E\u0437\u0432\u0430\u043D\u044B \u0432\u0441\u0435;",
3356
- " \u2022 \u0440\u043E\u0439 \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D \u043C\u0430\u0441\u0442\u0435\u0440-\u0442\u0443\u043C\u0431\u043B\u0435\u0440\u043E\u043C \u2014 \u0442\u043E\u043A\u0435\u043D \u0430\u0433\u0435\u043D\u0442\u0430 \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u0451\u0442 \u0434\u0435\u0439\u0441\u0442\u0432\u043E\u0432\u0430\u0442\u044C.",
3357
- "",
3358
- "\u0412\u0445\u043E\u0434 \u043D\u0430 \u0414\u0420\u0423\u0413\u041E\u0419 \u043C\u0430\u0448\u0438\u043D\u0435 \u044D\u0442\u043E\u0442 \u0442\u043E\u043A\u0435\u043D \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0433\u0430\u0441\u0438\u0442 (TASWM-324): \u0443 \u043A\u0430\u0436\u0434\u043E\u0439",
3359
- "\u043C\u0430\u0448\u0438\u043D\u044B \u0441\u0432\u043E\u0439 \u0442\u043E\u043A\u0435\u043D, \u043F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0439 \u043A \u0435\u0451 hostId.",
3360
- "",
3361
- "\u0414\u0435\u043C\u043E\u043D \u041D\u0415 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432\u0441\u043B\u0435\u043F\u0443\u044E: \u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442, \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043D\u0435 \u0431\u0435\u0440\u0443\u0442\u0441\u044F \u0438 \u043E\u0442\u0447\u0451\u0442\u044B",
3362
- "\u043D\u0435 \u043F\u0438\u0448\u0443\u0442\u0441\u044F. \u041F\u043E\u0447\u0438\u043D\u0438 \u0432\u0445\u043E\u0434\u043E\u043C \u0437\u0430\u043D\u043E\u0432\u043E: swarm login",
3363
- "\u041F\u0440\u043E\u0431\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435 \u043F\u0440\u043E\u0445\u043E\u0434\u0438\u0442, \u0430 \u0432\u043E\u0439\u0442\u0438 \u043C\u0435\u0448\u0430\u0435\u0442 \u0441\u0442\u0430\u0440\u044B\u0439 \u0444\u0430\u0439\u043B: swarm login --force",
3364
- ""
3365
- ].join("\n");
3366
- }
3367
- var STORE_PATH, base, TOKEN_PROBE_TIMEOUT_MS;
3368
- var init_agentToken = __esm({
3369
- "src/agentToken.ts"() {
3370
- "use strict";
3371
- init_config();
3372
- STORE_PATH = join10(SWARM_HOME, "agent.json");
3373
- base = (api) => api.replace(/\/$/, "");
3374
- TOKEN_PROBE_TIMEOUT_MS = 8e3;
3375
- }
3376
- });
3377
-
3378
3475
  // src/authState.ts
3379
3476
  function noteTokenRejected(nowMs = Date.now()) {
3380
3477
  if (!denial) {
@@ -3818,85 +3915,6 @@ var init_shotUpload = __esm({
3818
3915
  }
3819
3916
  });
3820
3917
 
3821
- // src/linkFailure.ts
3822
- function describeError(e, maxDepth = 4) {
3823
- const parts = [];
3824
- const seen = /* @__PURE__ */ new Set();
3825
- let cur = e;
3826
- for (let depth = 0; depth < maxDepth && cur != null && !seen.has(cur); depth++) {
3827
- seen.add(cur);
3828
- const msg = (cur instanceof Error ? cur.message : typeof cur === "string" ? cur : String(cur)).trim();
3829
- const code = cur.code;
3830
- const codeText = code === void 0 || code === null ? "" : String(code);
3831
- const label2 = codeText && !msg.includes(codeText) ? `${msg} [code=${codeText}]` : msg;
3832
- if (label2 && !parts.includes(label2)) parts.push(label2);
3833
- cur = cur.cause;
3834
- }
3835
- return parts.join(" \u2190 ") || String(e);
3836
- }
3837
- function explainLinkFailure(e) {
3838
- const detail = describeError(e);
3839
- if (detail.includes(UNKNOWN_LINK_REASON)) return { kind: "unknown", detail };
3840
- if (isRevokedTokenError(e) || isRevokedTokenError(detail)) return { kind: "unauthorized", detail };
3841
- if (MISSING_TOOL_RE.test(detail)) return { kind: "missing_tool", detail };
3842
- if (OFFLINE_RE.test(detail)) return { kind: "offline", detail };
3843
- return { kind: "rejected", detail };
3844
- }
3845
- var UNKNOWN_LINK_REASON, OFFLINE_RE, MISSING_TOOL_RE;
3846
- var init_linkFailure = __esm({
3847
- "src/linkFailure.ts"() {
3848
- "use strict";
3849
- init_agentToken();
3850
- UNKNOWN_LINK_REASON = "\u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F \u043D\u0435\u0442, \u0430 \u043F\u0440\u0438\u0447\u0438\u043D\u0443 \u043A\u043E\u043D\u043D\u0435\u043A\u0442\u043E\u0440 \u043D\u0435 \u043D\u0430\u0437\u0432\u0430\u043B";
3851
- OFFLINE_RE = /fetch failed|econnreset|econnrefused|econnaborted|enotfound|eai_again|etimedout|ehostunreach|enetunreach|epipe|socket hang up|und_err|network error|connection (?:closed|refused|reset|error)|connect timeout/i;
3852
- MISSING_TOOL_RE = /tool[^.]{0,40}not found|unknown tool|tool_not_found|method not found|-32601\b/i;
3853
- }
3854
- });
3855
-
3856
- // src/toolFailure.ts
3857
- var TOOL_FAILURE, TOOL_FAILURE_TEXT, TOOL_FAILURE_RETRIABLE;
3858
- var init_toolFailure = __esm({
3859
- "src/toolFailure.ts"() {
3860
- "use strict";
3861
- init_linkFailure();
3862
- TOOL_FAILURE = {
3863
- /** Тул отработал, но искомого в ответе нет: задачи с таким слагом в воркспейсе не существует. */
3864
- NOT_FOUND: "not_found",
3865
- /** Объект есть, но токену роя не виден (приватный проект, спейс, чужой воркспейс). */
3866
- FORBIDDEN: "forbidden",
3867
- /** Токен роя не состоит в воркспейсе, которым искали. */
3868
- WORKSPACE_MISMATCH: "workspace_mismatch",
3869
- /** До прода не достучались: сеть, DNS, таймаут, оборванный сокет. */
3870
- UNREACHABLE: "unreachable",
3871
- /** Прод ответил, но токен роя отвергнут (отозван, протух). Сетью это не лечится. */
3872
- UNAUTHORIZED: "unauthorized",
3873
- /** Тул ответил ошибкой, которую мы не узнали, — текст кладём в detail как есть. */
3874
- TOOL_ERROR: "tool_error",
3875
- /** Успех, но полезной нагрузки в ответе нет (форма ответа разошлась с ожиданием). */
3876
- EMPTY: "empty"
3877
- };
3878
- TOOL_FAILURE_TEXT = {
3879
- [TOOL_FAILURE.NOT_FOUND]: "\u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435\u0442 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043B\u0430\u0433 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
3880
- [TOOL_FAILURE.FORBIDDEN]: "\u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0435\u0442: \u043E\u0431\u044A\u0435\u043A\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0440\u043E\u044F \u043D\u0435 \u0432\u0438\u0434\u0435\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
3881
- [TOOL_FAILURE.WORKSPACE_MISMATCH]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435",
3882
- [TOOL_FAILURE.UNREACHABLE]: "Taskless \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u2014 \u0441\u0435\u0442\u044C, DNS \u0438\u043B\u0438 \u0442\u0430\u0439\u043C\u0430\u0443\u0442",
3883
- [TOOL_FAILURE.UNAUTHORIZED]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442 \u043F\u0440\u043E\u0434\u043E\u043C \u2014 \u043D\u0443\u0436\u0435\u043D swarm login \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435",
3884
- [TOOL_FAILURE.TOOL_ERROR]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0448\u0438\u0431\u043A\u043E\u0439",
3885
- [TOOL_FAILURE.EMPTY]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0443\u0441\u043F\u0435\u0445\u043E\u043C, \u043D\u043E \u0434\u0430\u043D\u043D\u044B\u0445 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442"
3886
- };
3887
- TOOL_FAILURE_RETRIABLE = {
3888
- [TOOL_FAILURE.NOT_FOUND]: false,
3889
- [TOOL_FAILURE.FORBIDDEN]: false,
3890
- [TOOL_FAILURE.WORKSPACE_MISMATCH]: false,
3891
- [TOOL_FAILURE.UNREACHABLE]: true,
3892
- // Отозванный токен повтором не чинится — его меняет человек (`swarm login`).
3893
- [TOOL_FAILURE.UNAUTHORIZED]: false,
3894
- [TOOL_FAILURE.TOOL_ERROR]: false,
3895
- [TOOL_FAILURE.EMPTY]: false
3896
- };
3897
- }
3898
- });
3899
-
3900
3918
  // src/workspace.ts
3901
3919
  import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync4 } from "fs";
3902
3920
  import { join as join17 } from "path";
@@ -6879,7 +6897,7 @@ init_config();
6879
6897
  init_resources();
6880
6898
  init_names2();
6881
6899
  init_storm();
6882
- import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
6900
+ import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
6883
6901
  import { dirname as dirname2 } from "path";
6884
6902
  import { randomUUID } from "crypto";
6885
6903
 
@@ -6906,20 +6924,23 @@ function parsePendingRepoRules(raw) {
6906
6924
  return out;
6907
6925
  }
6908
6926
 
6927
+ // src/swarmClient.ts
6928
+ init_toolFailure();
6929
+
6909
6930
  // src/logTail.ts
6910
6931
  import { closeSync, existsSync as existsSync5, openSync, readSync, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
6911
- import { join as join7 } from "path";
6932
+ import { join as join8 } from "path";
6912
6933
 
6913
6934
  // src/runLogDir.ts
6914
6935
  import { existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync as readdirSync4, renameSync as renameSync2, statSync as statSync3 } from "fs";
6915
- import { join as join6 } from "path";
6936
+ import { join as join7 } from "path";
6916
6937
  function runLogDirName(runLogKey) {
6917
6938
  const safe = runLogKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
6918
6939
  if (!safe) throw new Error(`\u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435 \u0433\u043E\u0434\u0438\u0442\u0441\u044F \u0432 \u0438\u043C\u044F \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430 run-log'\u043E\u0432: ${JSON.stringify(runLogKey)}`);
6919
6940
  return /^\d+$/.test(safe) ? `k-${safe}` : safe;
6920
6941
  }
6921
6942
  function runLogDirPath(runLogsDir, runLogKey) {
6922
- return join6(runLogsDir, runLogDirName(runLogKey));
6943
+ return join7(runLogsDir, runLogDirName(runLogKey));
6923
6944
  }
6924
6945
  function findRunLogDir(runLogsDir, keys) {
6925
6946
  for (const key of keys) {
@@ -6947,6 +6968,14 @@ function toolErrorText(res) {
6947
6968
  if (!text) return "\u043F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0440\u0433 \u0432\u044B\u0437\u043E\u0432 \u0431\u0435\u0437 \u043E\u0431\u044A\u044F\u0441\u043D\u0435\u043D\u0438\u044F";
6948
6969
  return text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
6949
6970
  }
6971
+ var DEPS_UNRETRIABLE_CODES = /* @__PURE__ */ new Set([
6972
+ TOOL_FAILURE.FORBIDDEN,
6973
+ TOOL_FAILURE.UNAUTHORIZED,
6974
+ TOOL_FAILURE.WORKSPACE_MISMATCH
6975
+ ]);
6976
+ function depsFailureRetriable(code) {
6977
+ return !DEPS_UNRETRIABLE_CODES.has(code);
6978
+ }
6950
6979
  function runRef(runKey) {
6951
6980
  return { __runRef: runKey };
6952
6981
  }
@@ -7043,6 +7072,13 @@ var SwarmClient = class _SwarmClient {
7043
7072
  // дальше удвоением до потолка TTL: устойчиво отвергающий прод (старее демона, скоуп
7044
7073
  // токена) не должен получать вопрос каждые четыре секунды до конца своей жизни.
7045
7074
  static DEPS_RETRY_BASE_MS = 5e3;
7075
+ // TASWM-517: сколько раз спрашиваем НЕСАМОЛЕЧАЩИЙСЯ отказ, прежде чем перестать вовсе.
7076
+ // `agent_forbidden` («тул недоступен аккаунту-агенту роя») повтором не чинится: 26.08
7077
+ // два прогона получили его по 53 раза каждый и остались в очереди навсегда. Три захода
7078
+ // при backoff 5с/10с — это ~15 секунд: запас на случай, если классификатор ошибся и
7079
+ // отказ на деле транзиентный, но не час молчаливого простоя. По исчерпании прогон
7080
+ // роняет гейт спавна (DEPS_UNRETRIABLE_ATTEMPTS читается и там).
7081
+ static DEPS_UNRETRIABLE_ATTEMPTS = 3;
7046
7082
  // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
7047
7083
  // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
7048
7084
  // каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
@@ -7114,13 +7150,13 @@ var SwarmClient = class _SwarmClient {
7114
7150
  recover() {
7115
7151
  if (existsSync6(this.mapPath)) {
7116
7152
  try {
7117
- const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
7153
+ const obj = JSON.parse(readFileSync5(this.mapPath, "utf8"));
7118
7154
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
7119
7155
  } catch {
7120
7156
  }
7121
7157
  }
7122
7158
  if (existsSync6(this.outboxPath)) {
7123
- const txt = readFileSync4(this.outboxPath, "utf8");
7159
+ const txt = readFileSync5(this.outboxPath, "utf8");
7124
7160
  for (const line of txt.split("\n")) {
7125
7161
  const s = line.trim();
7126
7162
  if (!s) continue;
@@ -7143,7 +7179,7 @@ var SwarmClient = class _SwarmClient {
7143
7179
  if (!existsSync6(this.stormPath)) return;
7144
7180
  let txt;
7145
7181
  try {
7146
- txt = readFileSync4(this.stormPath, "utf8");
7182
+ txt = readFileSync5(this.stormPath, "utf8");
7147
7183
  } catch {
7148
7184
  return;
7149
7185
  }
@@ -7648,11 +7684,12 @@ var SwarmClient = class _SwarmClient {
7648
7684
  res = await this.call("swarm_deps_status", { refs: ask });
7649
7685
  } catch (e) {
7650
7686
  for (const ref of ask) this.depsAskedAt.delete(ref);
7651
- this.noteDepsUnanswered(ask, nowMs, String(e));
7687
+ this.noteDepsUnanswered(ask, nowMs, String(e), depsFailureRetriable(classifyThrown(e).code));
7652
7688
  return;
7653
7689
  }
7654
7690
  if (res?.isError) {
7655
- this.noteDepsUnanswered(ask, nowMs, toolErrorText(res));
7691
+ const text = toolErrorText(res);
7692
+ this.noteDepsUnanswered(ask, nowMs, text, depsFailureRetriable(classifyToolFailure(text)));
7656
7693
  return;
7657
7694
  }
7658
7695
  const deps = res.structuredContent?.deps ?? [];
@@ -7692,6 +7729,7 @@ var SwarmClient = class _SwarmClient {
7692
7729
  depsRetryDelay(ref) {
7693
7730
  const fail = this.depsUnanswered.get(ref);
7694
7731
  if (fail) {
7732
+ if (!fail.retriable && fail.attempts >= _SwarmClient.DEPS_UNRETRIABLE_ATTEMPTS) return _SwarmClient.DEPS_TTL_MS;
7695
7733
  const backoff = _SwarmClient.DEPS_RETRY_BASE_MS * 2 ** Math.min(fail.attempts - 1, 10);
7696
7734
  return Math.min(backoff, _SwarmClient.DEPS_TTL_MS);
7697
7735
  }
@@ -7700,17 +7738,19 @@ var SwarmClient = class _SwarmClient {
7700
7738
  return _SwarmClient.DEPS_TTL_MS;
7701
7739
  }
7702
7740
  /** Запомнить, что прод по этим ссылкам не ответил (см. depsUnanswered / TASWM-486). */
7703
- noteDepsUnanswered(refs, nowMs, error) {
7741
+ noteDepsUnanswered(refs, nowMs, error, retriable = true) {
7704
7742
  for (const ref of refs) {
7705
7743
  const prev = this.depsUnanswered.get(ref);
7744
+ const sameKind = prev != null && prev.retriable === retriable;
7706
7745
  this.depsUnanswered.set(ref, {
7707
7746
  ref,
7708
7747
  slug: this.depSlug.get(ref) ?? ref,
7709
7748
  // `since` — начало ЭПИЗОДА слепоты, а не последняя попытка: именно возраст эпизода
7710
7749
  // отвечает на вопрос «сколько прогон уже стоит вслепую».
7711
7750
  since: prev?.since ?? nowMs,
7712
- attempts: (prev?.attempts ?? 0) + 1,
7713
- error
7751
+ attempts: sameKind ? prev.attempts + 1 : 1,
7752
+ error,
7753
+ retriable
7714
7754
  });
7715
7755
  }
7716
7756
  }
@@ -9254,13 +9294,13 @@ var SwarmClient = class _SwarmClient {
9254
9294
  }
9255
9295
  persistPending() {
9256
9296
  try {
9257
- writeFileSync4(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
9297
+ writeFileSync5(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
9258
9298
  } catch {
9259
9299
  }
9260
9300
  }
9261
9301
  persistMap() {
9262
9302
  try {
9263
- writeFileSync4(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
9303
+ writeFileSync5(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
9264
9304
  } catch {
9265
9305
  }
9266
9306
  }
@@ -9393,7 +9433,7 @@ init_config();
9393
9433
  import { $ as $6 } from "execa";
9394
9434
 
9395
9435
  // src/daemonLaunch.ts
9396
- import { openSync as openSync2, closeSync as closeSync2, readFileSync as readFileSync5 } from "fs";
9436
+ import { openSync as openSync2, closeSync as closeSync2, readFileSync as readFileSync6 } from "fs";
9397
9437
  import { spawn } from "child_process";
9398
9438
  var FOREGROUND_FLAGS = /* @__PURE__ */ new Set(["--foreground", "-f", "--attach"]);
9399
9439
  function parseStartMode(argv) {
@@ -9453,14 +9493,14 @@ function launchDetached(opts) {
9453
9493
  }
9454
9494
  function tailFile(path, lines) {
9455
9495
  try {
9456
- return tailText(readFileSync5(path, "utf8"), lines);
9496
+ return tailText(readFileSync6(path, "utf8"), lines);
9457
9497
  } catch {
9458
9498
  return "";
9459
9499
  }
9460
9500
  }
9461
9501
 
9462
9502
  // src/logRotate.ts
9463
- import { copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync5, fstatSync, truncateSync, unlinkSync, existsSync as existsSync7 } from "fs";
9503
+ import { copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync5, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync7 } from "fs";
9464
9504
  var realIo = {
9465
9505
  size: (path) => {
9466
9506
  try {
@@ -9472,7 +9512,7 @@ var realIo = {
9472
9512
  copy: copyFileSync2,
9473
9513
  rename: renameSync3,
9474
9514
  truncate: (path) => truncateSync(path, 0),
9475
- unlink: unlinkSync,
9515
+ unlink: unlinkSync2,
9476
9516
  exists: existsSync7
9477
9517
  };
9478
9518
  var STRAY_GENERATIONS = 8;
@@ -9501,17 +9541,17 @@ init_repoRegistry();
9501
9541
 
9502
9542
  // src/hostIdentity.ts
9503
9543
  import { hostname } from "os";
9504
- import { existsSync as existsSync8, readFileSync as readFileSync6, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
9505
- import { join as join8 } from "path";
9544
+ import { existsSync as existsSync8, readFileSync as readFileSync7, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
9545
+ import { join as join9 } from "path";
9506
9546
  import { randomUUID as randomUUID2 } from "crypto";
9507
9547
  function hostIdentityPath(home) {
9508
- return join8(home, "host.json");
9548
+ return join9(home, "host.json");
9509
9549
  }
9510
9550
  function loadHostIdentity(deps) {
9511
9551
  const exists = deps.exists ?? existsSync8;
9512
- const readFile2 = deps.readFile ?? ((p) => readFileSync6(p, "utf8"));
9552
+ const readFile2 = deps.readFile ?? ((p) => readFileSync7(p, "utf8"));
9513
9553
  const writeFile3 = deps.writeFile ?? ((p, data) => {
9514
- writeFileSync5(`${p}.tmp`, data);
9554
+ writeFileSync6(`${p}.tmp`, data);
9515
9555
  renameSync4(`${p}.tmp`, p);
9516
9556
  });
9517
9557
  const host = deps.hostname ?? hostname;
@@ -9627,7 +9667,7 @@ import { serve as serve2 } from "@hono/node-server";
9627
9667
 
9628
9668
  // src/diff.ts
9629
9669
  import { statSync as statSync6 } from "fs";
9630
- import { join as join9 } from "path";
9670
+ import { join as join10 } from "path";
9631
9671
  import { $ as $3 } from "execa";
9632
9672
  var MAX_FILES = 200;
9633
9673
  var MAX_LINES_PER_FILE = 1500;
@@ -9795,7 +9835,7 @@ async function collectRunDiff(worktree, opts = {}) {
9795
9835
  files.push(pf);
9796
9836
  }
9797
9837
  }
9798
- files.sort((a, b) => mtime(join9(worktree, b.path)) - mtime(join9(worktree, a.path)));
9838
+ files.sort((a, b) => mtime(join10(worktree, b.path)) - mtime(join10(worktree, a.path)));
9799
9839
  const truncatedFiles = files.length > MAX_FILES;
9800
9840
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
9801
9841
  const added = files.reduce((s, f) => s + f.added, 0);
package/dist/overmind.js CHANGED
@@ -3092,76 +3092,12 @@ var init_storm = __esm({
3092
3092
  }
3093
3093
  });
3094
3094
 
3095
- // src/heartbeat.ts
3096
- import { readFileSync as readFileSync6, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
3097
- function writeHeartbeat(path, now) {
3098
- const tmp = `${path}.tmp`;
3099
- writeFileSync6(tmp, String(now));
3100
- renameSync4(tmp, path);
3101
- }
3102
- function readHeartbeat(path) {
3103
- try {
3104
- const ms = Number(readFileSync6(path, "utf8").trim());
3105
- return Number.isFinite(ms) ? ms : null;
3106
- } catch {
3107
- return null;
3108
- }
3109
- }
3110
- function overmindAlive(path, now, staleMs) {
3111
- const ts = readHeartbeat(path);
3112
- if (ts == null) return false;
3113
- return now - ts < staleMs;
3114
- }
3115
- var init_heartbeat = __esm({
3116
- "src/heartbeat.ts"() {
3117
- "use strict";
3118
- }
3119
- });
3120
-
3121
- // src/logNoise.ts
3122
- function timesWord(n) {
3123
- const tens = n % 100;
3124
- if (tens >= 11 && tens <= 14) return "\u0440\u0430\u0437";
3125
- return n % 10 >= 2 && n % 10 <= 4 ? "\u0440\u0430\u0437\u0430" : "\u0440\u0430\u0437";
3126
- }
3127
- function makeRepeatFilter(opts = {}) {
3128
- const remindAfterMs = opts.remindAfterMs ?? DEFAULT_REMIND_MS;
3129
- const streaks = /* @__PURE__ */ new Map();
3130
- return {
3131
- say(key, message, now) {
3132
- const prev = streaks.get(key);
3133
- if (!prev || prev.message !== message) {
3134
- 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)})` : "";
3135
- streaks.set(key, { message, since: now, saidAt: now, hidden: 0 });
3136
- return `${message}${tail2}`;
3137
- }
3138
- prev.hidden += 1;
3139
- if (now - prev.saidAt < remindAfterMs) return null;
3140
- const hidden = prev.hidden;
3141
- const minutes = Math.max(1, Math.round((now - prev.since) / 6e4));
3142
- prev.saidAt = now;
3143
- prev.hidden = 0;
3144
- 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`;
3145
- },
3146
- forget(key) {
3147
- streaks.delete(key);
3148
- }
3149
- };
3150
- }
3151
- var DEFAULT_REMIND_MS;
3152
- var init_logNoise = __esm({
3153
- "src/logNoise.ts"() {
3154
- "use strict";
3155
- DEFAULT_REMIND_MS = 10 * 6e4;
3156
- }
3157
- });
3158
-
3159
3095
  // src/agentToken.ts
3160
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
3161
- import { join as join9 } from "path";
3096
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
3097
+ import { join as join7 } from "path";
3162
3098
  function loadAgentToken() {
3163
3099
  try {
3164
- const s = JSON.parse(readFileSync7(STORE_PATH, "utf8"));
3100
+ const s = JSON.parse(readFileSync5(STORE_PATH, "utf8"));
3165
3101
  return s.token ? s : null;
3166
3102
  } catch {
3167
3103
  return null;
@@ -3214,7 +3150,7 @@ var init_agentToken = __esm({
3214
3150
  "src/agentToken.ts"() {
3215
3151
  "use strict";
3216
3152
  init_config();
3217
- STORE_PATH = join9(SWARM_HOME, "agent.json");
3153
+ STORE_PATH = join7(SWARM_HOME, "agent.json");
3218
3154
  base = (api) => api.replace(/\/$/, "");
3219
3155
  TOKEN_PROBE_TIMEOUT_MS = 8e3;
3220
3156
  }
@@ -3255,6 +3191,142 @@ var init_linkFailure = __esm({
3255
3191
  }
3256
3192
  });
3257
3193
 
3194
+ // src/toolFailure.ts
3195
+ function trimDetail(s) {
3196
+ const t = s.trim();
3197
+ return t.length <= FAILURE_DETAIL_MAX ? t : `${t.slice(0, FAILURE_DETAIL_MAX)}\u2026`;
3198
+ }
3199
+ function classifyToolFailure(raw) {
3200
+ if (MISMATCH_RE.test(raw)) return TOOL_FAILURE.WORKSPACE_MISMATCH;
3201
+ if (FORBIDDEN_RE.test(raw)) return TOOL_FAILURE.FORBIDDEN;
3202
+ if (NOT_FOUND_RE.test(raw)) return TOOL_FAILURE.NOT_FOUND;
3203
+ return TOOL_FAILURE.TOOL_ERROR;
3204
+ }
3205
+ function classifyThrown(e) {
3206
+ const { kind, detail } = explainLinkFailure(e);
3207
+ const code = kind === "offline" ? TOOL_FAILURE.UNREACHABLE : kind === "unauthorized" ? TOOL_FAILURE.UNAUTHORIZED : (
3208
+ // «ручки нет» и «сервер отказал по существу» разбираем доменно: там может быть
3209
+ // и task_not_found, и отказ по правам — то есть наши коды, а не транспортные.
3210
+ classifyToolFailure(detail)
3211
+ );
3212
+ return { code, detail };
3213
+ }
3214
+ function describeToolFailure(f) {
3215
+ const text = TOOL_FAILURE_TEXT[f.code];
3216
+ const detail = trimDetail(f.detail);
3217
+ return detail ? `${text} (${detail})` : text;
3218
+ }
3219
+ var TOOL_FAILURE, TOOL_FAILURE_TEXT, TOOL_FAILURE_RETRIABLE, FAILURE_DETAIL_MAX, NOT_FOUND_RE, FORBIDDEN_RE, MISMATCH_RE;
3220
+ var init_toolFailure = __esm({
3221
+ "src/toolFailure.ts"() {
3222
+ "use strict";
3223
+ init_linkFailure();
3224
+ TOOL_FAILURE = {
3225
+ /** Тул отработал, но искомого в ответе нет: задачи с таким слагом в воркспейсе не существует. */
3226
+ NOT_FOUND: "not_found",
3227
+ /** Объект есть, но токену роя не виден (приватный проект, спейс, чужой воркспейс). */
3228
+ FORBIDDEN: "forbidden",
3229
+ /** Токен роя не состоит в воркспейсе, которым искали. */
3230
+ WORKSPACE_MISMATCH: "workspace_mismatch",
3231
+ /** До прода не достучались: сеть, DNS, таймаут, оборванный сокет. */
3232
+ UNREACHABLE: "unreachable",
3233
+ /** Прод ответил, но токен роя отвергнут (отозван, протух). Сетью это не лечится. */
3234
+ UNAUTHORIZED: "unauthorized",
3235
+ /** Тул ответил ошибкой, которую мы не узнали, — текст кладём в detail как есть. */
3236
+ TOOL_ERROR: "tool_error",
3237
+ /** Успех, но полезной нагрузки в ответе нет (форма ответа разошлась с ожиданием). */
3238
+ EMPTY: "empty"
3239
+ };
3240
+ TOOL_FAILURE_TEXT = {
3241
+ [TOOL_FAILURE.NOT_FOUND]: "\u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435\u0442 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043B\u0430\u0433 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
3242
+ [TOOL_FAILURE.FORBIDDEN]: "\u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0435\u0442: \u043E\u0431\u044A\u0435\u043A\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0440\u043E\u044F \u043D\u0435 \u0432\u0438\u0434\u0435\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
3243
+ [TOOL_FAILURE.WORKSPACE_MISMATCH]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435",
3244
+ [TOOL_FAILURE.UNREACHABLE]: "Taskless \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u2014 \u0441\u0435\u0442\u044C, DNS \u0438\u043B\u0438 \u0442\u0430\u0439\u043C\u0430\u0443\u0442",
3245
+ [TOOL_FAILURE.UNAUTHORIZED]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442 \u043F\u0440\u043E\u0434\u043E\u043C \u2014 \u043D\u0443\u0436\u0435\u043D swarm login \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435",
3246
+ [TOOL_FAILURE.TOOL_ERROR]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0448\u0438\u0431\u043A\u043E\u0439",
3247
+ [TOOL_FAILURE.EMPTY]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0443\u0441\u043F\u0435\u0445\u043E\u043C, \u043D\u043E \u0434\u0430\u043D\u043D\u044B\u0445 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442"
3248
+ };
3249
+ TOOL_FAILURE_RETRIABLE = {
3250
+ [TOOL_FAILURE.NOT_FOUND]: false,
3251
+ [TOOL_FAILURE.FORBIDDEN]: false,
3252
+ [TOOL_FAILURE.WORKSPACE_MISMATCH]: false,
3253
+ [TOOL_FAILURE.UNREACHABLE]: true,
3254
+ // Отозванный токен повтором не чинится — его меняет человек (`swarm login`).
3255
+ [TOOL_FAILURE.UNAUTHORIZED]: false,
3256
+ [TOOL_FAILURE.TOOL_ERROR]: false,
3257
+ [TOOL_FAILURE.EMPTY]: false
3258
+ };
3259
+ FAILURE_DETAIL_MAX = 300;
3260
+ NOT_FOUND_RE = /task_not_found|not_found|не найдена|не найден/i;
3261
+ FORBIDDEN_RE = /forbidden|not visible|no access|нет доступа|(?:http|status|code)[ =:]*403\b|\(403\)|403 forbidden/i;
3262
+ MISMATCH_RE = /workspace_mismatch|not a member of that workspace/i;
3263
+ }
3264
+ });
3265
+
3266
+ // src/heartbeat.ts
3267
+ import { readFileSync as readFileSync7, renameSync as renameSync4, writeFileSync as writeFileSync7 } from "fs";
3268
+ function writeHeartbeat(path, now) {
3269
+ const tmp = `${path}.tmp`;
3270
+ writeFileSync7(tmp, String(now));
3271
+ renameSync4(tmp, path);
3272
+ }
3273
+ function readHeartbeat(path) {
3274
+ try {
3275
+ const ms = Number(readFileSync7(path, "utf8").trim());
3276
+ return Number.isFinite(ms) ? ms : null;
3277
+ } catch {
3278
+ return null;
3279
+ }
3280
+ }
3281
+ function overmindAlive(path, now, staleMs) {
3282
+ const ts = readHeartbeat(path);
3283
+ if (ts == null) return false;
3284
+ return now - ts < staleMs;
3285
+ }
3286
+ var init_heartbeat = __esm({
3287
+ "src/heartbeat.ts"() {
3288
+ "use strict";
3289
+ }
3290
+ });
3291
+
3292
+ // src/logNoise.ts
3293
+ function timesWord(n) {
3294
+ const tens = n % 100;
3295
+ if (tens >= 11 && tens <= 14) return "\u0440\u0430\u0437";
3296
+ return n % 10 >= 2 && n % 10 <= 4 ? "\u0440\u0430\u0437\u0430" : "\u0440\u0430\u0437";
3297
+ }
3298
+ function makeRepeatFilter(opts = {}) {
3299
+ const remindAfterMs = opts.remindAfterMs ?? DEFAULT_REMIND_MS;
3300
+ const streaks = /* @__PURE__ */ new Map();
3301
+ return {
3302
+ say(key, message, now) {
3303
+ const prev = streaks.get(key);
3304
+ if (!prev || prev.message !== message) {
3305
+ 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)})` : "";
3306
+ streaks.set(key, { message, since: now, saidAt: now, hidden: 0 });
3307
+ return `${message}${tail2}`;
3308
+ }
3309
+ prev.hidden += 1;
3310
+ if (now - prev.saidAt < remindAfterMs) return null;
3311
+ const hidden = prev.hidden;
3312
+ const minutes = Math.max(1, Math.round((now - prev.since) / 6e4));
3313
+ prev.saidAt = now;
3314
+ prev.hidden = 0;
3315
+ 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`;
3316
+ },
3317
+ forget(key) {
3318
+ streaks.delete(key);
3319
+ }
3320
+ };
3321
+ }
3322
+ var DEFAULT_REMIND_MS;
3323
+ var init_logNoise = __esm({
3324
+ "src/logNoise.ts"() {
3325
+ "use strict";
3326
+ DEFAULT_REMIND_MS = 10 * 6e4;
3327
+ }
3328
+ });
3329
+
3258
3330
  // src/procList.ts
3259
3331
  function parsePsRows(out) {
3260
3332
  const rows = [];
@@ -4904,78 +4976,6 @@ var init_oauth = __esm({
4904
4976
  }
4905
4977
  });
4906
4978
 
4907
- // src/toolFailure.ts
4908
- function trimDetail(s) {
4909
- const t = s.trim();
4910
- return t.length <= FAILURE_DETAIL_MAX ? t : `${t.slice(0, FAILURE_DETAIL_MAX)}\u2026`;
4911
- }
4912
- function classifyToolFailure(raw) {
4913
- if (MISMATCH_RE.test(raw)) return TOOL_FAILURE.WORKSPACE_MISMATCH;
4914
- if (FORBIDDEN_RE.test(raw)) return TOOL_FAILURE.FORBIDDEN;
4915
- if (NOT_FOUND_RE.test(raw)) return TOOL_FAILURE.NOT_FOUND;
4916
- return TOOL_FAILURE.TOOL_ERROR;
4917
- }
4918
- function classifyThrown(e) {
4919
- const { kind, detail } = explainLinkFailure(e);
4920
- const code = kind === "offline" ? TOOL_FAILURE.UNREACHABLE : kind === "unauthorized" ? TOOL_FAILURE.UNAUTHORIZED : (
4921
- // «ручки нет» и «сервер отказал по существу» разбираем доменно: там может быть
4922
- // и task_not_found, и отказ по правам — то есть наши коды, а не транспортные.
4923
- classifyToolFailure(detail)
4924
- );
4925
- return { code, detail };
4926
- }
4927
- function describeToolFailure(f) {
4928
- const text = TOOL_FAILURE_TEXT[f.code];
4929
- const detail = trimDetail(f.detail);
4930
- return detail ? `${text} (${detail})` : text;
4931
- }
4932
- var TOOL_FAILURE, TOOL_FAILURE_TEXT, TOOL_FAILURE_RETRIABLE, FAILURE_DETAIL_MAX, NOT_FOUND_RE, FORBIDDEN_RE, MISMATCH_RE;
4933
- var init_toolFailure = __esm({
4934
- "src/toolFailure.ts"() {
4935
- "use strict";
4936
- init_linkFailure();
4937
- TOOL_FAILURE = {
4938
- /** Тул отработал, но искомого в ответе нет: задачи с таким слагом в воркспейсе не существует. */
4939
- NOT_FOUND: "not_found",
4940
- /** Объект есть, но токену роя не виден (приватный проект, спейс, чужой воркспейс). */
4941
- FORBIDDEN: "forbidden",
4942
- /** Токен роя не состоит в воркспейсе, которым искали. */
4943
- WORKSPACE_MISMATCH: "workspace_mismatch",
4944
- /** До прода не достучались: сеть, DNS, таймаут, оборванный сокет. */
4945
- UNREACHABLE: "unreachable",
4946
- /** Прод ответил, но токен роя отвергнут (отозван, протух). Сетью это не лечится. */
4947
- UNAUTHORIZED: "unauthorized",
4948
- /** Тул ответил ошибкой, которую мы не узнали, — текст кладём в detail как есть. */
4949
- TOOL_ERROR: "tool_error",
4950
- /** Успех, но полезной нагрузки в ответе нет (форма ответа разошлась с ожиданием). */
4951
- EMPTY: "empty"
4952
- };
4953
- TOOL_FAILURE_TEXT = {
4954
- [TOOL_FAILURE.NOT_FOUND]: "\u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435 \u0442\u0430\u043A\u043E\u0433\u043E \u043D\u0435\u0442 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0441\u043B\u0430\u0433 \u0438 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
4955
- [TOOL_FAILURE.FORBIDDEN]: "\u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0435\u0442: \u043E\u0431\u044A\u0435\u043A\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442, \u043D\u043E \u0442\u043E\u043A\u0435\u043D\u0443 \u0440\u043E\u044F \u043D\u0435 \u0432\u0438\u0434\u0435\u043D \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A",
4956
- [TOOL_FAILURE.WORKSPACE_MISMATCH]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u0438\u0442 \u0432 \u044D\u0442\u043E\u043C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0435",
4957
- [TOOL_FAILURE.UNREACHABLE]: "Taskless \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u2014 \u0441\u0435\u0442\u044C, DNS \u0438\u043B\u0438 \u0442\u0430\u0439\u043C\u0430\u0443\u0442",
4958
- [TOOL_FAILURE.UNAUTHORIZED]: "\u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u0432\u0435\u0440\u0433\u043D\u0443\u0442 \u043F\u0440\u043E\u0434\u043E\u043C \u2014 \u043D\u0443\u0436\u0435\u043D swarm login \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435",
4959
- [TOOL_FAILURE.TOOL_ERROR]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u043E\u0448\u0438\u0431\u043A\u043E\u0439",
4960
- [TOOL_FAILURE.EMPTY]: "\u0442\u0443\u043B \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0443\u0441\u043F\u0435\u0445\u043E\u043C, \u043D\u043E \u0434\u0430\u043D\u043D\u044B\u0445 \u0432 \u043E\u0442\u0432\u0435\u0442\u0435 \u043D\u0435\u0442"
4961
- };
4962
- TOOL_FAILURE_RETRIABLE = {
4963
- [TOOL_FAILURE.NOT_FOUND]: false,
4964
- [TOOL_FAILURE.FORBIDDEN]: false,
4965
- [TOOL_FAILURE.WORKSPACE_MISMATCH]: false,
4966
- [TOOL_FAILURE.UNREACHABLE]: true,
4967
- // Отозванный токен повтором не чинится — его меняет человек (`swarm login`).
4968
- [TOOL_FAILURE.UNAUTHORIZED]: false,
4969
- [TOOL_FAILURE.TOOL_ERROR]: false,
4970
- [TOOL_FAILURE.EMPTY]: false
4971
- };
4972
- FAILURE_DETAIL_MAX = 300;
4973
- NOT_FOUND_RE = /task_not_found|not_found|не найдена|не найден/i;
4974
- FORBIDDEN_RE = /forbidden|not visible|no access|нет доступа|(?:http|status|code)[ =:]*403\b|\(403\)|403 forbidden/i;
4975
- MISMATCH_RE = /workspace_mismatch|not a member of that workspace/i;
4976
- }
4977
- });
4978
-
4979
4979
  // src/authState.ts
4980
4980
  function noteTokenRejected(nowMs = Date.now()) {
4981
4981
  if (!denial) {
@@ -6689,7 +6689,7 @@ init_config();
6689
6689
  init_resources();
6690
6690
  init_names2();
6691
6691
  init_storm();
6692
- import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
6692
+ import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
6693
6693
  import { dirname as dirname2 } from "path";
6694
6694
  import { randomUUID } from "crypto";
6695
6695
 
@@ -6741,13 +6741,16 @@ function renderPendingRulesBlock(rules) {
6741
6741
  ].join("\n");
6742
6742
  }
6743
6743
 
6744
+ // src/swarmClient.ts
6745
+ init_toolFailure();
6746
+
6744
6747
  // src/logTail.ts
6745
6748
  import { closeSync, existsSync as existsSync6, openSync, readSync, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
6746
- import { join as join8 } from "path";
6749
+ import { join as join9 } from "path";
6747
6750
 
6748
6751
  // src/runLogDir.ts
6749
6752
  import { existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync4, renameSync as renameSync3, statSync as statSync3 } from "fs";
6750
- import { join as join7 } from "path";
6753
+ import { join as join8 } from "path";
6751
6754
  var LEGACY_RUN_LOGS_SUBDIR = "legacy";
6752
6755
  function runLogDirName(runLogKey) {
6753
6756
  const safe2 = runLogKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
@@ -6755,7 +6758,7 @@ function runLogDirName(runLogKey) {
6755
6758
  return /^\d+$/.test(safe2) ? `k-${safe2}` : safe2;
6756
6759
  }
6757
6760
  function runLogDirPath(runLogsDir, runLogKey) {
6758
- return join7(runLogsDir, runLogDirName(runLogKey));
6761
+ return join8(runLogsDir, runLogDirName(runLogKey));
6759
6762
  }
6760
6763
  function findRunLogDir(runLogsDir, keys) {
6761
6764
  for (const key of keys) {
@@ -6770,7 +6773,7 @@ function runLogDirForWrite(runLogsDir, keys) {
6770
6773
  return findRunLogDir(runLogsDir, keys) ?? runLogDirPath(runLogsDir, canonical);
6771
6774
  }
6772
6775
  function sweepLegacyRunLogs(runLogsDir) {
6773
- const legacyDir = join7(runLogsDir, LEGACY_RUN_LOGS_SUBDIR);
6776
+ const legacyDir = join8(runLogsDir, LEGACY_RUN_LOGS_SUBDIR);
6774
6777
  let entries;
6775
6778
  try {
6776
6779
  entries = readdirSync4(runLogsDir);
@@ -6781,12 +6784,12 @@ function sweepLegacyRunLogs(runLogsDir) {
6781
6784
  let failed = 0;
6782
6785
  for (const name of entries) {
6783
6786
  if (!/^\d+$/.test(name)) continue;
6784
- const from = join7(runLogsDir, name);
6787
+ const from = join8(runLogsDir, name);
6785
6788
  try {
6786
6789
  if (!statSync3(from).isDirectory()) continue;
6787
6790
  mkdirSync5(legacyDir, { recursive: true });
6788
- let to = join7(legacyDir, name);
6789
- for (let n = 2; existsSync5(to) && n < 100; n++) to = join7(legacyDir, `${name}-${n}`);
6791
+ let to = join8(legacyDir, name);
6792
+ for (let n = 2; existsSync5(to) && n < 100; n++) to = join8(legacyDir, `${name}-${n}`);
6790
6793
  renameSync3(from, to);
6791
6794
  moved++;
6792
6795
  } catch {
@@ -6852,8 +6855,8 @@ function latestRunLaunch(runLogsDir, runLogKeys) {
6852
6855
  }
6853
6856
  if (indexes.size === 0) return null;
6854
6857
  const index = Math.max(...indexes);
6855
- const out = [join8(dir, `${index}.json`), join8(dir, `${index}.json.done`)].find((p) => existsSync6(p)) ?? null;
6856
- const err = existsSync6(join8(dir, `${index}.err.log`)) ? join8(dir, `${index}.err.log`) : null;
6858
+ const out = [join9(dir, `${index}.json`), join9(dir, `${index}.json.done`)].find((p) => existsSync6(p)) ?? null;
6859
+ const err = existsSync6(join9(dir, `${index}.err.log`)) ? join9(dir, `${index}.err.log`) : null;
6857
6860
  return { index, total: indexes.size, out, err };
6858
6861
  }
6859
6862
  function runLogTail(runLogsDir, runLogKeys, opts) {
@@ -6940,6 +6943,14 @@ function toolErrorText(res) {
6940
6943
  if (!text) return "\u043F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0440\u0433 \u0432\u044B\u0437\u043E\u0432 \u0431\u0435\u0437 \u043E\u0431\u044A\u044F\u0441\u043D\u0435\u043D\u0438\u044F";
6941
6944
  return text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
6942
6945
  }
6946
+ var DEPS_UNRETRIABLE_CODES = /* @__PURE__ */ new Set([
6947
+ TOOL_FAILURE.FORBIDDEN,
6948
+ TOOL_FAILURE.UNAUTHORIZED,
6949
+ TOOL_FAILURE.WORKSPACE_MISMATCH
6950
+ ]);
6951
+ function depsFailureRetriable(code) {
6952
+ return !DEPS_UNRETRIABLE_CODES.has(code);
6953
+ }
6943
6954
  function runRef(runKey) {
6944
6955
  return { __runRef: runKey };
6945
6956
  }
@@ -7036,6 +7047,13 @@ var SwarmClient = class _SwarmClient {
7036
7047
  // дальше удвоением до потолка TTL: устойчиво отвергающий прод (старее демона, скоуп
7037
7048
  // токена) не должен получать вопрос каждые четыре секунды до конца своей жизни.
7038
7049
  static DEPS_RETRY_BASE_MS = 5e3;
7050
+ // TASWM-517: сколько раз спрашиваем НЕСАМОЛЕЧАЩИЙСЯ отказ, прежде чем перестать вовсе.
7051
+ // `agent_forbidden` («тул недоступен аккаунту-агенту роя») повтором не чинится: 26.08
7052
+ // два прогона получили его по 53 раза каждый и остались в очереди навсегда. Три захода
7053
+ // при backoff 5с/10с — это ~15 секунд: запас на случай, если классификатор ошибся и
7054
+ // отказ на деле транзиентный, но не час молчаливого простоя. По исчерпании прогон
7055
+ // роняет гейт спавна (DEPS_UNRETRIABLE_ATTEMPTS читается и там).
7056
+ static DEPS_UNRETRIABLE_ATTEMPTS = 3;
7039
7057
  // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
7040
7058
  // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
7041
7059
  // каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
@@ -7107,13 +7125,13 @@ var SwarmClient = class _SwarmClient {
7107
7125
  recover() {
7108
7126
  if (existsSync7(this.mapPath)) {
7109
7127
  try {
7110
- const obj = JSON.parse(readFileSync5(this.mapPath, "utf8"));
7128
+ const obj = JSON.parse(readFileSync6(this.mapPath, "utf8"));
7111
7129
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
7112
7130
  } catch {
7113
7131
  }
7114
7132
  }
7115
7133
  if (existsSync7(this.outboxPath)) {
7116
- const txt = readFileSync5(this.outboxPath, "utf8");
7134
+ const txt = readFileSync6(this.outboxPath, "utf8");
7117
7135
  for (const line of txt.split("\n")) {
7118
7136
  const s = line.trim();
7119
7137
  if (!s) continue;
@@ -7136,7 +7154,7 @@ var SwarmClient = class _SwarmClient {
7136
7154
  if (!existsSync7(this.stormPath)) return;
7137
7155
  let txt;
7138
7156
  try {
7139
- txt = readFileSync5(this.stormPath, "utf8");
7157
+ txt = readFileSync6(this.stormPath, "utf8");
7140
7158
  } catch {
7141
7159
  return;
7142
7160
  }
@@ -7641,11 +7659,12 @@ var SwarmClient = class _SwarmClient {
7641
7659
  res = await this.call("swarm_deps_status", { refs: ask });
7642
7660
  } catch (e) {
7643
7661
  for (const ref of ask) this.depsAskedAt.delete(ref);
7644
- this.noteDepsUnanswered(ask, nowMs, String(e));
7662
+ this.noteDepsUnanswered(ask, nowMs, String(e), depsFailureRetriable(classifyThrown(e).code));
7645
7663
  return;
7646
7664
  }
7647
7665
  if (res?.isError) {
7648
- this.noteDepsUnanswered(ask, nowMs, toolErrorText(res));
7666
+ const text = toolErrorText(res);
7667
+ this.noteDepsUnanswered(ask, nowMs, text, depsFailureRetriable(classifyToolFailure(text)));
7649
7668
  return;
7650
7669
  }
7651
7670
  const deps = res.structuredContent?.deps ?? [];
@@ -7685,6 +7704,7 @@ var SwarmClient = class _SwarmClient {
7685
7704
  depsRetryDelay(ref) {
7686
7705
  const fail = this.depsUnanswered.get(ref);
7687
7706
  if (fail) {
7707
+ if (!fail.retriable && fail.attempts >= _SwarmClient.DEPS_UNRETRIABLE_ATTEMPTS) return _SwarmClient.DEPS_TTL_MS;
7688
7708
  const backoff = _SwarmClient.DEPS_RETRY_BASE_MS * 2 ** Math.min(fail.attempts - 1, 10);
7689
7709
  return Math.min(backoff, _SwarmClient.DEPS_TTL_MS);
7690
7710
  }
@@ -7693,17 +7713,19 @@ var SwarmClient = class _SwarmClient {
7693
7713
  return _SwarmClient.DEPS_TTL_MS;
7694
7714
  }
7695
7715
  /** Запомнить, что прод по этим ссылкам не ответил (см. depsUnanswered / TASWM-486). */
7696
- noteDepsUnanswered(refs, nowMs, error) {
7716
+ noteDepsUnanswered(refs, nowMs, error, retriable = true) {
7697
7717
  for (const ref of refs) {
7698
7718
  const prev = this.depsUnanswered.get(ref);
7719
+ const sameKind = prev != null && prev.retriable === retriable;
7699
7720
  this.depsUnanswered.set(ref, {
7700
7721
  ref,
7701
7722
  slug: this.depSlug.get(ref) ?? ref,
7702
7723
  // `since` — начало ЭПИЗОДА слепоты, а не последняя попытка: именно возраст эпизода
7703
7724
  // отвечает на вопрос «сколько прогон уже стоит вслепую».
7704
7725
  since: prev?.since ?? nowMs,
7705
- attempts: (prev?.attempts ?? 0) + 1,
7706
- error
7726
+ attempts: sameKind ? prev.attempts + 1 : 1,
7727
+ error,
7728
+ retriable
7707
7729
  });
7708
7730
  }
7709
7731
  }
@@ -9247,13 +9269,13 @@ var SwarmClient = class _SwarmClient {
9247
9269
  }
9248
9270
  persistPending() {
9249
9271
  try {
9250
- writeFileSync5(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
9272
+ writeFileSync6(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
9251
9273
  } catch {
9252
9274
  }
9253
9275
  }
9254
9276
  persistMap() {
9255
9277
  try {
9256
- writeFileSync5(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
9278
+ writeFileSync6(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
9257
9279
  } catch {
9258
9280
  }
9259
9281
  }
@@ -9323,7 +9345,7 @@ function tickPulse(health, now, intervalMs) {
9323
9345
  }
9324
9346
 
9325
9347
  // src/logRotate.ts
9326
- import { copyFileSync as copyFileSync2, renameSync as renameSync5, statSync as statSync5, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync8 } from "fs";
9348
+ import { copyFileSync as copyFileSync2, renameSync as renameSync5, statSync as statSync5, fstatSync, truncateSync, unlinkSync as unlinkSync3, existsSync as existsSync8 } from "fs";
9327
9349
  var realIo = {
9328
9350
  size: (path) => {
9329
9351
  try {
@@ -9335,7 +9357,7 @@ var realIo = {
9335
9357
  copy: copyFileSync2,
9336
9358
  rename: renameSync5,
9337
9359
  truncate: (path) => truncateSync(path, 0),
9338
- unlink: unlinkSync2,
9360
+ unlink: unlinkSync3,
9339
9361
  exists: existsSync8
9340
9362
  };
9341
9363
  var realOwnerIo = {
@@ -14204,7 +14226,7 @@ import { serve as serve2 } from "@hono/node-server";
14204
14226
  // package.json
14205
14227
  var package_default = {
14206
14228
  name: "@taskless-app/swarm",
14207
- version: "0.6.27",
14229
+ version: "0.6.29",
14208
14230
  type: "module",
14209
14231
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
14210
14232
  keywords: [
@@ -14255,7 +14277,7 @@ var package_default = {
14255
14277
  execa: "^10.0.0",
14256
14278
  zod: "3.25.76"
14257
14279
  },
14258
- gitHead: "2e1ed83014422ae577be60271adc79e291b9e2ab"
14280
+ gitHead: "d8072f68198b68b4252687b01e7e82e2b4769346"
14259
14281
  };
14260
14282
 
14261
14283
  // src/version.ts
@@ -18411,6 +18433,15 @@ async function trySpawnQueuedRun(deps, run) {
18411
18433
  if (unmet.length) {
18412
18434
  const lost = unmet.filter((d) => d.outcome === "unknown");
18413
18435
  const blind = deps.db.depsUnconfirmed?.(run.depends_on) ?? [];
18436
+ const stuck = blind.find((d) => !d.retriable && d.attempts >= SwarmClient.DEPS_UNRETRIABLE_ATTEMPTS);
18437
+ if (stuck) {
18438
+ return await failBeforeStart(
18439
+ deps,
18440
+ run,
18441
+ `\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C ${stuck.slug} \u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C: \u043F\u0440\u043E\u0434 \u043E\u0442\u0432\u0435\u0440\u0433\u0430\u0435\u0442 swarm_deps_status \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u044D\u0442\u043E \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F (${stuck.attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A), \u043E\u0442\u0432\u0435\u0442 \u2014 ${stuck.error}). \u0412\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u043D\u0435 \u0436\u0434\u0443 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 (TASWM-517)`,
18442
+ { retriable: false }
18443
+ );
18444
+ }
18414
18445
  const why = lost.length ? `\u0436\u0434\u0451\u0442 \u0431\u043B\u043E\u043A\u0435\u0440\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u041D\u0415\u0422: ${lost.map((d) => d.slug).join(", ")} \u2014 \u043F\u043E \u044D\u0442\u043E\u0439 \u0441\u0441\u044B\u043B\u043A\u0435 \u043D\u0435\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430; \u0441\u0430\u043C \u043D\u0435 \u043F\u043E\u0435\u0434\u0435\u0442, \u0441\u043D\u0438\u043C\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C (swarm_run_upsert{dependsOn:[]}) \u0438\u043B\u0438 \u0437\u0430\u0432\u0435\u0434\u0438 \u0431\u043B\u043E\u043A\u0435\u0440 \u0437\u0430\u043D\u043E\u0432\u043E (TASWM-399)` : blind.length ? `\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 ${blind.map((d) => d.slug).join(", ")}, \u043D\u043E \u041F\u0420\u041E\u0414 \u0415\u0401 \u041D\u0415 \u041F\u041E\u0414\u0422\u0412\u0415\u0420\u0416\u0414\u0410\u0415\u0422: ${blind[0].attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) swarm_deps_status, \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u043E\u0448\u0438\u0431\u043A\u0430 \u2014 ${blind[0].error}. \u041F\u043E\u043A\u0430 \u043F\u0440\u043E\u0434 \u043C\u043E\u043B\u0447\u0438\u0442, \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438 \u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0430\u043C \u043D\u0435 \u043F\u043E\u0435\u0434\u0435\u0442 (TASWM-486)` : `\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438: ${unmet.map((d) => d.slug).join(", ")}`;
18415
18446
  return deferSpawn(deps, run, why);
18416
18447
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.27",
3
+ "version": "0.6.29",
4
4
  "type": "module",
5
5
  "description": "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "execa": "^10.0.0",
40
40
  "zod": "3.25.76"
41
41
  },
42
- "gitHead": "2e1ed83014422ae577be60271adc79e291b9e2ab",
42
+ "gitHead": "d8072f68198b68b4252687b01e7e82e2b4769346",
43
43
  "scripts": {
44
44
  "overmind": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/overmind.ts",
45
45
  "swarm": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/cli.ts",