@taskless-app/swarm 0.6.1 → 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.1",
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: "415ad928e18430894ae24ac2e4267073f5c6da14"
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` показывал, какие
@@ -5408,7 +5440,16 @@ var SwarmClient = class _SwarmClient {
5408
5440
  const data = res?.structuredContent ?? {};
5409
5441
  return {
5410
5442
  commands: Array.isArray(data.commands) ? data.commands.filter(isHostCommandShape) : [],
5411
- logRequests: parseLogRequests(data.logRequests)
5443
+ logRequests: parseLogRequests(data.logRequests),
5444
+ // Предел человека (TASWM-363). Различаем ТРИ случая, и разница не косметическая:
5445
+ // • ключа нет вовсе (старый прод, пульс без hostId) → undefined: прежнее число
5446
+ // не трогаем, иначе обновление прода молча снимало бы предел;
5447
+ // • число >0 → предел назван;
5448
+ // • всё прочее (null, ноль, мусор) → null: предела нет. Выдуманный потолок хуже
5449
+ // отсутствующего — под ним рой встанет молча.
5450
+ ...data.maxDrones === void 0 ? {} : {
5451
+ maxDrones: typeof data.maxDrones === "number" && Number.isFinite(data.maxDrones) && data.maxDrones > 0 ? Math.trunc(data.maxDrones) : null
5452
+ }
5412
5453
  };
5413
5454
  } catch {
5414
5455
  return EMPTY_HEARTBEAT_REPLY;
@@ -6395,7 +6436,7 @@ function hostIdentityPath(home) {
6395
6436
  function loadHostIdentity(deps) {
6396
6437
  const exists = deps.exists ?? existsSync6;
6397
6438
  const readFile2 = deps.readFile ?? ((p) => readFileSync7(p, "utf8"));
6398
- const writeFile2 = deps.writeFile ?? ((p, data) => {
6439
+ const writeFile3 = deps.writeFile ?? ((p, data) => {
6399
6440
  writeFileSync5(`${p}.tmp`, data);
6400
6441
  renameSync4(`${p}.tmp`, p);
6401
6442
  });
@@ -6415,7 +6456,7 @@ function loadHostIdentity(deps) {
6415
6456
  }
6416
6457
  const hostId = newId();
6417
6458
  try {
6418
- 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));
6419
6460
  } catch {
6420
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`);
6421
6462
  }
@@ -8763,9 +8804,10 @@ init_droneChannel();
8763
8804
 
8764
8805
  // src/git.ts
8765
8806
  import { existsSync as existsSync8, readdirSync as readdirSync8, statSync as statSync8 } from "fs";
8766
- import { appendFile, mkdir, readFile, rm } from "fs/promises";
8767
- 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";
8768
8809
  import { $ as $5 } from "execa";
8810
+ init_swarmFiles();
8769
8811
  function isLinkedWorktree(dir) {
8770
8812
  try {
8771
8813
  return statSync8(join13(dir, ".git")).isFile();
@@ -8816,8 +8858,10 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8816
8858
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
8817
8859
 
8818
8860
  // src/attachments.ts
8819
- 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";
8820
8862
  import { join as join15 } from "path";
8863
+ init_swarmFiles();
8864
+ var ATTACHMENTS_DIR = `${SWARM_TASK_DIR}/attachments`;
8821
8865
  var ATTACH_MAX_TOTAL_BYTES = 24 * 1024 * 1024;
8822
8866
  var ATTACH_MAX_FILE_BYTES = 8 * 1024 * 1024;
8823
8867
  var IMAGE_EXT = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "avif", "heic"]);
@@ -9230,11 +9274,11 @@ function savePauseState(home, state, io = {}) {
9230
9274
  remove(path);
9231
9275
  return;
9232
9276
  }
