@taskless-app/swarm 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/cli.js +45 -10
- package/dist/overmind.js +207 -55
- 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` показывал, какие
|
|
@@ -6404,7 +6436,7 @@ function hostIdentityPath(home) {
|
|
|
6404
6436
|
function loadHostIdentity(deps) {
|
|
6405
6437
|
const exists = deps.exists ?? existsSync6;
|
|
6406
6438
|
const readFile2 = deps.readFile ?? ((p) => readFileSync7(p, "utf8"));
|
|
6407
|
-
const
|
|
6439
|
+
const writeFile3 = deps.writeFile ?? ((p, data) => {
|
|
6408
6440
|
writeFileSync5(`${p}.tmp`, data);
|
|
6409
6441
|
renameSync4(`${p}.tmp`, p);
|
|
6410
6442
|
});
|
|
@@ -6424,7 +6456,7 @@ function loadHostIdentity(deps) {
|
|
|
6424
6456
|
}
|
|
6425
6457
|
const hostId = newId();
|
|
6426
6458
|
try {
|
|
6427
|
-
|
|
6459
|
+
writeFile3(path, JSON.stringify({ hostId, hostName, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
6428
6460
|
} catch {
|
|
6429
6461
|
console.warn(`[overmind] \u043D\u0435 \u0441\u043C\u043E\u0433 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C ${path} \u2014 hostId \u0436\u0438\u0432\u0451\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430`);
|
|
6430
6462
|
}
|
|
@@ -8772,9 +8804,10 @@ init_droneChannel();
|
|
|
8772
8804
|
|
|
8773
8805
|
// src/git.ts
|
|
8774
8806
|
import { existsSync as existsSync8, readdirSync as readdirSync8, statSync as statSync8 } from "fs";
|
|
8775
|
-
import { appendFile, mkdir, readFile, rm } from "fs/promises";
|
|
8776
|
-
import { isAbsolute as isAbsolute2, join as join13 } from "path";
|
|
8807
|
+
import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
8808
|
+
import { isAbsolute as isAbsolute2, join as join13, sep as sep2 } from "path";
|
|
8777
8809
|
import { $ as $5 } from "execa";
|
|
8810
|
+
init_swarmFiles();
|
|
8778
8811
|
function isLinkedWorktree(dir) {
|
|
8779
8812
|
try {
|
|
8780
8813
|
return statSync8(join13(dir, ".git")).isFile();
|
|
@@ -8825,8 +8858,10 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
8825
8858
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
8826
8859
|
|
|
8827
8860
|
// src/attachments.ts
|
|
8828
|
-
import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
|
|
8861
|
+
import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
8829
8862
|
import { join as join15 } from "path";
|
|
8863
|
+
init_swarmFiles();
|
|
8864
|
+
var ATTACHMENTS_DIR = `${SWARM_TASK_DIR}/attachments`;
|
|
8830
8865
|
var ATTACH_MAX_TOTAL_BYTES = 24 * 1024 * 1024;
|
|
8831
8866
|
var ATTACH_MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
8832
8867
|
var IMAGE_EXT = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "avif", "heic"]);
|
|
@@ -9239,11 +9274,11 @@ function savePauseState(home, state, io = {}) {
|
|
|
9239
9274
|
remove(path);
|
|
9240
9275
|
return;
|
|
9241
9276
|
}
|
|
9242
|
-
const
|
|
9277
|
+
const writeFile3 = io.writeFile ?? ((p, data) => {
|
|
9243
9278
|
writeFileSync10(`${p}.tmp`, data);
|
|
9244
9279
|
renameSync5(`${p}.tmp`, p);
|
|
9245
9280
|
});
|
|
9246
|
-
|
|
9281
|
+
writeFile3(path, JSON.stringify(state, null, 2));
|
|
9247
9282
|
}
|
|
9248
9283
|
function makePauseState(source, reason, now = Date.now()) {
|
|
9249
9284
|
return {
|
package/dist/overmind.js
CHANGED
|
@@ -1325,6 +1325,54 @@ var init_claudeBin = __esm({
|
|
|
1325
1325
|
}
|
|
1326
1326
|
});
|
|
1327
1327
|
|
|
1328
|
+
// src/swarmFiles.ts
|
|
1329
|
+
function sq(s) {
|
|
1330
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1331
|
+
}
|
|
1332
|
+
function guardHookScript(hook, delegate) {
|
|
1333
|
+
const paths = SWARM_WORKTREE_ARTIFACTS.map(sq).join(" ");
|
|
1334
|
+
const lines = [
|
|
1335
|
+
"#!/usr/bin/env sh",
|
|
1336
|
+
`# \u0421\u0442\u0440\u0430\u0445\u043E\u0432\u043A\u0430 \u0440\u043E\u044F (TASWM-372): \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B \u0440\u043E\u044F \u043D\u0435 \u0443\u0435\u0437\u0436\u0430\u044E\u0442 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.`,
|
|
1337
|
+
"# \u0425\u0443\u043A \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u041F\u0415\u0420-WORKTREE (git config --worktree core.hooksPath) \u043F\u0440\u0438 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0435",
|
|
1338
|
+
"# worktree \u0434\u0440\u043E\u043D\u0430; \u0445\u0443\u043A\u0438 \u0441\u0430\u043C\u043E\u0433\u043E \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u043D\u0435 \u0442\u0440\u043E\u043D\u0443\u0442\u044B \u2014 \u043F\u0440\u0435\u0436\u043D\u0438\u0439 \u0432\u044B\u0437\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u043D\u0438\u0436\u0435.",
|
|
1339
|
+
`swarm_found=$(git ls-files -- ${paths} 2>/dev/null)`,
|
|
1340
|
+
`if [ -n "$swarm_found" ]; then`,
|
|
1341
|
+
` echo "\u2716 \u0421\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B \u0420\u041E\u042F \u0432 \u0438\u043D\u0434\u0435\u043A\u0441\u0435 git \u2014 ${ACTION[hook]} (TASWM-372):" >&2`,
|
|
1342
|
+
` printf '%s\\n' "$swarm_found" | sed 's/^/ /' >&2`,
|
|
1343
|
+
` echo " \u042D\u0442\u043E \u0444\u0430\u0439\u043B\u044B \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0440\u043E\u044F, \u0430 \u043D\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430: \u0432 ${SWARM_MCP_CONFIG_FILE} \u043B\u0435\u0436\u0438\u0442 \u0442\u043E\u043A\u0435\u043D," >&2`,
|
|
1344
|
+
` echo " \u043A\u043E\u0442\u043E\u0440\u043E\u043C\u0443 \u043D\u0435\u043B\u044C\u0437\u044F \u043E\u043A\u0430\u0437\u0430\u0442\u044C\u0441\u044F \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 (\u0438\u043D\u0446\u0438\u0434\u0435\u043D\u0442 11.08)." >&2`,
|
|
1345
|
+
` echo " \u0423\u0431\u0435\u0440\u0438 \u0438\u0445 \u0438\u0437 \u0438\u043D\u0434\u0435\u043A\u0441\u0430 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438:" >&2`,
|
|
1346
|
+
` echo " git rm --cached -r --ignore-unmatch ${SWARM_WORKTREE_ARTIFACTS.join(" ")}" >&2`,
|
|
1347
|
+
` echo " \u0424\u0430\u0439\u043B\u044B \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 \u043E\u0441\u0442\u0430\u043D\u0443\u0442\u0441\u044F \u2014 \u043E\u043D\u0438 \u043D\u0443\u0436\u043D\u044B \u043F\u0440\u043E\u0433\u043E\u043D\u0443." >&2`,
|
|
1348
|
+
" exit 1",
|
|
1349
|
+
"fi"
|
|
1350
|
+
];
|
|
1351
|
+
if (delegate) {
|
|
1352
|
+
lines.push(
|
|
1353
|
+
"# \u041F\u0440\u0435\u0436\u043D\u0438\u0439 \u0445\u0443\u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (husky \u0438 \u0442.\u043F.) \u2014 \u043E\u043D \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u043F\u0440\u043E\u043F\u0430\u0441\u0442\u044C \u0438\u0437-\u0437\u0430 \u043D\u0430\u0448\u0435\u0439 \u0441\u0442\u0440\u0430\u0445\u043E\u0432\u043A\u0438.",
|
|
1354
|
+
`if [ -x ${sq(delegate)} ]; then exec ${sq(delegate)} "$@"; fi`
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
lines.push("exit 0", "");
|
|
1358
|
+
return lines.join("\n");
|
|
1359
|
+
}
|
|
1360
|
+
var SWARM_MCP_CONFIG_FILE, SWARM_TASK_DIR, SWARM_WORKTREE_ARTIFACTS, SWARM_EXCLUDE_PATTERNS, GUARD_HOOKS, ACTION;
|
|
1361
|
+
var init_swarmFiles = __esm({
|
|
1362
|
+
"src/swarmFiles.ts"() {
|
|
1363
|
+
"use strict";
|
|
1364
|
+
SWARM_MCP_CONFIG_FILE = ".swarm-mcp.json";
|
|
1365
|
+
SWARM_TASK_DIR = ".swarm-task";
|
|
1366
|
+
SWARM_WORKTREE_ARTIFACTS = [SWARM_MCP_CONFIG_FILE, SWARM_TASK_DIR];
|
|
1367
|
+
SWARM_EXCLUDE_PATTERNS = [`/${SWARM_MCP_CONFIG_FILE}`, `/${SWARM_TASK_DIR}/`];
|
|
1368
|
+
GUARD_HOOKS = ["pre-commit", "pre-push"];
|
|
1369
|
+
ACTION = {
|
|
1370
|
+
"pre-commit": "\u043A\u043E\u043C\u043C\u0438\u0442 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D",
|
|
1371
|
+
"pre-push": "\u043F\u0443\u0448 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D"
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1328
1376
|
// src/pricing.ts
|
|
1329
1377
|
function costFromUsage(usage, model, table = MODEL_PRICING) {
|
|
1330
1378
|
const pricing = model ? table[model] : void 0;
|
|
@@ -1463,9 +1511,10 @@ var init_claudeCode = __esm({
|
|
|
1463
1511
|
"src/executors/claudeCode.ts"() {
|
|
1464
1512
|
"use strict";
|
|
1465
1513
|
init_claudeBin();
|
|
1514
|
+
init_swarmFiles();
|
|
1466
1515
|
init_claudeResult();
|
|
1467
1516
|
ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
|
|
1468
|
-
MCP_CONFIG_FILE =
|
|
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` показывал, какие
|
|
@@ -7661,7 +7775,7 @@ function hostIdentityPath(home) {
|
|
|
7661
7775
|
function loadHostIdentity(deps) {
|
|
7662
7776
|
const exists = deps.exists ?? existsSync7;
|
|
7663
7777
|
const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
|
|
7664
|
-
const
|
|
7778
|
+
const writeFile3 = deps.writeFile ?? ((p, data) => {
|
|
7665
7779
|
writeFileSync8(`${p}.tmp`, data);
|
|
7666
7780
|
renameSync6(`${p}.tmp`, p);
|
|
7667
7781
|
});
|
|
@@ -7681,7 +7795,7 @@ function loadHostIdentity(deps) {
|
|
|
7681
7795
|
}
|
|
7682
7796
|
const hostId = newId();
|
|
7683
7797
|
try {
|
|
7684
|
-
|
|
7798
|
+
writeFile3(path, JSON.stringify({ hostId, hostName, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
7685
7799
|
} catch {
|
|
7686
7800
|
console.warn(`[overmind] \u043D\u0435 \u0441\u043C\u043E\u0433 \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C ${path} \u2014 hostId \u0436\u0438\u0432\u0451\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043E \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430`);
|
|
7687
7801
|
}
|
|
@@ -10327,6 +10441,20 @@ function makeExclusiveReader(opts) {
|
|
|
10327
10441
|
...opts.now !== void 0 ? { now: opts.now } : {}
|
|
10328
10442
|
});
|
|
10329
10443
|
}
|
|
10444
|
+
function makeDroneCapReader(opts) {
|
|
10445
|
+
const read = makeRepoValueReader({
|
|
10446
|
+
fetch: opts.fetch,
|
|
10447
|
+
fallback: 0,
|
|
10448
|
+
// «настройки нет» — ниже станет Infinity, как и записанный оператором 0
|
|
10449
|
+
valid: (v) => typeof v === "number" && Number.isFinite(v) && v >= 0,
|
|
10450
|
+
...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {},
|
|
10451
|
+
...opts.now !== void 0 ? { now: opts.now } : {}
|
|
10452
|
+
});
|
|
10453
|
+
return async (repo) => {
|
|
10454
|
+
const raw = await read(repo);
|
|
10455
|
+
return raw > 0 ? raw : Number.POSITIVE_INFINITY;
|
|
10456
|
+
};
|
|
10457
|
+
}
|
|
10330
10458
|
|
|
10331
10459
|
// src/envGate.ts
|
|
10332
10460
|
import { existsSync as existsSync13 } from "fs";
|
|
@@ -10391,9 +10519,9 @@ function parseRequirements(lines) {
|
|
|
10391
10519
|
out.push({ kind: "docker", raw });
|
|
10392
10520
|
continue;
|
|
10393
10521
|
}
|
|
10394
|
-
const
|
|
10395
|
-
const prefix =
|
|
10396
|
-
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();
|
|
10397
10525
|
if (!rest) {
|
|
10398
10526
|
out.push({ kind: "unknown", raw });
|
|
10399
10527
|
continue;
|
|
@@ -10766,7 +10894,7 @@ async function changedPaths(worktree, opts = {}) {
|
|
|
10766
10894
|
// package.json
|
|
10767
10895
|
var package_default = {
|
|
10768
10896
|
name: "@taskless-app/swarm",
|
|
10769
|
-
version: "0.6.
|
|
10897
|
+
version: "0.6.3",
|
|
10770
10898
|
type: "module",
|
|
10771
10899
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
10772
10900
|
keywords: [
|
|
@@ -10816,7 +10944,7 @@ var package_default = {
|
|
|
10816
10944
|
execa: "^10.0.0",
|
|
10817
10945
|
zod: "3.25.76"
|
|
10818
10946
|
},
|
|
10819
|
-
gitHead: "
|
|
10947
|
+
gitHead: "298573d4b361b8fc7ea4d6ea9d1e8784906bbd7e"
|
|
10820
10948
|
};
|
|
10821
10949
|
|
|
10822
10950
|
// src/version.ts
|
|
@@ -12828,11 +12956,11 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
|
|
|
12828
12956
|
}
|
|
12829
12957
|
function saveAnomalyState(home, state, io = {}) {
|
|
12830
12958
|
const path = anomalyStatePath(home);
|
|
12831
|
-
const
|
|
12959
|
+
const writeFile3 = io.writeFile ?? ((p, data) => {
|
|
12832
12960
|
writeFileSync14(`${p}.tmp`, data);
|
|
12833
12961
|
renameSync9(`${p}.tmp`, p);
|
|
12834
12962
|
});
|
|
12835
|
-
|
|
12963
|
+
writeFile3(path, JSON.stringify(state, null, 2));
|
|
12836
12964
|
}
|
|
12837
12965
|
var AnomalyStateStore = class {
|
|
12838
12966
|
constructor(home, now = Date.now(), io = {}) {
|
|
@@ -13958,7 +14086,7 @@ async function resurrectStormDead(deps, worktreeExists, hasTranscript, opts) {
|
|
|
13958
14086
|
let resurrected = 0;
|
|
13959
14087
|
for (const run of deps.db.listRuns("storm_dead")) {
|
|
13960
14088
|
if (countActiveDrones(deps.db) >= cap) break;
|
|
13961
|
-
if (!await
|
|
14089
|
+
if (!await projectQuotaGateAllows(deps, run)) continue;
|
|
13962
14090
|
const wt = run.worktree_path;
|
|
13963
14091
|
const mode = !!wt && worktreeExists(wt) && hasTranscript(wt) ? "continue" : "fresh";
|
|
13964
14092
|
deps.db.addEvent(run.id, "resumed", mode === "continue" ? STORM_RESUME_CONTINUE_TEXT : STORM_RESUME_FRESH_TEXT);
|
|
@@ -14111,7 +14239,7 @@ async function wakeAuthorOnRedCi(deps, run, now, ci, worktreeExists, hasTranscri
|
|
|
14111
14239
|
if (run.pid != null && deps.isAlive(run.pid)) return false;
|
|
14112
14240
|
if (deps.resourcesOk && !deps.resourcesOk()) return false;
|
|
14113
14241
|
if (countActiveDrones(deps.db) >= droneCap) return false;
|
|
14114
|
-
if (!await
|
|
14242
|
+
if (!await projectQuotaGateAllows(deps, run)) return false;
|
|
14115
14243
|
const stats = ciWakeStats(deps.db, run.id);
|
|
14116
14244
|
if (stats.count >= CI_WAKE_LIMIT) {
|
|
14117
14245
|
if (!stats.escalated) {
|
|
@@ -14242,7 +14370,7 @@ async function wakeAuthorOnChanges(deps, run, now, droneCap) {
|
|
|
14242
14370
|
if (deps.db.activeReviewersFor(run.id).some((r) => r.status !== "stalled")) return false;
|
|
14243
14371
|
if (deps.resourcesOk && !deps.resourcesOk()) return false;
|
|
14244
14372
|
if (countActiveDrones(deps.db) >= droneCap) return false;
|
|
14245
|
-
if (!await
|
|
14373
|
+
if (!await projectQuotaGateAllows(deps, run)) return false;
|
|
14246
14374
|
if (run.review_cycle >= REVIEW_CYCLE_LIMIT) return false;
|
|
14247
14375
|
const wakes = deps.db.events(run.id).filter((e) => e.kind === "resumed" && (e.text ?? "").includes(CHANGES_WAKE_MARK));
|
|
14248
14376
|
if (wakes.length >= CHANGES_WAKE_LIMIT) {
|
|
@@ -14336,37 +14464,55 @@ function activeDronesOnRepo(deps, repo, exceptRunId) {
|
|
|
14336
14464
|
(r) => r.id !== exceptRunId && (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status) && effectiveRepo(deps, r) === repo
|
|
14337
14465
|
);
|
|
14338
14466
|
}
|
|
14339
|
-
async function
|
|
14340
|
-
|
|
14467
|
+
async function projectCapFor(deps, repo) {
|
|
14468
|
+
let exclusive = false;
|
|
14469
|
+
if (deps.exclusiveProject) {
|
|
14470
|
+
try {
|
|
14471
|
+
exclusive = await deps.exclusiveProject(repo);
|
|
14472
|
+
} catch (e) {
|
|
14473
|
+
console.error("[overmind] \u0447\u0442\u0435\u043D\u0438\u0435 \u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u043A\u0430\u043A \u0440\u0430\u043D\u044C\u0448\u0435:", e);
|
|
14474
|
+
}
|
|
14475
|
+
}
|
|
14476
|
+
if (exclusive) return { cap: 1, exclusive: true };
|
|
14477
|
+
let quota = Number.POSITIVE_INFINITY;
|
|
14478
|
+
if (deps.projectDroneCap) {
|
|
14479
|
+
try {
|
|
14480
|
+
const v = await deps.projectDroneCap(repo);
|
|
14481
|
+
if (typeof v === "number" && v > 0) quota = v;
|
|
14482
|
+
} catch (e) {
|
|
14483
|
+
console.error("[overmind] \u0447\u0442\u0435\u043D\u0438\u0435 \u043A\u0432\u043E\u0442\u044B \xAB\u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\xBB \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u043A\u0430\u043A \u0440\u0430\u043D\u044C\u0448\u0435:", e);
|
|
14484
|
+
}
|
|
14485
|
+
}
|
|
14486
|
+
return { cap: quota, exclusive: false };
|
|
14487
|
+
}
|
|
14488
|
+
async function projectQuotaGateAllows(deps, run) {
|
|
14489
|
+
if (!deps.exclusiveProject && !deps.projectDroneCap) return true;
|
|
14341
14490
|
const repo = effectiveRepo(deps, run);
|
|
14342
14491
|
if (!repo) return true;
|
|
14343
|
-
|
|
14344
|
-
try {
|
|
14345
|
-
exclusive = await deps.exclusiveProject(repo);
|
|
14346
|
-
} catch (e) {
|
|
14347
|
-
console.error("[overmind] \u0447\u0442\u0435\u043D\u0438\u0435 \u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u043A\u0430\u043A \u0440\u0430\u043D\u044C\u0448\u0435:", e);
|
|
14348
|
-
return true;
|
|
14349
|
-
}
|
|
14492
|
+
const { cap, exclusive } = await projectCapFor(deps, repo);
|
|
14350
14493
|
const logged = deps.exclusiveLogged ??= /* @__PURE__ */ new Map();
|
|
14351
|
-
if (
|
|
14494
|
+
if (Number.isFinite(cap)) {
|
|
14352
14495
|
const busy = activeDronesOnRepo(deps, repo, run.id);
|
|
14353
|
-
if (busy.length
|
|
14496
|
+
if (busy.length >= cap) {
|
|
14354
14497
|
const who = busy.map((r) => `${droneLabel(r)} (${r.task_slug})`).join(", ");
|
|
14355
|
-
const reason = `\u043F\u0440\u043E\u0435\u043A\u0442 \u0433\u043E\u043D\u044F\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 (\u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B) \u0438 \u0441\u0435\u0439\u0447\u0430\u0441 \u0437\u0430\u043D\u044F\u0442: ${who}`;
|
|
14498
|
+
const reason = exclusive ? `\u043F\u0440\u043E\u0435\u043A\u0442 \u0433\u043E\u043D\u044F\u0435\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 (\u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B) \u0438 \u0441\u0435\u0439\u0447\u0430\u0441 \u0437\u0430\u043D\u044F\u0442: ${who}` : `\u043F\u0440\u043E\u0435\u043A\u0442\u0443 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u043E ${cap} \u0434\u0440\u043E\u043D(\u043E\u0432) \u043E\u0434\u043D\u043E\u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E (\xAB\u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\xBB), \u0438 \u0432\u0441\u0435 \u0437\u0430\u043D\u044F\u0442\u044B: ${who}`;
|
|
14356
14499
|
if (logged.get(run.id) !== reason) {
|
|
14357
14500
|
logged.set(run.id, reason);
|
|
14358
14501
|
deps.db.addEvent(
|
|
14359
14502
|
run.id,
|
|
14360
14503
|
"step",
|
|
14361
|
-
`${reason} \u2014 \u0436\u0434\u0443 \u043D\u0435 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u0443\u044E \u043C\u0430\u0448\u0438\u043D\u0443, \u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430; \u043F\u043E\u0434\u043D\u0438\u043C\u0443 \u0441\u0430\u043C, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u043E\u043D \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u0441\u044F (TASWM-314)`
|
|
14504
|
+
`${reason} \u2014 \u0436\u0434\u0443 \u043D\u0435 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u0443\u044E \u043C\u0430\u0448\u0438\u043D\u0443, \u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430; \u043F\u043E\u0434\u043D\u0438\u043C\u0443 \u0441\u0430\u043C, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u043E\u043D \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u0441\u044F (${exclusive ? "TASWM-314" : "TASWM-365"})`
|
|
14505
|
+
);
|
|
14506
|
+
narrate(
|
|
14507
|
+
deps,
|
|
14508
|
+
exclusive ? `${droneLabel(run)} \u2014 ${reason}. \u041D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E: \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043C\u0435\u0448\u0430\u044E\u0442 \u0434\u0440\u0443\u0433 \u0434\u0440\u0443\u0433\u0443.` : `${droneLabel(run)} \u2014 ${reason}. \u041D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E: \u043F\u0440\u043E\u0435\u043A\u0442 \u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0432\u043E\u044E \u043A\u0432\u043E\u0442\u0443 \u0434\u0440\u043E\u043D\u043E\u0432, \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043F\u043E\u0434\u043E\u0436\u0434\u0451\u0442.`
|
|
14362
14509
|
);
|
|
14363
|
-
narrate(deps, `${droneLabel(run)} \u2014 ${reason}. \u041D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E: \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u043C\u0435\u0448\u0430\u044E\u0442 \u0434\u0440\u0443\u0433 \u0434\u0440\u0443\u0433\u0443.`);
|
|
14364
14510
|
}
|
|
14365
14511
|
return false;
|
|
14366
14512
|
}
|
|
14367
14513
|
}
|
|
14368
14514
|
if (logged.delete(run.id)) {
|
|
14369
|
-
deps.db.addEvent(run.id, "step", "\u043F\u0440\u043E\u0435\u043A\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u043B\u0441\u044F \u2014 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0430 \
|
|
14515
|
+
deps.db.addEvent(run.id, "step", "\u043F\u0440\u043E\u0435\u043A\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u043B\u0441\u044F \u2014 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u043E \u043A\u0432\u043E\u0442\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0441\u043D\u044F\u0442\u0430 (TASWM-314/TASWM-365)");
|
|
14370
14516
|
}
|
|
14371
14517
|
return true;
|
|
14372
14518
|
}
|
|
@@ -14379,19 +14525,14 @@ function pruneGateLogs(deps) {
|
|
|
14379
14525
|
for (const id of [...map.keys()]) if (!open.has(id)) map.delete(id);
|
|
14380
14526
|
}
|
|
14381
14527
|
}
|
|
14382
|
-
async function
|
|
14528
|
+
async function projectQuotaBlockedRunIds(deps) {
|
|
14383
14529
|
const blocked = /* @__PURE__ */ new Set();
|
|
14384
|
-
if (!deps.exclusiveProject) return blocked;
|
|
14530
|
+
if (!deps.exclusiveProject && !deps.projectDroneCap) return blocked;
|
|
14385
14531
|
for (const run of deps.db.listRuns("queued")) {
|
|
14386
14532
|
const repo = effectiveRepo(deps, run);
|
|
14387
14533
|
if (!repo) continue;
|
|
14388
|
-
|
|
14389
|
-
|
|
14390
|
-
exclusive = await deps.exclusiveProject(repo);
|
|
14391
|
-
} catch {
|
|
14392
|
-
continue;
|
|
14393
|
-
}
|
|
14394
|
-
if (exclusive && activeDronesOnRepo(deps, repo, run.id).length > 0) blocked.add(run.id);
|
|
14534
|
+
const { cap } = await projectCapFor(deps, repo);
|
|
14535
|
+
if (Number.isFinite(cap) && activeDronesOnRepo(deps, repo, run.id).length >= cap) blocked.add(run.id);
|
|
14395
14536
|
}
|
|
14396
14537
|
return blocked;
|
|
14397
14538
|
}
|
|
@@ -14419,7 +14560,7 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
14419
14560
|
}
|
|
14420
14561
|
if (deps.db.unmetDeps(run.depends_on).length > 0) return "skipped";
|
|
14421
14562
|
if (!await envGateAllows(deps, run)) return "skipped";
|
|
14422
|
-
if (!await
|
|
14563
|
+
if (!await projectQuotaGateAllows(deps, run)) return "skipped";
|
|
14423
14564
|
if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
|
|
14424
14565
|
if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
|
|
14425
14566
|
await fillSuggestedArea(deps, run);
|
|
@@ -14456,7 +14597,7 @@ async function resumeWatchdogParked(deps, droneCap) {
|
|
|
14456
14597
|
if (!parked.length) return;
|
|
14457
14598
|
let candidate;
|
|
14458
14599
|
for (const run of orderQueue(parked)) {
|
|
14459
|
-
if (await
|
|
14600
|
+
if (await projectQuotaGateAllows(deps, run)) {
|
|
14460
14601
|
candidate = run;
|
|
14461
14602
|
break;
|
|
14462
14603
|
}
|
|
@@ -14990,7 +15131,7 @@ async function tickOnce(deps) {
|
|
|
14990
15131
|
if (st.conflicting && deps.resolveConflict && !stormActive) {
|
|
14991
15132
|
if (deps.resourcesOk && !deps.resourcesOk()) continue;
|
|
14992
15133
|
if (countActiveDrones(deps.db) >= droneCap) continue;
|
|
14993
|
-
if (!await
|
|
15134
|
+
if (!await projectQuotaGateAllows(deps, run)) continue;
|
|
14994
15135
|
if (deps.db.activeResolverFor(run.id)) continue;
|
|
14995
15136
|
if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) {
|
|
14996
15137
|
if (!deps.db.events(run.id).some((e) => e.kind === "conflict" && (e.text ?? "").includes(RESOLVE_ESCALATION_MARK))) {
|
|
@@ -15312,7 +15453,7 @@ async function tickOnce(deps) {
|
|
|
15312
15453
|
if (run.stop_reason === RELIEF_PAUSE_REASON) continue;
|
|
15313
15454
|
if (run.stop_reason != null && run.stop_reason.startsWith(EXITED_NO_DONE_REASON)) continue;
|
|
15314
15455
|
if (run.pid != null && deps.isAlive(run.pid)) continue;
|
|
15315
|
-
if (!await
|
|
15456
|
+
if (!await projectQuotaGateAllows(deps, run)) continue;
|
|
15316
15457
|
const wt = run.worktree_path;
|
|
15317
15458
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
15318
15459
|
const mode = canContinue ? "continue" : "fresh";
|
|
@@ -15392,7 +15533,7 @@ async function tickOnce(deps) {
|
|
|
15392
15533
|
}
|
|
15393
15534
|
if (deps.watchdog) {
|
|
15394
15535
|
try {
|
|
15395
|
-
const action = decideWatchdogAction(deps.db, now, deps.watchdog.state, deps.watchdog.thresholds, droneCap, { exists: worktreeExists, hasTranscript }, await
|
|
15536
|
+
const action = decideWatchdogAction(deps.db, now, deps.watchdog.state, deps.watchdog.thresholds, droneCap, { exists: worktreeExists, hasTranscript }, await projectQuotaBlockedRunIds(deps));
|
|
15396
15537
|
if (action) await applyWatchdogAction(deps, action);
|
|
15397
15538
|
} catch (e) {
|
|
15398
15539
|
console.error("[overmind] \u0441\u0442\u043E\u0440\u043E\u0436 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430: \u0442\u0438\u043A \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
@@ -15957,6 +16098,7 @@ async function runDaemon(opts) {
|
|
|
15957
16098
|
let baseBranchReader;
|
|
15958
16099
|
let envGate;
|
|
15959
16100
|
let exclusiveProject;
|
|
16101
|
+
let projectDroneCap;
|
|
15960
16102
|
if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
|
|
15961
16103
|
const client = db;
|
|
15962
16104
|
verifyModeReader = makeVerifyModeReader({
|
|
@@ -15996,6 +16138,13 @@ async function runDaemon(opts) {
|
|
|
15996
16138
|
},
|
|
15997
16139
|
ttlMs: 6e4
|
|
15998
16140
|
});
|
|
16141
|
+
projectDroneCap = makeDroneCapReader({
|
|
16142
|
+
fetch: async (repo) => {
|
|
16143
|
+
const repoFullName = await remoteRepoFullName(repo).catch(() => null);
|
|
16144
|
+
return repoFullName ? client.repoDroneCap(repoFullName) : null;
|
|
16145
|
+
},
|
|
16146
|
+
ttlMs: 6e4
|
|
16147
|
+
});
|
|
15999
16148
|
}
|
|
16000
16149
|
let spawnReviewerFn;
|
|
16001
16150
|
let verdictCommentFn;
|
|
@@ -16216,6 +16365,9 @@ async function runDaemon(opts) {
|
|
|
16216
16365
|
// client-режим (настройка живёт в проекте); repoFallback — чтобы прогон с пустым repo не
|
|
16217
16366
|
// выглядел отдельным проектом. Дедуп событий — своя мапа на цикл демона, как у envGate.
|
|
16218
16367
|
...exclusiveProject ? { exclusiveProject } : {},
|
|
16368
|
+
// TASWM-365: и квота «дронов на проект» рядом — один и тот же гейт очереди читает обе
|
|
16369
|
+
// настройки и берёт из них минимум (см. projectCapFor).
|
|
16370
|
+
...projectDroneCap ? { projectDroneCap } : {},
|
|
16219
16371
|
repoFallback: opts.repo,
|
|
16220
16372
|
exclusiveLogged: /* @__PURE__ */ new Map(),
|
|
16221
16373
|
// Зона задачи из истории репо (TASWM-348) — предлагаем сами, если постановщик
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taskless-app/swarm",
|
|
3
|
-
"version": "0.6.
|
|
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",
|