@taskless-app/swarm 0.6.39 → 0.6.40
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/dist/cli.js +104 -25
- package/dist/overmind.js +143 -30
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1366,7 +1366,7 @@ function buildZoneModel(entries, opts = {}) {
|
|
|
1366
1366
|
if (!m) labelVotes.set(z, m = /* @__PURE__ */ new Map());
|
|
1367
1367
|
bump(m, manual);
|
|
1368
1368
|
}
|
|
1369
|
-
const
|
|
1369
|
+
const rename2 = /* @__PURE__ */ new Map();
|
|
1370
1370
|
const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
|
|
1371
1371
|
const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
|
|
1372
1372
|
const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
|
|
@@ -1375,11 +1375,11 @@ function buildZoneModel(entries, opts = {}) {
|
|
|
1375
1375
|
for (const [zone, votes] of clustersByEvidence) {
|
|
1376
1376
|
const top = topKey(votes);
|
|
1377
1377
|
if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
|
|
1378
|
-
|
|
1378
|
+
rename2.set(zone, top.key);
|
|
1379
1379
|
takenNames.add(top.key);
|
|
1380
1380
|
}
|
|
1381
|
-
const renamed = (z) =>
|
|
1382
|
-
if (
|
|
1381
|
+
const renamed = (z) => rename2.get(z) ?? z;
|
|
1382
|
+
if (rename2.size) {
|
|
1383
1383
|
for (const [f, z] of fileZone) fileZone.set(f, renamed(z));
|
|
1384
1384
|
for (const [s, z] of taskZone) taskZone.set(s, renamed(z));
|
|
1385
1385
|
}
|
|
@@ -3191,7 +3191,7 @@ var init_procList = __esm({
|
|
|
3191
3191
|
|
|
3192
3192
|
// src/procCleanup.ts
|
|
3193
3193
|
function sleep(ms) {
|
|
3194
|
-
return new Promise((
|
|
3194
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
3195
3195
|
}
|
|
3196
3196
|
function parseLsofCwd(output) {
|
|
3197
3197
|
const rows = [];
|
|
@@ -3390,7 +3390,7 @@ var init_package = __esm({
|
|
|
3390
3390
|
"package.json"() {
|
|
3391
3391
|
package_default = {
|
|
3392
3392
|
name: "@taskless-app/swarm",
|
|
3393
|
-
version: "0.6.
|
|
3393
|
+
version: "0.6.40",
|
|
3394
3394
|
type: "module",
|
|
3395
3395
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
3396
3396
|
keywords: [
|
|
@@ -3441,7 +3441,7 @@ var init_package = __esm({
|
|
|
3441
3441
|
execa: "^10.0.0",
|
|
3442
3442
|
zod: "3.25.76"
|
|
3443
3443
|
},
|
|
3444
|
-
gitHead: "
|
|
3444
|
+
gitHead: "b58ef18678e8c0e03e0602a40bf76d6f0a6975c8"
|
|
3445
3445
|
};
|
|
3446
3446
|
}
|
|
3447
3447
|
});
|
|
@@ -3838,9 +3838,9 @@ var init_oauth = __esm({
|
|
|
3838
3838
|
});
|
|
3839
3839
|
|
|
3840
3840
|
// src/git.ts
|
|
3841
|
-
import { existsSync as existsSync10, readdirSync as readdirSync9, statSync as statSync9 } from "fs";
|
|
3842
|
-
import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
3843
|
-
import { isAbsolute, join as join15, sep as sep2 } from "path";
|
|
3841
|
+
import { existsSync as existsSync10, readdirSync as readdirSync9, realpathSync as realpathSync2, statSync as statSync9 } from "fs";
|
|
3842
|
+
import { appendFile, chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
3843
|
+
import { isAbsolute, join as join15, resolve as resolve2, sep as sep2 } from "path";
|
|
3844
3844
|
import { $ as $5 } from "execa";
|
|
3845
3845
|
function gitNet(repo, args, timeoutMs) {
|
|
3846
3846
|
return withHardTimeout(
|
|
@@ -3853,6 +3853,72 @@ function gitNet(repo, args, timeoutMs) {
|
|
|
3853
3853
|
async function reclaimStaleWorktree(repo, worktree) {
|
|
3854
3854
|
await detachWorktree(repo, worktree);
|
|
3855
3855
|
}
|
|
3856
|
+
async function inspectWorktreePath(path) {
|
|
3857
|
+
if (!existsSync10(path)) return "free";
|
|
3858
|
+
try {
|
|
3859
|
+
if (readdirSync9(path).length === 0) return "free";
|
|
3860
|
+
} catch {
|
|
3861
|
+
return "orphan";
|
|
3862
|
+
}
|
|
3863
|
+
try {
|
|
3864
|
+
if (statSync9(join15(path, ".git")).isDirectory()) return "main-checkout";
|
|
3865
|
+
} catch {
|
|
3866
|
+
}
|
|
3867
|
+
const top = await $5({ reject: false })`git -C ${path} rev-parse --show-toplevel`;
|
|
3868
|
+
return top.exitCode === 0 && samePath(top.stdout.trim(), path) ? "worktree" : "orphan";
|
|
3869
|
+
}
|
|
3870
|
+
function samePath(a, b) {
|
|
3871
|
+
const norm = (p) => {
|
|
3872
|
+
try {
|
|
3873
|
+
return realpathSync2(p);
|
|
3874
|
+
} catch {
|
|
3875
|
+
return resolve2(p);
|
|
3876
|
+
}
|
|
3877
|
+
};
|
|
3878
|
+
const [x, y] = [norm(a), norm(b)];
|
|
3879
|
+
return process.platform === "win32" ? posix(x).toLowerCase() === posix(y).toLowerCase() : x === y;
|
|
3880
|
+
}
|
|
3881
|
+
async function clearWorktreePath(repo, path, branch, opts = {}) {
|
|
3882
|
+
const holder = await inspectWorktreePath(path);
|
|
3883
|
+
if (holder === "free") return;
|
|
3884
|
+
if (holder === "main-checkout") {
|
|
3885
|
+
throw new WorktreeBusyError(
|
|
3886
|
+
branch,
|
|
3887
|
+
path,
|
|
3888
|
+
`\u043F\u043E \u043F\u0443\u0442\u0438 worktree ${path} \u043B\u0435\u0436\u0438\u0442 \u041E\u0421\u041D\u041E\u0412\u041D\u041E\u0419 \u0447\u0435\u043A\u0430\u0443\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (\`.git\` \u2014 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u0430 \u043D\u0435 \u0444\u0430\u0439\u043B) \u2014 \u044F \u0435\u0433\u043E \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E. \u041F\u043E\u0445\u043E\u0436\u0435, \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree'\u0435\u0432 \u0440\u043E\u044F \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D \u043D\u0430 \u0440\u0430\u0431\u043E\u0447\u0443\u044E \u043A\u043E\u043F\u0438\u044E \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430; \u043F\u043E\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0442\u044C, \u0441\u043F\u0430\u0432\u043D \u0442\u0443\u0434\u0430 \u0437\u0430\u0441\u0435\u043B\u044F\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0435\u0442.`
|
|
3889
|
+
);
|
|
3890
|
+
}
|
|
3891
|
+
if (holder === "worktree") {
|
|
3892
|
+
if (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(path) : true) throw new WorktreeBusyError(branch, path);
|
|
3893
|
+
if (await worktreeDirty(path)) {
|
|
3894
|
+
throw new WorktreeBusyError(
|
|
3895
|
+
branch,
|
|
3896
|
+
path,
|
|
3897
|
+
`worktree ${path} \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B, \u043D\u043E \u0432\u043D\u0443\u0442\u0440\u0438 \u0435\u0441\u0442\u044C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 (\u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u043E\u0435 \u0438\u043B\u0438 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435) \u2014 \u043D\u0435 \u0441\u043D\u043E\u0448\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435 \u0441\u0430\u043C\u0438 (\`git -C ${path} status --untracked-files=all\`) \u0438 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u043D\u0435 \u0436\u0430\u043B\u043A\u043E.`
|
|
3898
|
+
);
|
|
3899
|
+
}
|
|
3900
|
+
await detachWorktree(repo, path);
|
|
3901
|
+
opts.onStaleReclaim?.(path);
|
|
3902
|
+
}
|
|
3903
|
+
if (!existsSync10(path)) return;
|
|
3904
|
+
await parkOrphanDir(repo, path, branch);
|
|
3905
|
+
}
|
|
3906
|
+
async function parkOrphanDir(repo, path, branch) {
|
|
3907
|
+
const parked = `${path}.orphan-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
3908
|
+
try {
|
|
3909
|
+
await rename(path, parked);
|
|
3910
|
+
} catch (e) {
|
|
3911
|
+
throw new WorktreeBusyError(
|
|
3912
|
+
branch,
|
|
3913
|
+
path,
|
|
3914
|
+
`\u043F\u0443\u0442\u044C ${path} \u0437\u0430\u043D\u044F\u0442 \u043F\u043E\u0441\u0442\u043E\u0440\u043E\u043D\u043D\u0438\u043C \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u043E\u043C, \u0438 \u043E\u0442\u043E\u0434\u0432\u0438\u043D\u0443\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${String(e)}) \u2014 \u0441\u043F\u0430\u0432\u043D ${branch} \u0442\u0443\u0434\u0430 \u043D\u0435 \u0432\u0441\u0442\u0430\u043D\u0435\u0442. \u0423\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u0441\u0434\u0435\u043B\u0430\u0439\u0442\u0435 \`git -C ${repo} worktree prune\`.`
|
|
3915
|
+
);
|
|
3916
|
+
}
|
|
3917
|
+
console.error(
|
|
3918
|
+
`[git] \u043F\u043E \u043F\u0443\u0442\u0438 worktree ${path} \u043B\u0435\u0436\u0430\u043B \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 git \u0441\u0432\u043E\u0438\u043C \u0440\u0430\u0431\u043E\u0447\u0438\u043C \u0434\u0435\u0440\u0435\u0432\u043E\u043C \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u2014 \u043E\u0442\u043E\u0434\u0432\u0438\u043D\u0443\u043B \u0432 ${parked}, \u0447\u0442\u043E\u0431\u044B \u0441\u043F\u0430\u0432\u043D ${branch} \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435, \u0447\u0442\u043E \u0432\u043D\u0443\u0442\u0440\u0438, \u0438 \u0443\u0434\u0430\u043B\u0438\u0442\u0435, \u0435\u0441\u043B\u0438 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`
|
|
3919
|
+
);
|
|
3920
|
+
await $5({ reject: false })`git -C ${repo} worktree prune`;
|
|
3921
|
+
}
|
|
3856
3922
|
function isLinkedWorktree(dir) {
|
|
3857
3923
|
try {
|
|
3858
3924
|
return statSync9(join15(dir, ".git")).isFile();
|
|
@@ -3925,6 +3991,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
|
|
|
3925
3991
|
const originSha = await revParse(repo, `refs/remotes/origin/${branch}`);
|
|
3926
3992
|
if (!localSha && !originSha) {
|
|
3927
3993
|
const freshBase = opts.base ?? await freshWorktreeBase(repo);
|
|
3994
|
+
await clearWorktreePath(repo, path, branch, opts);
|
|
3928
3995
|
await $5`git -C ${repo} worktree add -b ${branch} ${path} ${freshBase}`;
|
|
3929
3996
|
return { reused: false };
|
|
3930
3997
|
}
|
|
@@ -3937,6 +4004,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
|
|
|
3937
4004
|
await reclaimStaleWorktree(repo, busyAt);
|
|
3938
4005
|
opts.onStaleReclaim?.(busyAt);
|
|
3939
4006
|
}
|
|
4007
|
+
await clearWorktreePath(repo, path, branch, opts);
|
|
3940
4008
|
if (originSha) {
|
|
3941
4009
|
await $5`git -C ${repo} worktree add -B ${branch} ${path} ${`origin/${branch}`}`;
|
|
3942
4010
|
return { reused: true, from: "origin", sha: originSha };
|
|
@@ -4057,8 +4125,14 @@ var init_git = __esm({
|
|
|
4057
4125
|
init_netTimeout();
|
|
4058
4126
|
init_swarmFiles();
|
|
4059
4127
|
WorktreeBusyError = class extends Error {
|
|
4060
|
-
|
|
4061
|
-
|
|
4128
|
+
/**
|
|
4129
|
+
* `why` (TASWM-553) — ПОЧЕМУ путь не отдали, если причина не «его держит живой прогон»:
|
|
4130
|
+
* основной чекаут по пути worktree, каталог с несданной работой, каталог, который не
|
|
4131
|
+
* получилось убрать. Дефолтная формулировка («прибери worktree или отмени прогон»)
|
|
4132
|
+
* человеку в таких случаях не помогает — она отправляет его не туда.
|
|
4133
|
+
*/
|
|
4134
|
+
constructor(branch, worktreePath, why) {
|
|
4135
|
+
super(why ?? `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})`);
|
|
4062
4136
|
this.branch = branch;
|
|
4063
4137
|
this.worktreePath = worktreePath;
|
|
4064
4138
|
this.name = "WorktreeBusyError";
|
|
@@ -4353,14 +4427,14 @@ function createNetworkSettleTracker(target) {
|
|
|
4353
4427
|
});
|
|
4354
4428
|
target.on("requestfinished", onDone);
|
|
4355
4429
|
target.on("requestfailed", onDone);
|
|
4356
|
-
const settleOnce = (quietMs, budgetMs) => new Promise((
|
|
4430
|
+
const settleOnce = (quietMs, budgetMs) => new Promise((resolve5) => {
|
|
4357
4431
|
let quietTimer;
|
|
4358
4432
|
const hardTimer = setTimeout(finish, Math.max(0, budgetMs));
|
|
4359
4433
|
function finish() {
|
|
4360
4434
|
clearTimeout(quietTimer);
|
|
4361
4435
|
clearTimeout(hardTimer);
|
|
4362
4436
|
onChange.delete(check);
|
|
4363
|
-
|
|
4437
|
+
resolve5();
|
|
4364
4438
|
}
|
|
4365
4439
|
function check() {
|
|
4366
4440
|
clearTimeout(quietTimer);
|
|
@@ -4637,7 +4711,7 @@ __export(db_exports, {
|
|
|
4637
4711
|
import { createRequire as createRequire2 } from "module";
|
|
4638
4712
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4639
4713
|
import { existsSync as existsSync12 } from "fs";
|
|
4640
|
-
import { resolve as
|
|
4714
|
+
import { resolve as resolve3 } from "path";
|
|
4641
4715
|
function rowToQuestion(row) {
|
|
4642
4716
|
return {
|
|
4643
4717
|
id: row.id,
|
|
@@ -5266,7 +5340,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
5266
5340
|
* транзакции: прервётся — маркер не встанет, повтор безопасен.
|
|
5267
5341
|
*/
|
|
5268
5342
|
importDb(sourcePath, repo) {
|
|
5269
|
-
const abs =
|
|
5343
|
+
const abs = resolve3(sourcePath);
|
|
5270
5344
|
const repoN = normalizeRepo(repo);
|
|
5271
5345
|
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
5272
5346
|
if (already) return { imported: false, runs: already.run_count };
|
|
@@ -5609,7 +5683,7 @@ var init_repoScan = __esm({
|
|
|
5609
5683
|
// src/repoDiscovery.ts
|
|
5610
5684
|
import { homedir as homedir6 } from "os";
|
|
5611
5685
|
import { readFileSync as readFileSync15, renameSync as renameSync7, writeFileSync as writeFileSync12 } from "fs";
|
|
5612
|
-
import { isAbsolute as isAbsolute3, join as join21, resolve as
|
|
5686
|
+
import { isAbsolute as isAbsolute3, join as join21, resolve as resolve4 } from "path";
|
|
5613
5687
|
function repoMapLines(rows) {
|
|
5614
5688
|
if (!rows?.length) return [];
|
|
5615
5689
|
const missing = rows.filter((r) => !r.path).length;
|
|
@@ -11784,14 +11858,14 @@ function makeHandler(deps) {
|
|
|
11784
11858
|
const ref = url.searchParams.get("pr") ?? "";
|
|
11785
11859
|
if (!ref.trim()) return Response.json({ error: "\u043D\u0443\u0436\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 pr=<url|\u043D\u043E\u043C\u0435\u0440>" }, { status: 400 });
|
|
11786
11860
|
const ctxRepo = url.searchParams.get("repo");
|
|
11787
|
-
const
|
|
11861
|
+
const resolve5 = deps.mergeGate ?? makeMergeGateResolver({
|
|
11788
11862
|
listRuns: () => deps.db.listRuns(),
|
|
11789
11863
|
fallbackAutoReview: SWARM_AUTO_REVIEW,
|
|
11790
11864
|
autoVerify: SWARM_AUTO_VERIFY,
|
|
11791
11865
|
// TASWM-344
|
|
11792
11866
|
...defaultRepo ? { repoFallback: defaultRepo } : {}
|
|
11793
11867
|
});
|
|
11794
|
-
return Response.json(await
|
|
11868
|
+
return Response.json(await resolve5(ref, ctxRepo));
|
|
11795
11869
|
}
|
|
11796
11870
|
if (p === "/resources") return Response.json(hostResourceSnapshot());
|
|
11797
11871
|
if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
|
|
@@ -12678,10 +12752,10 @@ async function restoreDaemon(d, loginOk) {
|
|
|
12678
12752
|
d.err("\u0434\u0435\u043C\u043E\u043D \u043E\u0431\u0440\u0430\u0442\u043D\u043E \u041D\u0415 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F (\u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u0432\u044B\u0448\u0435) \u2014 \u0440\u043E\u0439 \u0441\u0442\u043E\u0438\u0442. \u041F\u043E\u0434\u043D\u0438\u043C\u0438 \u0432\u0440\u0443\u0447\u043D\u0443\u044E: swarm start");
|
|
12679
12753
|
}
|
|
12680
12754
|
function isPortFree(port, host = "127.0.0.1") {
|
|
12681
|
-
return new Promise((
|
|
12755
|
+
return new Promise((resolve5) => {
|
|
12682
12756
|
const probe = createServer();
|
|
12683
|
-
probe.once("error", () =>
|
|
12684
|
-
probe.listen(port, host, () => probe.close(() =>
|
|
12757
|
+
probe.once("error", () => resolve5(false));
|
|
12758
|
+
probe.listen(port, host, () => probe.close(() => resolve5(true)));
|
|
12685
12759
|
});
|
|
12686
12760
|
}
|
|
12687
12761
|
async function chooseWorkspace(d) {
|
|
@@ -13511,7 +13585,7 @@ async function scoutRepo(repo) {
|
|
|
13511
13585
|
mkdirSync10(dirname5(abs(rel)), { recursive: true });
|
|
13512
13586
|
writeFileSync14(abs(rel), text);
|
|
13513
13587
|
},
|
|
13514
|
-
runScout: (prompt) => new Promise((
|
|
13588
|
+
runScout: (prompt) => new Promise((resolve5) => {
|
|
13515
13589
|
const child = spawn3(bin, [...ISOLATION_FLAGS, "--dangerously-skip-permissions", "-p", prompt], {
|
|
13516
13590
|
cwd: worktree,
|
|
13517
13591
|
stdio: ["ignore", "inherit", "inherit"]
|
|
@@ -13519,9 +13593,9 @@ async function scoutRepo(repo) {
|
|
|
13519
13593
|
});
|
|
13520
13594
|
child.on("error", (e) => {
|
|
13521
13595
|
console.error(`\u043D\u0435 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B\u0441\u044F \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C (${bin}): ${e.message}`);
|
|
13522
|
-
|
|
13596
|
+
resolve5(1);
|
|
13523
13597
|
});
|
|
13524
|
-
child.on("exit", (c) =>
|
|
13598
|
+
child.on("exit", (c) => resolve5(c ?? 1));
|
|
13525
13599
|
}),
|
|
13526
13600
|
publish: (files) => publishScoutPr({ worktree, repo, files, name, baseBranch, openPr: openPr.url, fresh: openPr.known && !openPr.url }),
|
|
13527
13601
|
log: (line) => console.log(line)
|
|
@@ -14059,6 +14133,11 @@ switch (cmd) {
|
|
|
14059
14133
|
{
|
|
14060
14134
|
db,
|
|
14061
14135
|
kill: (pid) => process.kill(pid, "SIGTERM"),
|
|
14136
|
+
// TASWM-553: тот же порядок, что у демона (stop_run / POST /cancel) — снос worktree
|
|
14137
|
+
// ПОСЛЕ смерти процесса, а не следом за просьбой умереть. Здесь это нужнее всего:
|
|
14138
|
+
// сюда попадают ровно тогда, когда демон не ответил, и подчистить за собой некому.
|
|
14139
|
+
isAlive: pidAlive,
|
|
14140
|
+
hardKill: (pid) => process.kill(pid, "SIGKILL"),
|
|
14062
14141
|
removeWorktree: (repo, p) => {
|
|
14063
14142
|
const r = repo || SWARM_REPO;
|
|
14064
14143
|
return r ? removeWorktree(r, p) : Promise.resolve();
|
package/dist/overmind.js
CHANGED
|
@@ -1593,7 +1593,7 @@ function buildZoneModel(entries, opts = {}) {
|
|
|
1593
1593
|
if (!m) labelVotes.set(z2, m = /* @__PURE__ */ new Map());
|
|
1594
1594
|
bump(m, manual);
|
|
1595
1595
|
}
|
|
1596
|
-
const
|
|
1596
|
+
const rename3 = /* @__PURE__ */ new Map();
|
|
1597
1597
|
const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
|
|
1598
1598
|
const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
|
|
1599
1599
|
const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
|
|
@@ -1602,11 +1602,11 @@ function buildZoneModel(entries, opts = {}) {
|
|
|
1602
1602
|
for (const [zone, votes] of clustersByEvidence) {
|
|
1603
1603
|
const top = topKey(votes);
|
|
1604
1604
|
if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
|
|
1605
|
-
|
|
1605
|
+
rename3.set(zone, top.key);
|
|
1606
1606
|
takenNames.add(top.key);
|
|
1607
1607
|
}
|
|
1608
|
-
const renamed = (z2) =>
|
|
1609
|
-
if (
|
|
1608
|
+
const renamed = (z2) => rename3.get(z2) ?? z2;
|
|
1609
|
+
if (rename3.size) {
|
|
1610
1610
|
for (const [f, z2] of fileZone) fileZone.set(f, renamed(z2));
|
|
1611
1611
|
for (const [s, z2] of taskZone) taskZone.set(s, renamed(z2));
|
|
1612
1612
|
}
|
|
@@ -3895,7 +3895,7 @@ var init_procList = __esm({
|
|
|
3895
3895
|
|
|
3896
3896
|
// src/procCleanup.ts
|
|
3897
3897
|
function sleep(ms) {
|
|
3898
|
-
return new Promise((
|
|
3898
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
3899
3899
|
}
|
|
3900
3900
|
function parseLsofCwd(output) {
|
|
3901
3901
|
const rows = [];
|
|
@@ -4113,9 +4113,9 @@ var init_procCleanup = __esm({
|
|
|
4113
4113
|
});
|
|
4114
4114
|
|
|
4115
4115
|
// src/git.ts
|
|
4116
|
-
import { existsSync as existsSync12, readdirSync as readdirSync12, statSync as statSync10 } from "fs";
|
|
4117
|
-
import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
4118
|
-
import { isAbsolute as isAbsolute2, join as join20, sep as sep2 } from "path";
|
|
4116
|
+
import { existsSync as existsSync12, readdirSync as readdirSync12, realpathSync as realpathSync2, statSync as statSync10 } from "fs";
|
|
4117
|
+
import { appendFile, chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
4118
|
+
import { isAbsolute as isAbsolute2, join as join20, resolve as resolve3, sep as sep2 } from "path";
|
|
4119
4119
|
import { $ as $2 } from "execa";
|
|
4120
4120
|
function gitNet(repo, args, timeoutMs) {
|
|
4121
4121
|
return withHardTimeout(
|
|
@@ -4136,6 +4136,72 @@ function isUnrecoverableRefError(e) {
|
|
|
4136
4136
|
async function reclaimStaleWorktree(repo, worktree) {
|
|
4137
4137
|
await detachWorktree(repo, worktree);
|
|
4138
4138
|
}
|
|
4139
|
+
async function inspectWorktreePath(path) {
|
|
4140
|
+
if (!existsSync12(path)) return "free";
|
|
4141
|
+
try {
|
|
4142
|
+
if (readdirSync12(path).length === 0) return "free";
|
|
4143
|
+
} catch {
|
|
4144
|
+
return "orphan";
|
|
4145
|
+
}
|
|
4146
|
+
try {
|
|
4147
|
+
if (statSync10(join20(path, ".git")).isDirectory()) return "main-checkout";
|
|
4148
|
+
} catch {
|
|
4149
|
+
}
|
|
4150
|
+
const top = await $2({ reject: false })`git -C ${path} rev-parse --show-toplevel`;
|
|
4151
|
+
return top.exitCode === 0 && samePath(top.stdout.trim(), path) ? "worktree" : "orphan";
|
|
4152
|
+
}
|
|
4153
|
+
function samePath(a, b) {
|
|
4154
|
+
const norm = (p) => {
|
|
4155
|
+
try {
|
|
4156
|
+
return realpathSync2(p);
|
|
4157
|
+
} catch {
|
|
4158
|
+
return resolve3(p);
|
|
4159
|
+
}
|
|
4160
|
+
};
|
|
4161
|
+
const [x, y] = [norm(a), norm(b)];
|
|
4162
|
+
return process.platform === "win32" ? posix(x).toLowerCase() === posix(y).toLowerCase() : x === y;
|
|
4163
|
+
}
|
|
4164
|
+
async function clearWorktreePath(repo, path, branch, opts = {}) {
|
|
4165
|
+
const holder = await inspectWorktreePath(path);
|
|
4166
|
+
if (holder === "free") return;
|
|
4167
|
+
if (holder === "main-checkout") {
|
|
4168
|
+
throw new WorktreeBusyError(
|
|
4169
|
+
branch,
|
|
4170
|
+
path,
|
|
4171
|
+
`\u043F\u043E \u043F\u0443\u0442\u0438 worktree ${path} \u043B\u0435\u0436\u0438\u0442 \u041E\u0421\u041D\u041E\u0412\u041D\u041E\u0419 \u0447\u0435\u043A\u0430\u0443\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (\`.git\` \u2014 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u0430 \u043D\u0435 \u0444\u0430\u0439\u043B) \u2014 \u044F \u0435\u0433\u043E \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E. \u041F\u043E\u0445\u043E\u0436\u0435, \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree'\u0435\u0432 \u0440\u043E\u044F \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D \u043D\u0430 \u0440\u0430\u0431\u043E\u0447\u0443\u044E \u043A\u043E\u043F\u0438\u044E \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430; \u043F\u043E\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0442\u044C, \u0441\u043F\u0430\u0432\u043D \u0442\u0443\u0434\u0430 \u0437\u0430\u0441\u0435\u043B\u044F\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0435\u0442.`
|
|
4172
|
+
);
|
|
4173
|
+
}
|
|
4174
|
+
if (holder === "worktree") {
|
|
4175
|
+
if (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(path) : true) throw new WorktreeBusyError(branch, path);
|
|
4176
|
+
if (await worktreeDirty(path)) {
|
|
4177
|
+
throw new WorktreeBusyError(
|
|
4178
|
+
branch,
|
|
4179
|
+
path,
|
|
4180
|
+
`worktree ${path} \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B, \u043D\u043E \u0432\u043D\u0443\u0442\u0440\u0438 \u0435\u0441\u0442\u044C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 (\u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u043E\u0435 \u0438\u043B\u0438 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435) \u2014 \u043D\u0435 \u0441\u043D\u043E\u0448\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435 \u0441\u0430\u043C\u0438 (\`git -C ${path} status --untracked-files=all\`) \u0438 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u043D\u0435 \u0436\u0430\u043B\u043A\u043E.`
|
|
4181
|
+
);
|
|
4182
|
+
}
|
|
4183
|
+
await detachWorktree(repo, path);
|
|
4184
|
+
opts.onStaleReclaim?.(path);
|
|
4185
|
+
}
|
|
4186
|
+
if (!existsSync12(path)) return;
|
|
4187
|
+
await parkOrphanDir(repo, path, branch);
|
|
4188
|
+
}
|
|
4189
|
+
async function parkOrphanDir(repo, path, branch) {
|
|
4190
|
+
const parked = `${path}.orphan-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
4191
|
+
try {
|
|
4192
|
+
await rename(path, parked);
|
|
4193
|
+
} catch (e) {
|
|
4194
|
+
throw new WorktreeBusyError(
|
|
4195
|
+
branch,
|
|
4196
|
+
path,
|
|
4197
|
+
`\u043F\u0443\u0442\u044C ${path} \u0437\u0430\u043D\u044F\u0442 \u043F\u043E\u0441\u0442\u043E\u0440\u043E\u043D\u043D\u0438\u043C \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u043E\u043C, \u0438 \u043E\u0442\u043E\u0434\u0432\u0438\u043D\u0443\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${String(e)}) \u2014 \u0441\u043F\u0430\u0432\u043D ${branch} \u0442\u0443\u0434\u0430 \u043D\u0435 \u0432\u0441\u0442\u0430\u043D\u0435\u0442. \u0423\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u0441\u0434\u0435\u043B\u0430\u0439\u0442\u0435 \`git -C ${repo} worktree prune\`.`
|
|
4198
|
+
);
|
|
4199
|
+
}
|
|
4200
|
+
console.error(
|
|
4201
|
+
`[git] \u043F\u043E \u043F\u0443\u0442\u0438 worktree ${path} \u043B\u0435\u0436\u0430\u043B \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 git \u0441\u0432\u043E\u0438\u043C \u0440\u0430\u0431\u043E\u0447\u0438\u043C \u0434\u0435\u0440\u0435\u0432\u043E\u043C \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u2014 \u043E\u0442\u043E\u0434\u0432\u0438\u043D\u0443\u043B \u0432 ${parked}, \u0447\u0442\u043E\u0431\u044B \u0441\u043F\u0430\u0432\u043D ${branch} \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435, \u0447\u0442\u043E \u0432\u043D\u0443\u0442\u0440\u0438, \u0438 \u0443\u0434\u0430\u043B\u0438\u0442\u0435, \u0435\u0441\u043B\u0438 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`
|
|
4202
|
+
);
|
|
4203
|
+
await $2({ reject: false })`git -C ${repo} worktree prune`;
|
|
4204
|
+
}
|
|
4139
4205
|
function isLinkedWorktree(dir) {
|
|
4140
4206
|
try {
|
|
4141
4207
|
return statSync10(join20(dir, ".git")).isFile();
|
|
@@ -4208,6 +4274,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
|
|
|
4208
4274
|
const originSha = await revParse(repo, `refs/remotes/origin/${branch}`);
|
|
4209
4275
|
if (!localSha && !originSha) {
|
|
4210
4276
|
const freshBase = opts.base ?? await freshWorktreeBase(repo);
|
|
4277
|
+
await clearWorktreePath(repo, path, branch, opts);
|
|
4211
4278
|
await $2`git -C ${repo} worktree add -b ${branch} ${path} ${freshBase}`;
|
|
4212
4279
|
return { reused: false };
|
|
4213
4280
|
}
|
|
@@ -4220,6 +4287,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
|
|
|
4220
4287
|
await reclaimStaleWorktree(repo, busyAt);
|
|
4221
4288
|
opts.onStaleReclaim?.(busyAt);
|
|
4222
4289
|
}
|
|
4290
|
+
await clearWorktreePath(repo, path, branch, opts);
|
|
4223
4291
|
if (originSha) {
|
|
4224
4292
|
await $2`git -C ${repo} worktree add -B ${branch} ${path} ${`origin/${branch}`}`;
|
|
4225
4293
|
return { reused: true, from: "origin", sha: originSha };
|
|
@@ -4371,10 +4439,11 @@ async function journalForBranch(repo, branch, slug) {
|
|
|
4371
4439
|
const out = await $2({ reject: false })`git -C ${repo} show ${`origin/${branch}:journal/${slug}.md`}`;
|
|
4372
4440
|
return out.exitCode === 0 && out.stdout ? out.stdout : null;
|
|
4373
4441
|
}
|
|
4374
|
-
async function prepareDetachedWorktree(repo, branch, worktree) {
|
|
4442
|
+
async function prepareDetachedWorktree(repo, branch, worktree, opts = {}) {
|
|
4375
4443
|
await gitNetStrict(repo, ["fetch", "--quiet", "origin"], GIT_HEAVY_NET_TIMEOUT_MS);
|
|
4376
4444
|
const ref = `origin/${branch}`;
|
|
4377
4445
|
if (!await revParse(repo, `${ref}^{commit}`)) throw new MissingRefError(ref, repo);
|
|
4446
|
+
await clearWorktreePath(repo, worktree, branch, opts);
|
|
4378
4447
|
await $2`git -C ${repo} worktree add --detach --force ${worktree} ${ref}`;
|
|
4379
4448
|
await ensureWorktreeHygiene(worktree);
|
|
4380
4449
|
}
|
|
@@ -4417,8 +4486,14 @@ var init_git = __esm({
|
|
|
4417
4486
|
init_netTimeout();
|
|
4418
4487
|
init_swarmFiles();
|
|
4419
4488
|
WorktreeBusyError = class extends Error {
|
|
4420
|
-
|
|
4421
|
-
|
|
4489
|
+
/**
|
|
4490
|
+
* `why` (TASWM-553) — ПОЧЕМУ путь не отдали, если причина не «его держит живой прогон»:
|
|
4491
|
+
* основной чекаут по пути worktree, каталог с несданной работой, каталог, который не
|
|
4492
|
+
* получилось убрать. Дефолтная формулировка («прибери worktree или отмени прогон»)
|
|
4493
|
+
* человеку в таких случаях не помогает — она отправляет его не туда.
|
|
4494
|
+
*/
|
|
4495
|
+
constructor(branch, worktreePath, why) {
|
|
4496
|
+
super(why ?? `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})`);
|
|
4422
4497
|
this.branch = branch;
|
|
4423
4498
|
this.worktreePath = worktreePath;
|
|
4424
4499
|
this.name = "WorktreeBusyError";
|
|
@@ -4705,8 +4780,8 @@ function startDroneChannel(deps, port) {
|
|
|
4705
4780
|
return { port, server };
|
|
4706
4781
|
}
|
|
4707
4782
|
function closeDroneChannel(server) {
|
|
4708
|
-
return new Promise((
|
|
4709
|
-
server.close(() =>
|
|
4783
|
+
return new Promise((resolve5) => {
|
|
4784
|
+
server.close(() => resolve5());
|
|
4710
4785
|
server.closeAllConnections?.();
|
|
4711
4786
|
});
|
|
4712
4787
|
}
|
|
@@ -6061,7 +6136,7 @@ __export(db_exports, {
|
|
|
6061
6136
|
import { createRequire as createRequire2 } from "module";
|
|
6062
6137
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
6063
6138
|
import { existsSync as existsSync15 } from "fs";
|
|
6064
|
-
import { resolve as
|
|
6139
|
+
import { resolve as resolve4 } from "path";
|
|
6065
6140
|
function rowToQuestion(row) {
|
|
6066
6141
|
return {
|
|
6067
6142
|
id: row.id,
|
|
@@ -6690,7 +6765,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
6690
6765
|
* транзакции: прервётся — маркер не встанет, повтор безопасен.
|
|
6691
6766
|
*/
|
|
6692
6767
|
importDb(sourcePath, repo) {
|
|
6693
|
-
const abs =
|
|
6768
|
+
const abs = resolve4(sourcePath);
|
|
6694
6769
|
const repoN = normalizeRepo(repo);
|
|
6695
6770
|
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
6696
6771
|
if (already) return { imported: false, runs: already.run_count };
|
|
@@ -11327,7 +11402,7 @@ init_config();
|
|
|
11327
11402
|
// src/envCache.ts
|
|
11328
11403
|
init_config();
|
|
11329
11404
|
import { createHash as createHash2 } from "crypto";
|
|
11330
|
-
import { cp, link, lstat, mkdir as mkdir2, readdir, readlink, rename, rm as rm2, stat, symlink, utimes, writeFile as writeFile2 } from "fs/promises";
|
|
11405
|
+
import { cp, link, lstat, mkdir as mkdir2, readdir, readlink, rename as rename2, rm as rm2, stat, symlink, utimes, writeFile as writeFile2 } from "fs/promises";
|
|
11331
11406
|
import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
|
|
11332
11407
|
import { join as join22 } from "path";
|
|
11333
11408
|
function fingerprint(parts) {
|
|
@@ -11418,7 +11493,7 @@ async function saveEnvSnapshot(worktree, key, dirs, io = {}) {
|
|
|
11418
11493
|
for (let i = 0; i < present.length; i++) await clone(join22(worktree, present[i] ?? ""), join22(tmp, "trees", String(i)));
|
|
11419
11494
|
const manifest = { key, dirs: present, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
11420
11495
|
await writeFile2(join22(tmp, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
11421
|
-
await
|
|
11496
|
+
await rename2(tmp, snapshotDir(io, key));
|
|
11422
11497
|
return true;
|
|
11423
11498
|
} catch {
|
|
11424
11499
|
await rm2(tmp, { recursive: true, force: true }).catch(() => {
|
|
@@ -12219,7 +12294,7 @@ function parkReasonUnretriable(reason) {
|
|
|
12219
12294
|
// package.json
|
|
12220
12295
|
var package_default = {
|
|
12221
12296
|
name: "@taskless-app/swarm",
|
|
12222
|
-
version: "0.6.
|
|
12297
|
+
version: "0.6.40",
|
|
12223
12298
|
type: "module",
|
|
12224
12299
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
12225
12300
|
keywords: [
|
|
@@ -12270,7 +12345,7 @@ var package_default = {
|
|
|
12270
12345
|
execa: "^10.0.0",
|
|
12271
12346
|
zod: "3.25.76"
|
|
12272
12347
|
},
|
|
12273
|
-
gitHead: "
|
|
12348
|
+
gitHead: "b58ef18678e8c0e03e0602a40bf76d6f0a6975c8"
|
|
12274
12349
|
};
|
|
12275
12350
|
|
|
12276
12351
|
// src/version.ts
|
|
@@ -15307,11 +15382,11 @@ async function hasCommand(name, env) {
|
|
|
15307
15382
|
return hasCommandInPath(name, env);
|
|
15308
15383
|
}
|
|
15309
15384
|
async function portOpen(host, port) {
|
|
15310
|
-
return new Promise((
|
|
15385
|
+
return new Promise((resolve5) => {
|
|
15311
15386
|
const socket = createConnection({ host, port });
|
|
15312
15387
|
const finish = (ok) => {
|
|
15313
15388
|
socket.destroy();
|
|
15314
|
-
|
|
15389
|
+
resolve5(ok);
|
|
15315
15390
|
};
|
|
15316
15391
|
socket.setTimeout(PORT_TIMEOUT_MS);
|
|
15317
15392
|
socket.once("connect", () => finish(true));
|
|
@@ -16860,14 +16935,14 @@ function makeHandler(deps) {
|
|
|
16860
16935
|
const ref = url.searchParams.get("pr") ?? "";
|
|
16861
16936
|
if (!ref.trim()) return Response.json({ error: "\u043D\u0443\u0436\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 pr=<url|\u043D\u043E\u043C\u0435\u0440>" }, { status: 400 });
|
|
16862
16937
|
const ctxRepo = url.searchParams.get("repo");
|
|
16863
|
-
const
|
|
16938
|
+
const resolve5 = deps.mergeGate ?? makeMergeGateResolver({
|
|
16864
16939
|
listRuns: () => deps.db.listRuns(),
|
|
16865
16940
|
fallbackAutoReview: SWARM_AUTO_REVIEW,
|
|
16866
16941
|
autoVerify: SWARM_AUTO_VERIFY,
|
|
16867
16942
|
// TASWM-344
|
|
16868
16943
|
...defaultRepo ? { repoFallback: defaultRepo } : {}
|
|
16869
16944
|
});
|
|
16870
|
-
return Response.json(await
|
|
16945
|
+
return Response.json(await resolve5(ref, ctxRepo));
|
|
16871
16946
|
}
|
|
16872
16947
|
if (p === "/resources") return Response.json(hostResourceSnapshot());
|
|
16873
16948
|
if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
|
|
@@ -17020,8 +17095,8 @@ function startWeb(deps, port = WEB_PORT) {
|
|
|
17020
17095
|
return { port, server };
|
|
17021
17096
|
}
|
|
17022
17097
|
function closeWebServer(server) {
|
|
17023
|
-
return new Promise((
|
|
17024
|
-
server.close(() =>
|
|
17098
|
+
return new Promise((resolve5) => {
|
|
17099
|
+
server.close(() => resolve5());
|
|
17025
17100
|
server.closeAllConnections?.();
|
|
17026
17101
|
});
|
|
17027
17102
|
}
|
|
@@ -17355,7 +17430,7 @@ function executorHealthy(opts = {}) {
|
|
|
17355
17430
|
const executor = opts.executor ?? resolveExecutor();
|
|
17356
17431
|
const bin = opts.bin ?? executor.resolveBin();
|
|
17357
17432
|
const timeoutMs = opts.timeoutMs ?? 2e4;
|
|
17358
|
-
return new Promise((
|
|
17433
|
+
return new Promise((resolve5) => {
|
|
17359
17434
|
let done = false;
|
|
17360
17435
|
let out = "";
|
|
17361
17436
|
const finish = (r) => {
|
|
@@ -17366,7 +17441,7 @@ function executorHealthy(opts = {}) {
|
|
|
17366
17441
|
child.kill("SIGKILL");
|
|
17367
17442
|
} catch {
|
|
17368
17443
|
}
|
|
17369
|
-
|
|
17444
|
+
resolve5(r);
|
|
17370
17445
|
};
|
|
17371
17446
|
const child = spawn3(bin, [...executor.probe.args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
17372
17447
|
child.stdout?.on("data", (d) => {
|
|
@@ -21160,6 +21235,27 @@ async function replayDecision(deps, qid, chosen, by = "human") {
|
|
|
21160
21235
|
deps.db.supersede(old.id, fresh.id, by);
|
|
21161
21236
|
return fresh;
|
|
21162
21237
|
}
|
|
21238
|
+
var STOP_POLL_MS = 200;
|
|
21239
|
+
var STOP_TERM_GRACE_MS = 1e4;
|
|
21240
|
+
var STOP_KILL_GRACE_MS = 3e3;
|
|
21241
|
+
async function awaitDroneDeath(deps, pid) {
|
|
21242
|
+
const isAlive = deps.isAlive;
|
|
21243
|
+
if (!isAlive) return false;
|
|
21244
|
+
const wait = deps.wait ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
21245
|
+
const poll = async (budgetMs) => {
|
|
21246
|
+
for (let spent = 0; spent < budgetMs; spent += STOP_POLL_MS) {
|
|
21247
|
+
if (!isAlive(pid)) return true;
|
|
21248
|
+
await wait(STOP_POLL_MS);
|
|
21249
|
+
}
|
|
21250
|
+
return !isAlive(pid);
|
|
21251
|
+
};
|
|
21252
|
+
if (await poll(STOP_TERM_GRACE_MS)) return true;
|
|
21253
|
+
try {
|
|
21254
|
+
(deps.hardKill ?? deps.kill)(pid);
|
|
21255
|
+
} catch {
|
|
21256
|
+
}
|
|
21257
|
+
return await poll(STOP_KILL_GRACE_MS);
|
|
21258
|
+
}
|
|
21163
21259
|
async function cancelRun(deps, slug, reason) {
|
|
21164
21260
|
const run = deps.db.activeRunForSlug(slug);
|
|
21165
21261
|
if (!run) return null;
|
|
@@ -21168,11 +21264,21 @@ async function cancelRun(deps, slug, reason) {
|
|
|
21168
21264
|
deps.kill(run.pid);
|
|
21169
21265
|
} catch {
|
|
21170
21266
|
}
|
|
21267
|
+
if (deps.isAlive && !await awaitDroneDeath(deps, run.pid)) {
|
|
21268
|
+
deps.db.addEvent(
|
|
21269
|
+
run.id,
|
|
21270
|
+
"anomaly",
|
|
21271
|
+
`\u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0434\u0440\u043E\u043D\u0430 (pid ${run.pid}) \u043F\u0435\u0440\u0435\u0436\u0438\u043B \u0438 SIGTERM, \u0438 SIGKILL \u2014 worktree ${run.worktree_path ?? ""} \u0441\u043D\u043E\u0448\u0443 \u043F\u043E\u0432\u0435\u0440\u0445 \u0436\u0438\u0432\u043E\u0433\u043E. \u0415\u0441\u043B\u0438 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u044D\u0442\u043E\u0433\u043E \u0441\u043B\u0430\u0433\u0430 \u0443\u043F\u0440\u0451\u0442\u0441\u044F \u0432 \u0437\u0430\u043D\u044F\u0442\u044B\u0439 \u043F\u0443\u0442\u044C, \u043D\u0430\u0447\u043D\u0438\u0442\u0435 \u043E\u0442\u0441\u044E\u0434\u0430.`
|
|
21272
|
+
);
|
|
21273
|
+
}
|
|
21171
21274
|
}
|
|
21172
21275
|
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
21173
21276
|
});
|
|
21174
21277
|
return deps.db.cancel(run.id, reason);
|
|
21175
21278
|
}
|
|
21279
|
+
function detachedHelperWorktree(db) {
|
|
21280
|
+
return (repo, branch, wt) => prepareDetachedWorktree(repo, branch, wt, { isWorktreeHeldLive: (p) => worktreePathHeldLive(db, p) });
|
|
21281
|
+
}
|
|
21176
21282
|
async function resolveConflict(deps, reviewRun) {
|
|
21177
21283
|
const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
|
|
21178
21284
|
const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
|
|
@@ -21987,7 +22093,7 @@ async function runDaemon(opts) {
|
|
|
21987
22093
|
repoFallback: opts.repo,
|
|
21988
22094
|
worktreesDir,
|
|
21989
22095
|
launch,
|
|
21990
|
-
git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
|
|
22096
|
+
git: { prepareDetachedWorktree: detachedHelperWorktree(db), setWorktreeIdentity, removeWorktree },
|
|
21991
22097
|
// TASWM-356: ветка ревьюера — фактическая ветка PR. Штатно она уже в поле прогона
|
|
21992
22098
|
// (реконсайл синхронит её из headBranch), этот ридер — для прогонов без неё.
|
|
21993
22099
|
prHeadBranch,
|
|
@@ -22022,7 +22128,7 @@ async function runDaemon(opts) {
|
|
|
22022
22128
|
repoFallback: opts.repo,
|
|
22023
22129
|
worktreesDir,
|
|
22024
22130
|
launch,
|
|
22025
|
-
git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
|
|
22131
|
+
git: { prepareDetachedWorktree: detachedHelperWorktree(db), setWorktreeIdentity, removeWorktree },
|
|
22026
22132
|
prHeadBranch,
|
|
22027
22133
|
...verifyModeReader ? { verifyMode: verifyModeReader } : {},
|
|
22028
22134
|
...checkerAttachments ? { taskAttachments: checkerAttachments } : {}
|
|
@@ -22366,7 +22472,7 @@ async function runDaemon(opts) {
|
|
|
22366
22472
|
repoFallback: opts.repo,
|
|
22367
22473
|
worktreesDir,
|
|
22368
22474
|
launch,
|
|
22369
|
-
git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree },
|
|
22475
|
+
git: { prepareDetachedWorktree: detachedHelperWorktree(db), setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree },
|
|
22370
22476
|
prHeadBranch
|
|
22371
22477
|
// TASWM-356: ветка PR для прогонов без записанного поля branch
|
|
22372
22478
|
},
|
|
@@ -22564,6 +22670,10 @@ async function runDaemon(opts) {
|
|
|
22564
22670
|
{
|
|
22565
22671
|
db,
|
|
22566
22672
|
kill: (pid) => killProcessGroup(pid, "SIGTERM"),
|
|
22673
|
+
// TASWM-553: остановка обязана ОСВОБОДИТЬ машину — значит, дождаться смерти
|
|
22674
|
+
// процесса и только потом сносить worktree (иначе платит следующий спавн).
|
|
22675
|
+
isAlive: pidAlive,
|
|
22676
|
+
hardKill: (pid) => killProcessGroup(pid, "SIGKILL"),
|
|
22567
22677
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
|
|
22568
22678
|
},
|
|
22569
22679
|
slug,
|
|
@@ -22749,6 +22859,9 @@ async function runDaemon(opts) {
|
|
|
22749
22859
|
{
|
|
22750
22860
|
db,
|
|
22751
22861
|
kill: (pid) => killProcessGroup(pid, "SIGTERM"),
|
|
22862
|
+
isAlive: pidAlive,
|
|
22863
|
+
// TASWM-553: снос worktree — после смерти процесса, не следом за SIGTERM
|
|
22864
|
+
hardKill: (pid) => killProcessGroup(pid, "SIGKILL"),
|
|
22752
22865
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
|
|
22753
22866
|
},
|
|
22754
22867
|
slug,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taskless-app/swarm",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.40",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
6
6
|
"keywords": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"execa": "^10.0.0",
|
|
40
40
|
"zod": "3.25.76"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "b58ef18678e8c0e03e0602a40bf76d6f0a6975c8",
|
|
43
43
|
"scripts": {
|
|
44
44
|
"overmind": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/overmind.ts",
|
|
45
45
|
"swarm": "NODE_OPTIONS=\"${NODE_OPTIONS:-} --conditions=development\" tsx src/cli.ts",
|