9233
- const writeFile2 = io.writeFile ?? ((p, data) => {
9277
+ const writeFile3 = io.writeFile ?? ((p, data) => {
9234
9278
  writeFileSync10(`${p}.tmp`, data);
9235
9279
  renameSync5(`${p}.tmp`, p);
9236
9280
  });
9237
- writeFile2(path, JSON.stringify(state, null, 2));
9281
+ writeFile3(path, JSON.stringify(state, null, 2));
9238
9282
  }
9239
9283
  function makePauseState(source, reason, now = Date.now()) {
9240
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` показывал, какие
@@ -6710,7 +6824,16 @@ var SwarmClient = class _SwarmClient {
6710
6824
  const data = res?.structuredContent ?? {};
6711
6825
  return {
6712
6826
  commands: Array.isArray(data.commands) ? data.commands.filter(isHostCommandShape) : [],
6713
- logRequests: parseLogRequests(data.logRequests)
6827
+ logRequests: parseLogRequests(data.logRequests),
6828
+ // Предел человека (TASWM-363). Различаем ТРИ случая, и разница не косметическая:
6829
+ // • ключа нет вовсе (старый прод, пульс без hostId) → undefined: прежнее число
6830
+ // не трогаем, иначе обновление прода молча снимало бы предел;
6831
+ // • число >0 → предел назван;
6832
+ // • всё прочее (null, ноль, мусор) → null: предела нет. Выдуманный потолок хуже
6833
+ // отсутствующего — под ним рой встанет молча.
6834
+ ...data.maxDrones === void 0 ? {} : {
6835
+ maxDrones: typeof data.maxDrones === "number" && Number.isFinite(data.maxDrones) && data.maxDrones > 0 ? Math.trunc(data.maxDrones) : null
6836
+ }
6714
6837
  };
6715
6838
  } catch {
6716
6839
  return EMPTY_HEARTBEAT_REPLY;
@@ -7652,7 +7775,7 @@ function hostIdentityPath(home) {
7652
7775
  function loadHostIdentity(deps) {
7653
7776
  const exists = deps.exists ?? existsSync7;
7654
7777
  const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
7655
- const writeFile2 = deps.writeFile ?? ((p, data) => {
7778
+ const writeFile3 = deps.writeFile ?? ((p, data) => {
7656
7779
  writeFileSync8(`${p}.tmp`, data);
7657
7780
  renameSync6(`${p}.tmp`, p);
7658
7781
  });
@@ -7672,7 +7795,7 @@ function loadHostIdentity(deps) {
7672
7795
  }
7673
7796
  const hostId = newId();
7674
7797
  try {
7675
- 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));
7676
7799
  } catch {
7677
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`);
7678
7801
  }
@@ -10318,6 +10441,20 @@ function makeExclusiveReader(opts) {
10318
10441
  ...opts.now !== void 0 ? { now: opts.now } : {}
10319
10442
  });
10320
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
+ }
10321
10458
 
10322
10459
  // src/envGate.ts
10323
10460
  import { existsSync as existsSync13 } from "fs";
@@ -10382,9 +10519,9 @@ function parseRequirements(lines) {
10382
10519
  out.push({ kind: "docker", raw });
10383
10520
  continue;
10384
10521
  }
