@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 +18 -0
- package/dist/cli.js +55 -11
- package/dist/overmind.js +225 -58
- package/package.json +2 -2
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 =
|
|
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.
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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
|
|
9277
|
+
const writeFile3 = io.writeFile ?? ((p, data) => {
|
|
9234
9278
|
writeFileSync10(`${p}.tmp`, data);
|
|
9235
9279
|
renameSync5(`${p}.tmp`, p);
|
|
9236
9280
|
});
|
|
9237
|
-
|
|
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 =
|
|
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
|
|
2767
|
-
|
|
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) =>
|
|
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,
|
|
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
|
-
|
|
3495
|
-
|
|
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
|
|
5327
|
+
const writeFile3 = io.writeFile ?? ((p, data) => {
|
|
5234
5328
|
writeFileSync(`${p}.tmp`, data);
|
|
5235
5329
|
renameSync(`${p}.tmp`, p);
|
|
5236
5330
|
});
|
|
5237
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
10386
|
-
const prefix =
|
|
10387
|
-
const rest =
|
|
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.
|
|
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: "
|
|
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
|
|
12959
|
+
const writeFile3 = io.writeFile ?? ((p, data) => {
|
|
12823
12960
|
writeFileSync14(`${p}.tmp`, data);
|
|
12824
12961
|
renameSync9(`${p}.tmp`, p);
|
|
12825
12962
|
});
|
|
12826
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
14331
|
-
|
|
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
|
-
|
|
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 (
|
|
14494
|
+
if (Number.isFinite(cap)) {
|
|
14343
14495
|
const busy = activeDronesOnRepo(deps, repo, run.id);
|
|
14344
|
-
if (busy.length
|
|
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 \
|
|
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
|
|
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
|
-
|
|
14380
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 () =>
|
|
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.
|
|
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": "
|
|
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",
|