@taskless-app/swarm 0.6.2 → 0.6.3

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/README.md CHANGED
@@ -211,6 +211,24 @@ deny-правил на файловые тулы у него нет — дрон
211
211
  (`src/executors/index.ts`). Если для него пришлось тронуть общий код — значит,
212
212
  чинить надо интерфейс, а не общий код; на это и стоит `contract.test.ts`.
213
213
 
214
+ ## Служебные файлы роя в worktree
215
+
216
+ Кое-что рой кладёт прямо в рабочее дерево дрона: `.swarm-mcp.json` (конфиг
217
+ MCP-сервера роя, в нём **токен прогона**) и `.swarm-task/` (вложения задачи).
218
+ Ваш `.gitignore` про них знать не обязан — это файлы роя, а не проекта, и рой
219
+ прячет их сам, на каждом подготовленном worktree (TASWM-372):
220
+
221
+ - паттерны уезжают в `.git/info/exclude` — локальный exclude вашего клона, файлы
222
+ в репозиторий не коммитятся, а сам репозиторий при этом не тронут;
223
+ - `pre-commit`/`pre-push` рвут операцию, если служебный файл всё-таки оказался в
224
+ индексе (`git add -f`, файл приехал закоммиченным с ветки). Хуки ставятся
225
+ строго пер-worktree (`git config --worktree core.hooksPath`, каталог —
226
+ `.git/worktrees/<id>/swarm-hooks`) и в конце **вызывают ваш прежний хук**:
227
+ husky и прочая дисциплина репозитория продолжают работать.
228
+
229
+ Список файлов — один, `src/swarmFiles.ts`; оттуда же берут имена те, кто их
230
+ пишет. Кладёте в worktree что-то новое — впишите туда, и оно спрячется само.
231
+
214
232
  ## Windows
215
233
 
216
234
  Рой работает на Windows, но **рекомендуемый способ — WSL2**: там он ведёт себя ровно
package/dist/cli.js CHANGED
@@ -1156,6 +1156,17 @@ var init_claudeBin = __esm({
1156
1156
  }
1157
1157
  });
1158
1158
 
1159
+ // src/swarmFiles.ts
1160
+ var SWARM_MCP_CONFIG_FILE, SWARM_TASK_DIR, SWARM_EXCLUDE_PATTERNS;
1161
+ var init_swarmFiles = __esm({
1162
+ "src/swarmFiles.ts"() {
1163
+ "use strict";
1164
+ SWARM_MCP_CONFIG_FILE = ".swarm-mcp.json";
1165
+ SWARM_TASK_DIR = ".swarm-task";
1166
+ SWARM_EXCLUDE_PATTERNS = [`/${SWARM_MCP_CONFIG_FILE}`, `/${SWARM_TASK_DIR}/`];
1167
+ }
1168
+ });
1169
+
1159
1170
  // src/pricing.ts