10385
- const sep2 = raw.indexOf(":");
10386
- const prefix = sep2 === -1 ? "" : lower.slice(0, sep2);
10387
- 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();
10388
10525
  if (!rest) {
10389
10526
  out.push({ kind: "unknown", raw });
10390
10527
  continue;
@@ -10757,7 +10894,7 @@ async function changedPaths(worktree, opts = {}) {
10757
10894
  // package.json
10758
10895
  var package_default = {
10759
10896
  name: "@taskless-app/swarm",
10760
- version: "0.6.1",
10897
+ version: "0.6.3",
10761
10898
  type: "module",
10762
10899
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
10763
10900
  keywords: [
@@ -10807,7 +10944,7 @@ var package_default = {
10807
10944
  execa: "^10.0.0",
10808
10945
  zod: "3.25.76"
10809
10946
  },
10810
- gitHead: "415ad928e18430894ae24ac2e4267073f5c6da14"
10947
+ gitHead: "298573d4b361b8fc7ea4d6ea9d1e8784906bbd7e"
10811
10948
  };
10812
10949
 
10813
10950
  // src/version.ts
@@ -12819,11 +12956,11 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
12819
12956
  }
12820
12957
  function saveAnomalyState(home, state, io = {}) {
12821
12958
  const path = anomalyStatePath(home);
12822
- const writeFile2 = io.writeFile ?? ((p, data) => {
12959
+ const writeFile3 = io.writeFile ?? ((p, data) => {
12823
12960
  writeFileSync14(`${p}.tmp`, data);
12824
12961
  renameSync9(`${p}.tmp`, p);
12825
12962
  });
12826
- writeFile2(path, JSON.stringify(state, null, 2));
12963
+ writeFile3(path, JSON.stringify(state, null, 2));
12827
12964
  }
12828
12965
  var AnomalyStateStore = class {
12829
12966
  constructor(home, now = Date.now(), io = {}) {
@@ -13949,7 +14086,7 @@ async function resurrectStormDead(deps, worktreeExists, hasTranscript, opts) {
13949
14086
  let resurrected = 0;
13950
14087
  for (const run of deps.db.listRuns("storm_dead")) {
13951
14088
  if (countActiveDrones(deps.db) >= cap) break;
13952
- if (!await exclusiveGateAllows(deps, run)) continue;
14089
+ if (!await projectQuotaGateAllows(deps, run)) continue;
13953
14090
  const wt = run.worktree_path;
13954
14091
  const mode = !!wt && worktreeExists(wt) && hasTranscript(wt) ? "continue" : "fresh";
13955
14092
  deps.db.addEvent(run.id, "resumed", mode === "continue" ? STORM_RESUME_CONTINUE_TEXT : STORM_RESUME_FRESH_TEXT);
@@ -14102,7 +14239,7 @@ async function wakeAuthorOnRedCi(deps, run, now, ci, worktreeExists, hasTranscri
14102
14239
  if (run.pid != null && deps.isAlive(run.pid)) return false;
14103
14240
  if (deps.resourcesOk && !deps.resourcesOk()) return false;
14104
14241
  if (countActiveDrones(deps.db) >= droneCap) return false;
14105
- if (!await exclusiveGateAllows(deps, run)) return false;
14242
+ if (!await projectQuotaGateAllows(deps, run)) return false;
14106
14243
  const stats = ciWakeStats(deps.db, run.id);
14107
14244
  if (stats.count >= CI_WAKE_LIMIT) {
14108
14245
  if (!stats.escalated) {
@@ -14233,7 +14370,7 @@ async function wakeAuthorOnChanges(deps, run, now, droneCap) {
14233
14370
  if (deps.db.activeReviewersFor(run.id).some((r) => r.status !== "stalled")) return false;
14234
14371
  if (deps.resourcesOk && !deps.resourcesOk()) return false;
14235
14372
  if (countActiveDrones(deps.db) >= droneCap) return false;
14236
- if (!await exclusiveGateAllows(deps, run)) return false;
14373
+ if (!await projectQuotaGateAllows(deps, run)) return false;
14237
14374
  if (run.review_cycle >= REVIEW_CYCLE_LIMIT) return false;
14238
14375
  const wakes = deps.db.events(run.id).filter((e) => e.kind === "resumed" && (e.text ?? "").includes(CHANGES_WAKE_MARK));
14239
14376
  if (wakes.length >= CHANGES_WAKE_LIMIT) {
@@ -14327,37 +14464,55 @@ function activeDronesOnRepo(deps, repo, exceptRunId) {
14327
14464
  (r) => r.id !== exceptRunId && (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status) && effectiveRepo(deps, r) === repo
14328
14465
  );
14329
14466
  }
14330
- async function exclusiveGateAllows(deps, run) {
14331
- 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;
14332
14490
  const repo = effectiveRepo(deps, run);
14333
14491
  if (!repo) return true;
14334
- let exclusive;
14335
- try {
14336
- exclusive = await deps.exclusiveProject(repo);
14337
- } catch (e) {
14338
- 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);
14339
- return true;
14340
- }
14492
+ const { cap, exclusive } = await projectCapFor(deps, repo);
14341
14493
  const logged = deps.exclusiveLogged ??= /* @__PURE__ */ new Map();
14342
- if (exclusive) {
14494
+ if (Number.isFinite(cap)) {
14343
14495
  const busy = activeDronesOnRepo(deps, repo, run.id);
14344
- if (busy.length > 0) {
14496
+ if (busy.length >= cap) {
14345
14497
  const who = busy.map((r) => `${droneLabel(r)} (${r.task_slug})`).join(", ");
14346
- 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}`;
14347
14499
  if (logged.get(run.id) !== reason) {
14348
14500
  logged.set(run.id, reason);
14349
14501
  deps.db.addEvent(
14350
14502
  run.id,
14351
14503
  "step",
14352
- `${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.`
14353
14509
  );
14354
- 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.`);
14355
14510
  }
14356
14511
  return false;
14357
14512
  }
14358
14513
  }
14359
14514
  if (logged.delete(run.id)) {
14360
- 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)");
14361
14516
  }
14362
14517
  return true;
14363
14518
  }
@@ -14370,19 +14525,14 @@ function pruneGateLogs(deps) {
14370
14525
  for (const id of [...map.keys()]) if (!open.has(id)) map.delete(id);
14371
14526
  }
14372
14527
  }
14373
- async function exclusivityBlockedRunIds(deps) {
14528
+ async function projectQuotaBlockedRunIds(deps) {
14374
14529
  const blocked = /* @__PURE__ */ new Set();
14375
- if (!deps.exclusiveProject) return blocked;
14530
+ if (!deps.exclusiveProject && !deps.projectDroneCap) return blocked;
14376
14531
  for (const run of deps.db.listRuns("queued")) {
14377
14532
  const repo = effectiveRepo(deps, run);
14378
14533
  if (!repo) continue;
14379
- let exclusive;
14380
- try {
14381
- exclusive = await deps.exclusiveProject(repo);
14382
- } catch {
14383
- continue;
14384
- }
14385
- 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);
14386
14536
  }
14387
14537
  return blocked;
14388
14538
  }
@@ -14410,7 +14560,7 @@ async function trySpawnQueuedRun(deps, run) {
14410
14560
  }
14411
14561
  if (deps.db.unmetDeps(run.depends_on).length > 0) return "skipped";
14412
14562
  if (!await envGateAllows(deps, run)) return "skipped";
14413
- if (!await exclusiveGateAllows(deps, run)) return "skipped";
14563
+ if (!await projectQuotaGateAllows(deps, run)) return "skipped";
14414
14564
  if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
14415
14565
  if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
14416
14566
  await fillSuggestedArea(deps, run);
@@ -14447,7 +14597,7 @@ async function resumeWatchdogParked(deps, droneCap) {
14447
14597
  if (!parked.length) return;
14448
14598
  let candidate;
14449
14599
  for (const run of orderQueue(parked)) {
14450
- if (await exclusiveGateAllows(deps, run)) {
14600
+ if (await projectQuotaGateAllows(deps, run)) {
14451
14601
  candidate = run;
14452
14602
  break;
14453
14603
  }
@@ -14981,7 +15131,7 @@ async function tickOnce(deps) {
14981
15131
  if (st.conflicting && deps.resolveConflict && !stormActive) {
14982
15132
  if (deps.resourcesOk && !deps.resourcesOk()) continue;
14983
15133
  if (countActiveDrones(deps.db) >= droneCap) continue;
14984
- if (!await exclusiveGateAllows(deps, run)) continue;
15134
+ if (!await projectQuotaGateAllows(deps, run)) continue;
14985
15135
  if (deps.db.activeResolverFor(run.id)) continue;
14986
15136
  if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) {
14987
15137
  if (!deps.db.events(run.id).some((e) => e.kind === "conflict" && (e.text ?? "").includes(RESOLVE_ESCALATION_MARK))) {
@@ -15303,7 +15453,7 @@ async function tickOnce(deps) {
15303
15453
  if (run.stop_reason === RELIEF_PAUSE_REASON) continue;
15304
15454
  if (run.stop_reason != null && run.stop_reason.startsWith(EXITED_NO_DONE_REASON)) continue;
15305
15455
  if (run.pid != null && deps.isAlive(run.pid)) continue;
15306
- if (!await exclusiveGateAllows(deps, run)) continue;
15456
+ if (!await projectQuotaGateAllows(deps, run)) continue;
15307
15457
  const wt = run.worktree_path;
15308
15458
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
15309
15459
  const mode = canContinue ? "continue" : "fresh";
@@ -15383,7 +15533,7 @@ async function tickOnce(deps) {
15383
15533
  }
15384
15534
  if (deps.watchdog) {
15385
15535
  try {
15386
- 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));
15387
15537
  if (action) await applyWatchdogAction(deps, action);
15388
15538
  } catch (e) {
15389
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);
@@ -15787,8 +15937,10 @@ async function runDaemon(opts) {
15787
15937
  }
15788
15938
  } : void 0;
15789
15939
  let lastDroneCap = null;
15940
+ let lastOwnCapacity = null;
15790
15941
  const hostCommandState = hostCommandStatePath(SWARM_HOME);
15791
15942
  let hostMaxDrones = loadMaxDronesOverride(hostCommandState);
15943
+ let operatorMaxDrones = null;
15792
15944
  const launch = makeLauncher({
15793
15945
  overmindEntry: THIS_FILE,
15794
15946
  dbPath: DB_PATH,
@@ -15843,7 +15995,8 @@ async function runDaemon(opts) {
15843
15995
  const client = db;
15844
15996
  heartbeat = {
15845
15997
  push: async () => {
15846
- const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastDroneCap);
15998
+ const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastOwnCapacity);
15999
+ if (reply.maxDrones !== void 0) operatorMaxDrones = reply.maxDrones;
15847
16000
  if (reply.commands.length) onHostCommands(reply.commands);
15848
16001
  if (reply.logRequests.length) {
15849
16002
  await serveLogRequests(
@@ -15936,12 +16089,16 @@ async function runDaemon(opts) {
15936
16089
  fallback: SWARM_MAX_IN_FLIGHT > 0 ? SWARM_MAX_IN_FLIGHT : 3,
15937
16090
  ttlMs: 1e4
15938
16091
  });
15939
- maxDrones = async () => lastDroneCap = hostMaxDrones ?? await readMaxDrones();
16092
+ maxDrones = async () => {
16093
+ const own = lastOwnCapacity = hostMaxDrones ?? await readMaxDrones();
16094
+ return lastDroneCap = operatorMaxDrones != null ? Math.min(own, operatorMaxDrones) : own;
16095
+ };
15940
16096
  }
15941
16097
  let verifyModeReader;
15942
16098
  let baseBranchReader;
15943
16099
  let envGate;
15944
16100
  let exclusiveProject;
16101
+ let projectDroneCap;
15945
16102
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
15946
16103
  const client = db;
15947
16104
  verifyModeReader = makeVerifyModeReader({
@@ -15981,6 +16138,13 @@ async function runDaemon(opts) {
15981
16138
  },
15982
16139
  ttlMs: 6e4
15983
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
+ });
15984
16148
  }
15985
16149
  let spawnReviewerFn;
15986
16150
  let verdictCommentFn;
@@ -16201,6 +16365,9 @@ async function runDaemon(opts) {
16201
16365
  // client-режим (настройка живёт в проекте); repoFallback — чтобы прогон с пустым repo не
16202
16366
  // выглядел отдельным проектом. Дедуп событий — своя мапа на цикл демона, как у envGate.
16203
16367
  ...exclusiveProject ? { exclusiveProject } : {},
16368
+ // TASWM-365: и квота «дронов на проект» рядом — один и тот же гейт очереди читает обе
16369
+ // настройки и берёт из них минимум (см. projectCapFor).
16370
+ ...projectDroneCap ? { projectDroneCap } : {},
16204
16371
  repoFallback: opts.repo,
16205
16372
  exclusiveLogged: /* @__PURE__ */ new Map(),
16206
16373
  // Зона задачи из истории репо (TASWM-348) — предлагаем сами, если постановщик
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taskless-app/swarm",
3
- "version": "0.6.1",
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": "415ad928e18430894ae24ac2e4267073f5c6da14",
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",