1160
1171
  function costFromUsage(usage2, model, table = MODEL_PRICING) {
1161
1172
  const pricing = model ? table[model] : void 0;
@@ -1294,9 +1305,10 @@ var init_claudeCode = __esm({
1294
1305
  "src/executors/claudeCode.ts"() {
1295
1306
  "use strict";
1296
1307
  init_claudeBin();
1308
+ init_swarmFiles();
1297
1309
  init_claudeResult();
1298
1310
  ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
1299
- MCP_CONFIG_FILE = ".swarm-mcp.json";
1311
+ MCP_CONFIG_FILE = SWARM_MCP_CONFIG_FILE;
1300
1312
  claudeCodeExecutor = {
1301
1313
  id: "claude-code",
1302
1314
  title: "Claude Code (claude)",
@@ -1837,7 +1849,7 @@ var init_package = __esm({
1837
1849
  "package.json"() {
1838
1850
  package_default = {
1839
1851
  name: "@taskless-app/swarm",
1840
- version: "0.6.2",
1852
+ version: "0.6.3",
1841
1853
  type: "module",
1842
1854
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
1843
1855
  keywords: [
@@ -1887,7 +1899,7 @@ var init_package = __esm({
1887
1899
  execa: "^10.0.0",
1888
1900
  zod: "3.25.76"
1889
1901
  },
1890
- gitHead: "e812f35a996b00111f3f535b6b0a45b379e7afb2"
1902
+ gitHead: "298573d4b361b8fc7ea4d6ea9d1e8784906bbd7e"
1891
1903
  };
1892
1904
  }
1893
1905
  });
@@ -5293,6 +5305,26 @@ var SwarmClient = class _SwarmClient {
5293
5305
  const v = sc.config?.exclusive;
5294
5306
  return typeof v === "boolean" ? v : null;
5295
5307
  }
5308
+ /**
5309
+ * Live КВОТА «дронов на проект» из Taskless (TASWM-365): тот же swarm_repo_config по
5310
+ * repoFullName — Project.swarmConfig.maxDrones. Сколько прогонов этого проекта могут идти
5311
+ * одновременно; 0 — без ограничения (дефолт проекта). Отдаём ЧИСЛО КАК ЕСТЬ, включая ноль:
5312
+ * превращать его в «без ограничения» — дело ридера (makeDroneCapReader), клиент здесь только
5313
+ * достаёт настройку. null (прод недоступен / repo не смэплен / поля нет / мусор) → ридер
5314
+ * возьмёт «без ограничения», то есть прежнее поведение.
5315
+ */
5316
+ async repoDroneCap(repoFullName) {
5317
+ let res;
5318
+ try {
5319
+ res = await this.call("swarm_repo_config", { repoFullName });
5320
+ } catch {
5321
+ return null;
5322
+ }
5323
+ if (res?.isError) return null;
5324
+ const sc = res.structuredContent ?? {};
5325
+ const v = sc.config?.maxDrones;
5326
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
5327
+ }
5296
5328
  /**
5297
5329
  * TASWM-291: МАШИНЫ роя владельца (swarm_list_hosts) — чтобы `swarm spawn --host
5298
5330
  * <имя>` резолвил человеческое имя в hostId, а `swarm hosts` показывал, какие
@@ -6404,7 +6436,7 @@ function hostIdentityPath(home) {
6404
6436
  function loadHostIdentity(deps) {
6405
6437
  const exists = deps.exists ?? existsSync6;
6406
6438
  const readFile2 = deps.readFile ?? ((p) => readFileSync7(p, "utf8"));
6407
- const writeFile2 = deps.writeFile ?? ((p, data) => {
6439
+ const writeFile3 = deps.writeFile ?? ((p, data) => {
6408
6440
  writeFileSync5(`${p}.tmp`, data);
6409
6441
  renameSync4(`${p}.tmp`, p);
6410
6442
  });
@@ -6424,7 +6456,7 @@ function loadHostIdentity(deps) {
6424
6456
  }
6425
6457
  const hostId = newId();
6426
6458
  try {
6427
- writeFile2(path, JSON.stringify({ hostId, hostName, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
6459
+ writeFile3(path, JSON.stringify({ hostId, hostName, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
6428
6460
  } catch {
6429
6461
  console.warn(`[overmind] \u043D\u0435 \u0441\u043C\u043E\u0433 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C ${path} \u2014 hostId \u0436\u0438\u0432\u0451\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430`);
6430
6462
  }
@@ -8772,9 +8804,10 @@ init_droneChannel();
8772
8804
 
8773
8805
  // src/git.ts
8774
8806
  import { existsSync as existsSync8, readdirSync as readdirSync8, statSync as statSync8 } from "fs";
8775
- import { appendFile, mkdir, readFile, rm } from "fs/promises";
8776
- import { isAbsolute as isAbsolute2, join as join13 } from "path";
8807
+ import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
8808
+ import { isAbsolute as isAbsolute2, join as join13, sep as sep2 } from "path";
8777
8809
  import { $ as $5 } from "execa";
8810
+ init_swarmFiles();
8778
8811
  function isLinkedWorktree(dir) {
8779
8812
  try {
8780
8813
  return statSync8(join13(dir, ".git")).isFile();
@@ -8825,8 +8858,10 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8825
8858
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
8826
8859
 
8827
8860
  // src/attachments.ts
8828
- import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
8861
+ import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
8829
8862
  import { join as join15 } from "path";
8863
+ init_swarmFiles();
8864
+ var ATTACHMENTS_DIR = `${SWARM_TASK_DIR}/attachments`;
8830
8865
  var ATTACH_MAX_TOTAL_BYTES = 24 * 1024 * 1024;
8831
8866
  var ATTACH_MAX_FILE_BYTES = 8 * 1024 * 1024;
8832
8867
  var IMAGE_EXT = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "avif", "heic"]);
@@ -9239,11 +9274,11 @@ function savePauseState(home, state, io = {}) {
9239
9274
  remove(path);
9240
9275
  return;
9241
9276
  }
9242
- const writeFile2 = io.writeFile ?? ((p, data) => {
9277
+ const writeFile3 = io.writeFile ?? ((p, data) => {
9243
9278
  writeFileSync10(`${p}.tmp`, data);
9244
9279
  renameSync5(`${p}.tmp`, p);
9245
9280
  });
9246
- writeFile2(path, JSON.stringify(state, null, 2));
9281
+ writeFile3(path, JSON.stringify(state, null, 2));
9247
9282
  }
9248
9283
  function makePauseState(source, reason, now = Date.now()) {
9249
9284
  return {
package/dist/overmind.js CHANGED
@@ -1325,6 +1325,54 @@ var init_claudeBin = __esm({
1325
1325
  }
1326
1326
  });
1327
1327
 
1328
+ // src/swarmFiles.ts
1329
+ function sq(s) {
1330
+ return `'${s.replace(/'/g, `'\\''`)}'`;
1331
+ }
1332
+ function guardHookScript(hook, delegate) {
1333
+ const paths = SWARM_WORKTREE_ARTIFACTS.map(sq).join(" ");
1334
+ const lines = [
1335
+ "#!/usr/bin/env sh",
1336
+ `# \u0421\u0442\u0440\u0430\u0445\u043E\u0432\u043A\u0430 \u0440\u043E\u044F (TASWM-372): \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B \u0440\u043E\u044F \u043D\u0435 \u0443\u0435\u0437\u0436\u0430\u044E\u0442 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.`,
1337
+ "# \u0425\u0443\u043A \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u041F\u0415\u0420-WORKTREE (git config --worktree core.hooksPath) \u043F\u0440\u0438 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0435",
1338
+ "# worktree \u0434\u0440\u043E\u043D\u0430; \u0445\u0443\u043A\u0438 \u0441\u0430\u043C\u043E\u0433\u043E \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u043D\u0435 \u0442\u0440\u043E\u043D\u0443\u0442\u044B \u2014 \u043F\u0440\u0435\u0436\u043D\u0438\u0439 \u0432\u044B\u0437\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u043D\u0438\u0436\u0435.",
1339
+ `swarm_found=$(git ls-files -- ${paths} 2>/dev/null)`,
1340
+ `if [ -n "$swarm_found" ]; then`,
1341
+ ` echo "\u2716 \u0421\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B \u0420\u041E\u042F \u0432 \u0438\u043D\u0434\u0435\u043A\u0441\u0435 git \u2014 ${ACTION[hook]} (TASWM-372):" >&2`,
1342
+ ` printf '%s\\n' "$swarm_found" | sed 's/^/ /' >&2`,
1343
+ ` echo " \u042D\u0442\u043E \u0444\u0430\u0439\u043B\u044B \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0440\u043E\u044F, \u0430 \u043D\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430: \u0432 ${SWARM_MCP_CONFIG_FILE} \u043B\u0435\u0436\u0438\u0442 \u0442\u043E\u043A\u0435\u043D," >&2`,
1344
+ ` echo " \u043A\u043E\u0442\u043E\u0440\u043E\u043C\u0443 \u043D\u0435\u043B\u044C\u0437\u044F \u043E\u043A\u0430\u0437\u0430\u0442\u044C\u0441\u044F \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 (\u0438\u043D\u0446\u0438\u0434\u0435\u043D\u0442 11.08)." >&2`,
1345
+ ` echo " \u0423\u0431\u0435\u0440\u0438 \u0438\u0445 \u0438\u0437 \u0438\u043D\u0434\u0435\u043A\u0441\u0430 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438:" >&2`,
1346
+ ` echo " git rm --cached -r --ignore-unmatch ${SWARM_WORKTREE_ARTIFACTS.join(" ")}" >&2`,
1347
+ ` echo " \u0424\u0430\u0439\u043B\u044B \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 \u043E\u0441\u0442\u0430\u043D\u0443\u0442\u0441\u044F \u2014 \u043E\u043D\u0438 \u043D\u0443\u0436\u043D\u044B \u043F\u0440\u043E\u0433\u043E\u043D\u0443." >&2`,
1348
+ " exit 1",
1349
+ "fi"
1350
+ ];
1351
+ if (delegate) {
1352
+ lines.push(
1353
+ "# \u041F\u0440\u0435\u0436\u043D\u0438\u0439 \u0445\u0443\u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (husky \u0438 \u0442.\u043F.) \u2014 \u043E\u043D \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u043F\u0440\u043E\u043F\u0430\u0441\u0442\u044C \u0438\u0437-\u0437\u0430 \u043D\u0430\u0448\u0435\u0439 \u0441\u0442\u0440\u0430\u0445\u043E\u0432\u043A\u0438.",
1354
+ `if [ -x ${sq(delegate)} ]; then exec ${sq(delegate)} "$@"; fi`
1355
+ );
1356
+ }
1357
+ lines.push("exit 0", "");
1358
+ return lines.join("\n");
1359
+ }
1360
+ var SWARM_MCP_CONFIG_FILE, SWARM_TASK_DIR, SWARM_WORKTREE_ARTIFACTS, SWARM_EXCLUDE_PATTERNS, GUARD_HOOKS, ACTION;
1361
+ var init_swarmFiles = __esm({
1362
+ "src/swarmFiles.ts"() {
1363
+ "use strict";
1364
+ SWARM_MCP_CONFIG_FILE = ".swarm-mcp.json";
1365
+ SWARM_TASK_DIR = ".swarm-task";
1366
+ SWARM_WORKTREE_ARTIFACTS = [SWARM_MCP_CONFIG_FILE, SWARM_TASK_DIR];
1367
+ SWARM_EXCLUDE_PATTERNS = [`/${SWARM_MCP_CONFIG_FILE}`, `/${SWARM_TASK_DIR}/`];
1368
+ GUARD_HOOKS = ["pre-commit", "pre-push"];
1369
+ ACTION = {
1370
+ "pre-commit": "\u043A\u043E\u043C\u043C\u0438\u0442 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D",
1371
+ "pre-push": "\u043F\u0443\u0448 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D"
1372
+ };
1373
+ }
1374
+ });
1375
+
1328
1376
  // src/pricing.ts
1329
1377
  function costFromUsage(usage, model, table = MODEL_PRICING) {
1330
1378
  const pricing = model ? table[model] : void 0;
@@ -1463,9 +1511,10 @@ var init_claudeCode = __esm({
1463
1511
  "src/executors/claudeCode.ts"() {
1464
1512
  "use strict";
1465
1513
  init_claudeBin();
1514
+ init_swarmFiles();
1466
1515
  init_claudeResult();
1467
1516
  ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
1468
- MCP_CONFIG_FILE = ".swarm-mcp.json";
1517
+ MCP_CONFIG_FILE = SWARM_MCP_CONFIG_FILE;
1469
1518
  claudeCodeExecutor = {
1470
1519
  id: "claude-code",
1471
1520
  title: "Claude Code (claude)",
@@ -2647,8 +2696,8 @@ var init_procCleanup = __esm({
2647
2696
 
2648
2697
  // src/git.ts
2649
2698
  import { existsSync as existsSync9, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
2650
- import { appendFile, mkdir, readFile, rm } from "fs/promises";
2651
- import { isAbsolute as isAbsolute2, join as join15 } from "path";
2699
+ import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
2700
+ import { isAbsolute as isAbsolute2, join as join15, sep as sep2 } from "path";
2652
2701
  import { $ as $2 } from "execa";
2653
2702
  function isUnrecoverableRefError(e) {
2654
2703
  if (e instanceof MissingRefError) return true;
@@ -2720,6 +2769,11 @@ async function worktreeForBranch(repo, branch) {
2720
2769
  return null;
2721
2770
  }
2722
2771
  async function addWorktree(repo, path, branch, opts = {}) {
2772
+ const res = await checkoutWorktree(repo, path, branch, opts);
2773
+ await ensureWorktreeHygiene(path);
2774
+ return res;
2775
+ }
2776
+ async function checkoutWorktree(repo, path, branch, opts) {
2723
2777
  await ensureLongPaths(repo);
2724
2778
  const localSha = await revParse(repo, `refs/heads/${branch}`);
2725
2779
  const originSha = await revParse(repo, `refs/remotes/origin/${branch}`);
@@ -2763,10 +2817,8 @@ async function setWorktreeIdentity(worktree, name) {
2763
2817
  }
2764
2818
  async function ensureGitExcluded(worktree, pattern) {
2765
2819
  try {
2766
- const out = await $2({ reject: false })`git -C ${worktree} rev-parse --git-common-dir`;
2767
- const raw = out.stdout.trim();
2768
- if (out.exitCode !== 0 || !raw) return false;
2769
- const commonDir = isAbsolute2(raw) ? raw : join15(worktree, raw);
2820
+ const commonDir = await gitPath(worktree, "--git-common-dir");
2821
+ if (!commonDir) return false;
2770
2822
  const infoDir = join15(commonDir, "info");
2771
2823
  const excludeFile = join15(infoDir, "exclude");
2772
2824
  let current = "";
@@ -2784,6 +2836,46 @@ async function ensureGitExcluded(worktree, pattern) {
2784
2836
  return false;
2785
2837
  }
2786
2838
  }
2839
+ async function gitPath(worktree, flag) {
2840
+ const out = await $2({ reject: false })`git -C ${worktree} rev-parse ${flag}`;
2841
+ const raw = out.stdout.trim();
2842
+ if (out.exitCode !== 0 || !raw) return null;
2843
+ return isAbsolute2(raw) ? raw : join15(worktree, raw);
2844
+ }
2845
+ async function ensureWorktreeHygiene(worktree) {
2846
+ for (const pattern of SWARM_EXCLUDE_PATTERNS) await ensureGitExcluded(worktree, pattern);
2847
+ await installGuardHooks(worktree);
2848
+ }
2849
+ async function installGuardHooks(worktree) {
2850
+ try {
2851
+ const gitDir = await gitPath(worktree, "--git-dir");
2852
+ if (!gitDir) return false;
2853
+ const hooksDir = join15(gitDir, "swarm-hooks");
2854
+ const prev = await previousHooksPath(worktree);
2855
+ await mkdir(hooksDir, { recursive: true });
2856
+ for (const hook of GUARD_HOOKS) {
2857
+ await writeFile(join15(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
2858
+ await chmod(join15(hooksDir, hook), 493);
2859
+ }
2860
+ await $2`git -C ${worktree} config extensions.worktreeConfig true`;
2861
+ await $2`git -C ${worktree} config --worktree core.hooksPath ${posix(hooksDir)}`;
2862
+ return true;
2863
+ } catch {
2864
+ return false;
2865
+ }
2866
+ }
2867
+ function posix(p) {
2868
+ return (sep2 === "\\" ? p.replace(/\\/g, "/") : p).replace(/\/+$/, "");
2869
+ }
2870
+ async function previousHooksPath(worktree) {
2871
+ for (const scope of ["--local", "--global", "--system"]) {
2872
+ const out = await $2({ reject: false })`git -C ${worktree} config ${scope} --get core.hooksPath`;
2873
+ const value = out.stdout.trim();
2874
+ if (out.exitCode === 0 && value) return value;
2875
+ }
2876
+ const commonDir = await gitPath(worktree, "--git-common-dir");
2877
+ return commonDir ? join15(commonDir, "hooks") : null;
2878
+ }
2787
2879
  async function removeWorktree(repo, path) {
2788
2880
  const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
2789
2881
  await cleanupWorktreeProcesses(path);
@@ -2848,6 +2940,7 @@ async function prepareDetachedWorktree(repo, branch, worktree) {
2848
2940
  const ref = `origin/${branch}`;
2849
2941
  if (!await revParse(repo, `${ref}^{commit}`)) throw new MissingRefError(ref, repo);
2850
2942
  await $2`git -C ${repo} worktree add --detach --force ${worktree} ${ref}`;
2943
+ await ensureWorktreeHygiene(worktree);
2851
2944
  }
2852
2945
  async function mergeRef(worktree, ref) {
2853
2946
  const before = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
@@ -2885,6 +2978,7 @@ var init_git = __esm({
2885
2978
  "src/git.ts"() {
2886
2979
  "use strict";
2887
2980
  init_procCleanup();
2981
+ init_swarmFiles();
2888
2982
  WorktreeBusyError = class extends Error {
2889
2983
  constructor(branch, worktreePath) {
2890
2984
  super(`worktree \u0437\u0430\u043D\u044F\u0442: ${worktreePath} (\u0432\u0435\u0442\u043A\u0430 ${branch}) \u2014 \u043F\u0440\u0438\u0431\u0435\u0440\u0438 worktree \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438 \u043F\u0440\u043E\u0433\u043E\u043D (swarm cancel ${branch})`);
@@ -3266,7 +3360,7 @@ var init_droneChannel = __esm({
3266
3360
  });
3267
3361
 
3268
3362
  // src/attachments.ts
3269
- import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
3363
+ import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3270
3364
  import { join as join18 } from "path";
3271
3365
  function attachmentsFromDoc(doc, origin) {
3272
3366
  const out = [];
@@ -3354,7 +3448,7 @@ async function materializeAttachments(opts) {
3354
3448
  const io = opts.io ?? {
3355
3449
  removeDir: async (dir2) => rm2(dir2, { recursive: true, force: true }),
3356
3450
  mkdir: async (dir2) => void await mkdir2(dir2, { recursive: true }),
3357
- writeFile: async (path, bytes) => writeFile(path, bytes)
3451
+ writeFile: async (path, bytes) => writeFile2(path, bytes)
3358
3452
  };
3359
3453
  const result = { files: [], skipped: [] };
3360
3454
  if (opts.refs.length === 0) return result;
@@ -3486,13 +3580,13 @@ function mb(bytes) {
3486
3580
  function errText(e) {
3487
3581
  return e instanceof Error ? e.message : String(e);
3488
3582
  }
3489
- var ATTACHMENTS_DIR, SWARM_TASK_DIR, ATTACH_MAX_FILES, ATTACH_MAX_TOTAL_BYTES, ATTACH_MAX_FILE_BYTES, IMAGE_EXT, DOC_EXT, TEXT_EXT, KNOWN_EXT, TRANSLIT, EXT_BY_MIME;
3583
+ var ATTACHMENTS_DIR, ATTACH_MAX_FILES, ATTACH_MAX_TOTAL_BYTES, ATTACH_MAX_FILE_BYTES, IMAGE_EXT, DOC_EXT, TEXT_EXT, KNOWN_EXT, TRANSLIT, EXT_BY_MIME;
3490
3584
  var init_attachments = __esm({
3491
3585
  "src/attachments.ts"() {
3492
3586
  "use strict";
3493
3587
  init_git();
3494
- ATTACHMENTS_DIR = ".swarm-task/attachments";
3495
- SWARM_TASK_DIR = ".swarm-task";
3588
+ init_swarmFiles();
3589
+ ATTACHMENTS_DIR = `${SWARM_TASK_DIR}/attachments`;
3496
3590
  ATTACH_MAX_FILES = 12;
3497
3591
  ATTACH_MAX_TOTAL_BYTES = 24 * 1024 * 1024;
3498
3592
  ATTACH_MAX_FILE_BYTES = 8 * 1024 * 1024;
@@ -5230,11 +5324,11 @@ function savePauseState(home, state, io = {}) {
5230
5324
  remove(path);
5231
5325
  return;
5232
5326
  }
5233
- const writeFile2 = io.writeFile ?? ((p, data) => {
5327
+ const writeFile3 = io.writeFile ?? ((p, data) => {
5234
5328
  writeFileSync(`${p}.tmp`, data);
5235
5329
  renameSync(`${p}.tmp`, p);
5236
5330
  });
5237
- writeFile2(path, JSON.stringify(state, null, 2));
5331
+ writeFile3(path, JSON.stringify(state, null, 2));
5238
5332
  }
5239
5333
  function makePauseState(source, reason, now = Date.now()) {
5240
5334
  return {
@@ -6595,6 +6689,26 @@ var SwarmClient = class _SwarmClient {
6595
6689
  const v = sc.config?.exclusive;
6596
6690
  return typeof v === "boolean" ? v : null;
6597
6691
  }
6692
+ /**
6693
+ * Live КВОТА «дронов на проект» из Taskless (TASWM-365): тот же swarm_repo_config по
6694
+ * repoFullName — Project.swarmConfig.maxDrones. Сколько прогонов этого проекта могут идти
6695
+ * одновременно; 0 — без ограничения (дефолт проекта). Отдаём ЧИСЛО КАК ЕСТЬ, включая ноль:
6696
+ * превращать его в «без ограничения» — дело ридера (makeDroneCapReader), клиент здесь только
6697
+ * достаёт настройку. null (прод недоступен / repo не смэплен / поля нет / мусор) → ридер
6698
+ * возьмёт «без ограничения», то есть прежнее поведение.
6699
+ */
6700
+ async repoDroneCap(repoFullName) {
6701
+ let res;
6702
+ try {
6703
+ res = await this.call("swarm_repo_config", { repoFullName });
6704
+ } catch {
6705
+ return null;
6706
+ }
6707
+ if (res?.isError) return null;
6708
+ const sc = res.structuredContent ?? {};
6709
+ const v = sc.config?.maxDrones;
6710
+ return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
6711
+ }
6598
6712
  /**
6599
6713
  * TASWM-291: МАШИНЫ роя владельца (swarm_list_hosts) — чтобы `swarm spawn --host
6600
6714
  * <имя>` резолвил человеческое имя в hostId, а `swarm hosts` показывал, какие
@@ -7661,7 +7775,7 @@ function hostIdentityPath(home) {
7661
7775
  function loadHostIdentity(deps) {
7662
7776
  const exists = deps.exists ?? existsSync7;
7663
7777
  const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
7664
- const writeFile2 = deps.writeFile ?? ((p, data) => {
7778
+ const writeFile3 = deps.writeFile ?? ((p, data) => {
7665
7779
  writeFileSync8(`${p}.tmp`, data);
7666
7780
  renameSync6(`${p}.tmp`, p);
7667
7781
  });
@@ -7681,7 +7795,7 @@ function loadHostIdentity(deps) {
7681
7795
  }
7682
7796
  const hostId = newId();
7683
7797
  try {
7684
- writeFile2(path, JSON.stringify({ hostId, hostName, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
7798
+ writeFile3(path, JSON.stringify({ hostId, hostName, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
7685
7799
  } catch {
7686
7800
  console.warn(`[overmind] \u043D\u0435 \u0441\u043C\u043E\u0433 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C ${path} \u2014 hostId \u0436\u0438\u0432\u0451\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430`);
7687
7801
  }
@@ -10327,6 +10441,20 @@ function makeExclusiveReader(opts) {
10327
10441
  ...opts.now !== void 0 ? { now: opts.now } : {}
10328
10442
  });
10329
10443
  }
10444
+ function makeDroneCapReader(opts) {
10445
+ const read = makeRepoValueReader({
10446
+ fetch: opts.fetch,
10447
+ fallback: 0,
10448
+ // «настройки нет» — ниже станет Infinity, как и записанный оператором 0
10449
+ valid: (v) => typeof v === "number" && Number.isFinite(v) && v >= 0,
10450
+ ...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {},
10451
+ ...opts.now !== void 0 ? { now: opts.now } : {}
10452
+ });
10453
+ return async (repo) => {
10454
+ const raw = await read(repo);
10455
+ return raw > 0 ? raw : Number.POSITIVE_INFINITY;
10456
+ };
10457
+ }
10330
10458
 
10331
10459
  // src/envGate.ts
10332
10460
  import { existsSync as existsSync13 } from "fs";
@@ -10391,9 +10519,9 @@ function parseRequirements(lines) {
10391
10519
  out.push({ kind: "docker", raw });
10392
10520
  continue;
10393
10521
  }
10394
- const sep2 = raw.indexOf(":");
10395
- const prefix = sep2 === -1 ? "" : lower.slice(0, sep2);
10396
- const rest = sep2 === -1 ? "" : raw.slice(sep2 + 1).trim();
10522
+ const sep3 = raw.indexOf(":");
10523
+ const prefix = sep3 === -1 ? "" : lower.slice(0, sep3);
10524
+ const rest = sep3 === -1 ? "" : raw.slice(sep3 + 1).trim();
10397
10525
  if (!rest) {
10398
10526
  out.push({ kind: "unknown", raw });
10399
10527
  continue;
@@ -10766,7 +10894,7 @@ async function changedPaths(worktree, opts = {}) {
10766
10894
  // package.json
10767
10895
  var package_default = {
10768
10896
  name: "@taskless-app/swarm",
10769
- version: "0.6.2",
10897
+ version: "0.6.3",
10770
10898
  type: "module",
10771
10899
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
10772
10900
  keywords: [
@@ -10816,7 +10944,7 @@ var package_default = {
10816
10944
  execa: "^10.0.0",
10817
10945
  zod: "3.25.76"
10818
10946
  },
10819
- gitHead: "e812f35a996b00111f3f535b6b0a45b379e7afb2"
10947
+ gitHead: "298573d4b361b8fc7ea4d6ea9d1e8784906bbd7e"
10820
10948
  };
10821
10949
 
10822
10950
  // src/version.ts
@@ -12828,11 +12956,11 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
12828
12956
  }
12829
12957
  function saveAnomalyState(home, state, io = {}) {
12830
12958
  const path = anomalyStatePath(home);
12831
- const writeFile2 = io.writeFile ?? ((p, data) => {
12959
+ const writeFile3 = io.writeFile ?? ((p, data) => {
12832
12960
  writeFileSync14(`${p}.tmp`, data);
12833
12961
  renameSync9(`${p}.tmp`, p);
12834
12962
  });
12835
- writeFile2(path, JSON.stringify(state, null, 2));
12963
+ writeFile3(path, JSON.stringify(state, null, 2));
12836
12964
  }
12837
12965
  var AnomalyStateStore = class {
12838
12966
  constructor(home, now = Date.now(), io = {}) {
@@ -13958,7 +14086,7 @@ async function resurrectStormDead(deps, worktreeExists, hasTranscript, opts) {
13958
14086
  let resurrected = 0;
13959
14087
  for (const run of deps.db.listRuns("storm_dead")) {
13960
14088
  if (countActiveDrones(deps.db) >= cap) break;
13961
- if (!await exclusiveGateAllows(deps, run)) continue;
14089
+ if (!await projectQuotaGateAllows(deps, run)) continue;
13962
14090
  const wt = run.worktree_path;
13963
14091
  const mode = !!wt && worktreeExists(wt) && hasTranscript(wt) ? "continue" : "fresh";
13964
14092
  deps.db.addEvent(run.id, "resumed", mode === "continue" ? STORM_RESUME_CONTINUE_TEXT : STORM_RESUME_FRESH_TEXT);
@@ -14111,7 +14239,7 @@ async function wakeAuthorOnRedCi(deps, run, now, ci, worktreeExists, hasTranscri
14111
14239
  if (run.pid != null && deps.isAlive(run.pid)) return false;
14112
14240
  if (deps.resourcesOk && !deps.resourcesOk()) return false;
14113
14241
  if (countActiveDrones(deps.db) >= droneCap) return false;
14114
- if (!await exclusiveGateAllows(deps, run)) return false;
14242
+ if (!await projectQuotaGateAllows(deps, run)) return false;
14115
14243
  const stats = ciWakeStats(deps.db, run.id);
14116
14244
  if (stats.count >= CI_WAKE_LIMIT) {
14117
14245
  if (!stats.escalated) {
@@ -14242,7 +14370,7 @@ async function wakeAuthorOnChanges(deps, run, now, droneCap) {
14242
14370
  if (deps.db.activeReviewersFor(run.id).some((r) => r.status !== "stalled")) return false;
14243
14371
  if (deps.resourcesOk && !deps.resourcesOk()) return false;
14244
14372
  if (countActiveDrones(deps.db) >= droneCap) return false;
14245
- if (!await exclusiveGateAllows(deps, run)) return false;
14373
+ if (!await projectQuotaGateAllows(deps, run)) return false;
14246
14374
  if (run.review_cycle >= REVIEW_CYCLE_LIMIT) return false;
14247
14375
  const wakes = deps.db.events(run.id).filter((e) => e.kind === "resumed" && (e.text ?? "").includes(CHANGES_WAKE_MARK));
14248
14376
  if (wakes.length >= CHANGES_WAKE_LIMIT) {
@@ -14336,37 +14464,55 @@ function activeDronesOnRepo(deps, repo, exceptRunId) {
14336
14464
  (r) => r.id !== exceptRunId && (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status) && effectiveRepo(deps, r) === repo
14337
14465
  );
14338
14466
  }
14339
- async function exclusiveGateAllows(deps, run) {
14340
- if (!deps.exclusiveProject) return true;
14467
+ async function projectCapFor(deps, repo) {
14468
+ let exclusive = false;
14469
+ if (deps.exclusiveProject) {
14470
+ try {
14471
+ exclusive = await deps.exclusiveProject(repo);
14472
+ } catch (e) {
14473
+ console.error("[overmind] \u0447\u0442\u0435\u043D\u0438\u0435 \u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u043A\u0430\u043A \u0440\u0430\u043D\u044C\u0448\u0435:", e);
14474
+ }
14475
+ }
14476
+ if (exclusive) return { cap: 1, exclusive: true };
14477
+ let quota = Number.POSITIVE_INFINITY;
14478
+ if (deps.projectDroneCap) {
14479
+ try {
14480
+ const v = await deps.projectDroneCap(repo);
14481
+ if (typeof v === "number" && v > 0) quota = v;
14482
+ } catch (e) {
14483
+ console.error("[overmind] \u0447\u0442\u0435\u043D\u0438\u0435 \u043A\u0432\u043E\u0442\u044B \xAB\u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\xBB \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u043A\u0430\u043A \u0440\u0430\u043D\u044C\u0448\u0435:", e);
14484
+ }
14485
+ }
14486
+ return { cap: quota, exclusive: false };
14487
+ }
14488
+ async function projectQuotaGateAllows(deps, run) {
14489
+ if (!deps.exclusiveProject && !deps.projectDroneCap) return true;
14341
14490
  const repo = effectiveRepo(deps, run);
14342
14491
  if (!repo) return true;
14343
- let exclusive;
14344
- try {
14345
- exclusive = await deps.exclusiveProject(repo);
14346
- } catch (e) {
14347
- console.error("[overmind] \u0447\u0442\u0435\u043D\u0438\u0435 \u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u043A\u0430\u043A \u0440\u0430\u043D\u044C\u0448\u0435:", e);
14348
- return true;
14349
- }
14492
+ const { cap, exclusive } = await projectCapFor(deps, repo);
14350
14493
  const logged = deps.exclusiveLogged ??= /* @__PURE__ */ new Map();
14351
- if (exclusive) {
14494
+ if (Number.isFinite(cap)) {
14352
14495
  const busy = activeDronesOnRepo(deps, repo, run.id);
14353
- if (busy.length > 0) {
14496
+ if (busy.length >= cap) {
14354
14497
  const who = busy.map((r) => `${droneLabel(r)} (${r.task_slug})`).join(", ");
14355
- const reason = `\u043F\u0440\u043E\u0435\u043A\u0442 \u0433\u043E\u043D\u044F\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 (\u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B) \u0438 \u0441\u0435\u0439\u0447\u0430\u0441 \u0437\u0430\u043D\u044F\u0442: ${who}`;
14498
+ const reason = exclusive ? `\u043F\u0440\u043E\u0435\u043A\u0442 \u0433\u043E\u043D\u044F\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 (\u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B) \u0438 \u0441\u0435\u0439\u0447\u0430\u0441 \u0437\u0430\u043D\u044F\u0442: ${who}` : `\u043F\u0440\u043E\u0435\u043A\u0442\u0443 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u043E ${cap} \u0434\u0440\u043E\u043D(\u043E\u0432) \u043E\u0434\u043D\u043E\u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E (\xAB\u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\xBB), \u0438 \u0432\u0441\u0435 \u0437\u0430\u043D\u044F\u0442\u044B: ${who}`;
14356
14499
  if (logged.get(run.id) !== reason) {
14357
14500
  logged.set(run.id, reason);
14358
14501
  deps.db.addEvent(
14359
14502
  run.id,
14360
14503
  "step",
14361
- `${reason} \u2014 \u0436\u0434\u0443 \u043D\u0435 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u0443\u044E \u043C\u0430\u0448\u0438\u043D\u0443, \u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430; \u043F\u043E\u0434\u043D\u0438\u043C\u0443 \u0441\u0430\u043C, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u043E\u043D \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u0441\u044F (TASWM-314)`
14504
+ `${reason} \u2014 \u0436\u0434\u0443 \u043D\u0435 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u0443\u044E \u043C\u0430\u0448\u0438\u043D\u0443, \u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430; \u043F\u043E\u0434\u043D\u0438\u043C\u0443 \u0441\u0430\u043C, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u043E\u043D \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u0441\u044F (${exclusive ? "TASWM-314" : "TASWM-365"})`
14505
+ );
14506
+ narrate(
14507
+ deps,
14508
+ exclusive ? `${droneLabel(run)} \u2014 ${reason}. \u041D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E: \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043C\u0435\u0448\u0430\u044E\u0442 \u0434\u0440\u0443\u0433 \u0434\u0440\u0443\u0433\u0443.` : `${droneLabel(run)} \u2014 ${reason}. \u041D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E: \u043F\u0440\u043E\u0435\u043A\u0442 \u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0432\u043E\u044E \u043A\u0432\u043E\u0442\u0443 \u0434\u0440\u043E\u043D\u043E\u0432, \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043F\u043E\u0434\u043E\u0436\u0434\u0451\u0442.`
14362
14509
  );
14363
- narrate(deps, `${droneLabel(run)} \u2014 ${reason}. \u041D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E: \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043C\u0435\u0448\u0430\u044E\u0442 \u0434\u0440\u0443\u0433 \u0434\u0440\u0443\u0433\u0443.`);
14364
14510
  }
14365
14511
  return false;
14366
14512
  }
14367
14513
  }
14368
14514
  if (logged.delete(run.id)) {
14369
- deps.db.addEvent(run.id, "step", "\u043F\u0440\u043E\u0435\u043A\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u043B\u0441\u044F \u2014 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0430 \u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u043E\u0441\u0442\u0438 \u0441\u043D\u044F\u0442\u0430 (TASWM-314)");
14515
+ deps.db.addEvent(run.id, "step", "\u043F\u0440\u043E\u0435\u043A\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u043B\u0441\u044F \u2014 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u043E \u043A\u0432\u043E\u0442\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441\u043D\u044F\u0442\u0430 (TASWM-314/TASWM-365)");
14370
14516
  }
14371
14517
  return true;
14372
14518
  }
@@ -14379,19 +14525,14 @@ function pruneGateLogs(deps) {
14379
14525
  for (const id of [...map.keys()]) if (!open.has(id)) map.delete(id);
14380
14526
  }
14381
14527
  }
14382
- async function exclusivityBlockedRunIds(deps) {
14528
+ async function projectQuotaBlockedRunIds(deps) {
14383
14529
  const blocked = /* @__PURE__ */ new Set();
14384
- if (!deps.exclusiveProject) return blocked;
14530
+ if (!deps.exclusiveProject && !deps.projectDroneCap) return blocked;
14385
14531
  for (const run of deps.db.listRuns("queued")) {
14386
14532
  const repo = effectiveRepo(deps, run);
14387
14533
  if (!repo) continue;
14388
- let exclusive;
14389
- try {
14390
- exclusive = await deps.exclusiveProject(repo);
14391
- } catch {
14392
- continue;
14393
- }
14394
- if (exclusive && activeDronesOnRepo(deps, repo, run.id).length > 0) blocked.add(run.id);
14534
+ const { cap } = await projectCapFor(deps, repo);
14535
+ if (Number.isFinite(cap) && activeDronesOnRepo(deps, repo, run.id).length >= cap) blocked.add(run.id);
14395
14536
  }
14396
14537
  return blocked;
14397
14538
  }
@@ -14419,7 +14560,7 @@ async function trySpawnQueuedRun(deps, run) {
14419
14560
  }
14420
14561
  if (deps.db.unmetDeps(run.depends_on).length > 0) return "skipped";
14421
14562
  if (!await envGateAllows(deps, run)) return "skipped";
14422
- if (!await exclusiveGateAllows(deps, run)) return "skipped";
14563
+ if (!await projectQuotaGateAllows(deps, run)) return "skipped";
14423
14564
  if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
14424
14565
  if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
14425
14566
  await fillSuggestedArea(deps, run);
@@ -14456,7 +14597,7 @@ async function resumeWatchdogParked(deps, droneCap) {
14456
14597
  if (!parked.length) return;
14457
14598
  let candidate;
14458
14599
  for (const run of orderQueue(parked)) {
14459
- if (await exclusiveGateAllows(deps, run)) {
14600
+ if (await projectQuotaGateAllows(deps, run)) {
14460
14601
  candidate = run;
14461
14602
  break;
14462
14603
  }
@@ -14990,7 +15131,7 @@ async function tickOnce(deps) {
14990
15131
  if (st.conflicting && deps.resolveConflict && !stormActive) {
14991
15132
  if (deps.resourcesOk && !deps.resourcesOk()) continue;
14992
15133
  if (countActiveDrones(deps.db) >= droneCap) continue;
14993
- if (!await exclusiveGateAllows(deps, run)) continue;
15134
+ if (!await projectQuotaGateAllows(deps, run)) continue;
14994
15135
  if (deps.db.activeResolverFor(run.id)) continue;
14995
15136
  if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) {
14996
15137
  if (!deps.db.events(run.id).some((e) => e.kind === "conflict" && (e.text ?? "").includes(RESOLVE_ESCALATION_MARK))) {
@@ -15312,7 +15453,7 @@ async function tickOnce(deps) {
15312
15453
  if (run.stop_reason === RELIEF_PAUSE_REASON) continue;
15313
15454
  if (run.stop_reason != null && run.stop_reason.startsWith(EXITED_NO_DONE_REASON)) continue;
15314
15455
  if (run.pid != null && deps.isAlive(run.pid)) continue;
15315
- if (!await exclusiveGateAllows(deps, run)) continue;
15456
+ if (!await projectQuotaGateAllows(deps, run)) continue;
15316
15457
  const wt = run.worktree_path;
15317
15458
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
15318
15459
  const mode = canContinue ? "continue" : "fresh";
@@ -15392,7 +15533,7 @@ async function tickOnce(deps) {
15392
15533
  }
15393
15534
  if (deps.watchdog) {
15394
15535
  try {
15395
- const action = decideWatchdogAction(deps.db, now, deps.watchdog.state, deps.watchdog.thresholds, droneCap, { exists: worktreeExists, hasTranscript }, await exclusivityBlockedRunIds(deps));
15536
+ const action = decideWatchdogAction(deps.db, now, deps.watchdog.state, deps.watchdog.thresholds, droneCap, { exists: worktreeExists, hasTranscript }, await projectQuotaBlockedRunIds(deps));
15396
15537
  if (action) await applyWatchdogAction(deps, action);
15397
15538
  } catch (e) {
15398
15539
  console.error("[overmind] \u0441\u0442\u043E\u0440\u043E\u0436 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430: \u0442\u0438\u043A \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
@@ -15957,6 +16098,7 @@ async function runDaemon(opts) {
15957
16098
  let baseBranchReader;
15958
16099
  let envGate;
15959
16100
  let exclusiveProject;
16101
+ let projectDroneCap;
15960
16102
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
15961
16103
  const client = db;
15962
16104
  verifyModeReader = makeVerifyModeReader({
@@ -15996,6 +16138,13 @@ async function runDaemon(opts) {
15996
16138
  },
15997
16139
  ttlMs: 6e4
15998
16140
  });
16141
+ projectDroneCap = makeDroneCapReader({
16142
+ fetch: async (repo) => {
16143
+ const repoFullName = await remoteRepoFullName(repo).catch(() => null);
16144
+ return repoFullName ? client.repoDroneCap(repoFullName) : null;
16145
+ },
16146
+ ttlMs: 6e4
16147
+ });
15999
16148
  }
16000
16149
  let spawnReviewerFn;
16001
16150
  let verdictCommentFn;
@@ -16216,6 +16365,9 @@ async function runDaemon(opts) {
16216
16365
  // client-режим (настройка живёт в проекте); repoFallback — чтобы прогон с пустым repo не
16217
16366
  // выглядел отдельным проектом. Дедуп событий — своя мапа на цикл демона, как у envGate.
16218
16367
  ...exclusiveProject ? { exclusiveProject } : {},
16368
+ // TASWM-365: и квота «дронов на проект» рядом — один и тот же гейт очереди читает обе
16369
+ // настройки и берёт из них минимум (см. projectCapFor).
16370
+ ...projectDroneCap ? { projectDroneCap } : {},
16219
16371
  repoFallback: opts.repo,
16220
16372
  exclusiveLogged: /* @__PURE__ */ new Map(),
16221
16373
  // Зона задачи из истории репо (TASWM-348) — предлагаем сами, если постановщик
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
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": "e812f35a996b00111f3f535b6b0a45b379e7afb2",
41
+ "gitHead": "298573d4b361b8fc7ea4d6ea9d1e8784906bbd7e",
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",