@taskless-app/swarm 0.6.38 → 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 +364 -168
- package/dist/overmind.js +752 -278
- package/package.json +2 -2
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
|
}
|
|
@@ -2211,10 +2211,94 @@ var init_claudeResult = __esm({
|
|
|
2211
2211
|
}
|
|
2212
2212
|
});
|
|
2213
2213
|
|
|
2214
|
-
// src/
|
|
2215
|
-
import { readdirSync as readdirSync2, statSync
|
|
2214
|
+
// src/transcriptSpend.ts
|
|
2215
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync2, statSync } from "fs";
|
|
2216
2216
|
import { homedir as homedir2 } from "os";
|
|
2217
2217
|
import { join as join3 } from "path";
|
|
2218
|
+
function transcriptDir(worktree, projectsDir) {
|
|
2219
|
+
return join3(projectsDir, worktree.replace(/[/.]/g, "-"));
|
|
2220
|
+
}
|
|
2221
|
+
function transcriptSpendInWindow(worktree, fromMs, toMs, opts = {}) {
|
|
2222
|
+
const projectsDir = opts.projectsDir ?? join3(homedir2(), ".claude", "projects");
|
|
2223
|
+
const empty = { costUsd: 0, tokensIn: 0, tokensOut: 0, durationMs: 0, replies: 0, unpriced: 0, unpricedModel: null };
|
|
2224
|
+
const dir = transcriptDir(worktree, projectsDir);
|
|
2225
|
+
let files;
|
|
2226
|
+
try {
|
|
2227
|
+
files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl"));
|
|
2228
|
+
} catch {
|
|
2229
|
+
return empty;
|
|
2230
|
+
}
|
|
2231
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2232
|
+
let costUsd = 0;
|
|
2233
|
+
let tokensIn = 0;
|
|
2234
|
+
let tokensOut = 0;
|
|
2235
|
+
let firstAt = null;
|
|
2236
|
+
let lastAt = null;
|
|
2237
|
+
let replies = 0;
|
|
2238
|
+
let unpriced = 0;
|
|
2239
|
+
let unpricedModel = null;
|
|
2240
|
+
for (const f of files) {
|
|
2241
|
+
const path = join3(dir, f);
|
|
2242
|
+
let raw;
|
|
2243
|
+
try {
|
|
2244
|
+
if (statSync(path).mtimeMs < fromMs) continue;
|
|
2245
|
+
raw = readFileSync3(path, "utf8");
|
|
2246
|
+
} catch {
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
for (const line of raw.split("\n")) {
|
|
2250
|
+
if (!line.startsWith("{") || !line.includes('"usage"')) continue;
|
|
2251
|
+
let entry;
|
|
2252
|
+
try {
|
|
2253
|
+
entry = JSON.parse(line);
|
|
2254
|
+
} catch {
|
|
2255
|
+
continue;
|
|
2256
|
+
}
|
|
2257
|
+
if (entry.type !== "assistant") continue;
|
|
2258
|
+
const usage = entry.message?.usage;
|
|
2259
|
+
if (!usage) continue;
|
|
2260
|
+
const at = entry.timestamp ? Date.parse(entry.timestamp) : NaN;
|
|
2261
|
+
if (!Number.isFinite(at) || at < fromMs || at >= toMs) continue;
|
|
2262
|
+
const key = entry.requestId ?? entry.message?.id ?? entry.uuid;
|
|
2263
|
+
if (!key || seen.has(key)) continue;
|
|
2264
|
+
seen.add(key);
|
|
2265
|
+
const model = entry.message?.model ?? null;
|
|
2266
|
+
const est = costFromUsage(usage, model, opts.table);
|
|
2267
|
+
costUsd += est.costUsd;
|
|
2268
|
+
if (est.warning) {
|
|
2269
|
+
unpriced += 1;
|
|
2270
|
+
unpricedModel ??= model;
|
|
2271
|
+
}
|
|
2272
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
2273
|
+
tokensIn += num(usage.input_tokens) + num(usage.cache_creation_input_tokens) + num(usage.cache_read_input_tokens);
|
|
2274
|
+
tokensOut += num(usage.output_tokens);
|
|
2275
|
+
if (firstAt === null || at < firstAt) firstAt = at;
|
|
2276
|
+
if (lastAt === null || at > lastAt) lastAt = at;
|
|
2277
|
+
replies += 1;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
if (replies === 0) return empty;
|
|
2281
|
+
return {
|
|
2282
|
+
costUsd,
|
|
2283
|
+
tokensIn,
|
|
2284
|
+
tokensOut,
|
|
2285
|
+
durationMs: firstAt !== null && lastAt !== null ? lastAt - firstAt : 0,
|
|
2286
|
+
replies,
|
|
2287
|
+
unpriced,
|
|
2288
|
+
unpricedModel
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
var init_transcriptSpend = __esm({
|
|
2292
|
+
"src/transcriptSpend.ts"() {
|
|
2293
|
+
"use strict";
|
|
2294
|
+
init_pricing();
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
// src/executors/claudeCode.ts
|
|
2299
|
+
import { readdirSync as readdirSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2300
|
+
import { homedir as homedir3 } from "os";
|
|
2301
|
+
import { join as join4 } from "path";
|
|
2218
2302
|
function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings, maxBudgetUsd, effort) {
|
|
2219
2303
|
const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
|
|
2220
2304
|
const budgetFlags = typeof maxBudgetUsd === "number" && maxBudgetUsd > 0 ? ["--max-budget-usd", String(maxBudgetUsd)] : [];
|
|
@@ -2246,21 +2330,21 @@ function droneSettingsJson(paths, mergeGateHook = null) {
|
|
|
2246
2330
|
if (mergeGateHook) settings.hooks = { PreToolUse: [{ matcher: "Bash", hooks: [{ type: "command", command: mergeGateHook }] }] };
|
|
2247
2331
|
return JSON.stringify(settings);
|
|
2248
2332
|
}
|
|
2249
|
-
function
|
|
2250
|
-
return
|
|
2333
|
+
function transcriptDir2(worktree, projectsDir) {
|
|
2334
|
+
return join4(projectsDir, worktree.replace(/[/.]/g, "-"));
|
|
2251
2335
|
}
|
|
2252
|
-
function sessionTranscriptExists(worktree, projectsDir =
|
|
2336
|
+
function sessionTranscriptExists(worktree, projectsDir = join4(homedir3(), ".claude", "projects")) {
|
|
2253
2337
|
try {
|
|
2254
|
-
return
|
|
2338
|
+
return readdirSync3(transcriptDir2(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
|
|
2255
2339
|
} catch {
|
|
2256
2340
|
return false;
|
|
2257
2341
|
}
|
|
2258
2342
|
}
|
|
2259
|
-
function transcriptMtimeMs(worktree, projectsDir =
|
|
2260
|
-
const dir =
|
|
2343
|
+
function transcriptMtimeMs(worktree, projectsDir = join4(homedir3(), ".claude", "projects")) {
|
|
2344
|
+
const dir = transcriptDir2(worktree, projectsDir);
|
|
2261
2345
|
let files;
|
|
2262
2346
|
try {
|
|
2263
|
-
files =
|
|
2347
|
+
files = readdirSync3(dir);
|
|
2264
2348
|
} catch {
|
|
2265
2349
|
return null;
|
|
2266
2350
|
}
|
|
@@ -2268,7 +2352,7 @@ function transcriptMtimeMs(worktree, projectsDir = join3(homedir2(), ".claude",
|
|
|
2268
2352
|
for (const f of files) {
|
|
2269
2353
|
if (!f.endsWith(".jsonl")) continue;
|
|
2270
2354
|
try {
|
|
2271
|
-
const mtime2 =
|
|
2355
|
+
const mtime2 = statSync2(join4(dir, f)).mtimeMs;
|
|
2272
2356
|
if (latest === null || mtime2 > latest) latest = mtime2;
|
|
2273
2357
|
} catch {
|
|
2274
2358
|
}
|
|
@@ -2281,7 +2365,7 @@ function writeMcpConfig(spec) {
|
|
|
2281
2365
|
[spec.mcp.name]: { command: spec.mcp.command, args: spec.mcp.args, env: spec.mcp.env }
|
|
2282
2366
|
}
|
|
2283
2367
|
};
|
|
2284
|
-
const path =
|
|
2368
|
+
const path = join4(spec.worktree, MCP_CONFIG_FILE);
|
|
2285
2369
|
writeFileSync3(path, JSON.stringify(cfg, null, 2));
|
|
2286
2370
|
return path;
|
|
2287
2371
|
}
|
|
@@ -2296,6 +2380,7 @@ var init_claudeCode = __esm({
|
|
|
2296
2380
|
init_swarmFiles();
|
|
2297
2381
|
init_mergeGateHook();
|
|
2298
2382
|
init_claudeResult();
|
|
2383
|
+
init_transcriptSpend();
|
|
2299
2384
|
ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
|
|
2300
2385
|
MCP_CONFIG_FILE = SWARM_MCP_CONFIG_FILE;
|
|
2301
2386
|
claudeCodeExecutor = {
|
|
@@ -2334,6 +2419,26 @@ var init_claudeCode = __esm({
|
|
|
2334
2419
|
...abortField(sessionAbort(result))
|
|
2335
2420
|
};
|
|
2336
2421
|
},
|
|
2422
|
+
// TASWM-548: итога нет — берём трату из транскрипта сессии. Тот пишется по ходу работы и
|
|
2423
|
+
// переживает SIGTERM разгрузки, из-за которого итог и не был напечатан; формат usage там
|
|
2424
|
+
// тот же, что в итоге, поэтому цена считается тем же прайсингом (см. transcriptSpend.ts).
|
|
2425
|
+
rescueSpend(worktree, fromMs, toMs) {
|
|
2426
|
+
const spend = transcriptSpendInWindow(worktree, fromMs, toMs);
|
|
2427
|
+
if (spend.replies === 0) return null;
|
|
2428
|
+
return {
|
|
2429
|
+
costUsd: spend.costUsd,
|
|
2430
|
+
tokensIn: spend.tokensIn,
|
|
2431
|
+
tokensOut: spend.tokensOut,
|
|
2432
|
+
durationMs: spend.durationMs,
|
|
2433
|
+
// Часть ответов не по чему было тарифицировать (модели нет в MODEL_PRICING) — их
|
|
2434
|
+
// токены в трату вошли, а деньги НЕТ. Тот же канал, что и у warning штатного итога
|
|
2435
|
+
// (cost.accountRunCost его печатает): иначе спасённые $0 при сожжённых токенах
|
|
2436
|
+
// выглядели бы как честный ноль.
|
|
2437
|
+
...spend.unpriced > 0 ? {
|
|
2438
|
+
warning: `\u0437\u0430\u043F\u0438\u0441\u044C \u0441\u0435\u0441\u0441\u0438\u0438: ${spend.unpriced} \u0438\u0437 ${spend.replies} \u043E\u0442\u0432\u0435\u0442\u043E\u0432 \u0431\u0435\u0437 \u0441\u0442\u0440\u043E\u043A\u0438 \u0432 \u043F\u0440\u0430\u0439\u0441\u0438\u043D\u0433\u0435 (\u043C\u043E\u0434\u0435\u043B\u044C "${spend.unpricedModel ?? ""}") \u2014 \u0438\u0445 \u0446\u0435\u043D\u0430 \u0432 \u0441\u043F\u0430\u0441\u0451\u043D\u043D\u0443\u044E \u0442\u0440\u0430\u0442\u0443 \u041D\u0415 \u0432\u043E\u0448\u043B\u0430`
|
|
2439
|
+
} : {}
|
|
2440
|
+
};
|
|
2441
|
+
},
|
|
2337
2442
|
session: {
|
|
2338
2443
|
exists: (worktree) => sessionTranscriptExists(worktree),
|
|
2339
2444
|
mtimeMs: (worktree) => transcriptMtimeMs(worktree)
|
|
@@ -2351,34 +2456,34 @@ var init_claudeCode = __esm({
|
|
|
2351
2456
|
});
|
|
2352
2457
|
|
|
2353
2458
|
// src/swarmHome.ts
|
|
2354
|
-
import { homedir as
|
|
2355
|
-
import { join as
|
|
2459
|
+
import { homedir as homedir4 } from "os";
|
|
2460
|
+
import { join as join5 } from "path";
|
|
2356
2461
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
2357
2462
|
var SWARM_HOME;
|
|
2358
2463
|
var init_swarmHome = __esm({
|
|
2359
2464
|
"src/swarmHome.ts"() {
|
|
2360
2465
|
"use strict";
|
|
2361
|
-
SWARM_HOME = process.env.SWARM_HOME ||
|
|
2466
|
+
SWARM_HOME = process.env.SWARM_HOME || join5(homedir4(), ".taskless-swarm");
|
|
2362
2467
|
mkdirSync2(SWARM_HOME, { recursive: true });
|
|
2363
2468
|
}
|
|
2364
2469
|
});
|
|
2365
2470
|
|
|
2366
2471
|
// src/executors/codex.ts
|
|
2367
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as
|
|
2368
|
-
import { join as
|
|
2472
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, statSync as statSync3, symlinkSync } from "fs";
|
|
2473
|
+
import { join as join6 } from "path";
|
|
2369
2474
|
function resolveCodexBin(env = process.env) {
|
|
2370
2475
|
return (env.SWARM_CODEX_BIN ?? "").trim() || "codex";
|
|
2371
2476
|
}
|
|
2372
2477
|
function codexHomePath(worktree, swarmHome = SWARM_HOME) {
|
|
2373
|
-
return
|
|
2478
|
+
return join6(swarmHome, "executor-state", "codex", worktree.replace(/[/.]/g, "-"));
|
|
2374
2479
|
}
|
|
2375
2480
|
function ensureCodexHome(worktree, realHome, env = process.env, swarmHome = SWARM_HOME) {
|
|
2376
2481
|
const home = codexHomePath(worktree, swarmHome);
|
|
2377
2482
|
mkdirSync3(home, { recursive: true });
|
|
2378
|
-
const real = (env.CODEX_HOME ?? "").trim() ||
|
|
2483
|
+
const real = (env.CODEX_HOME ?? "").trim() || join6(realHome, ".codex");
|
|
2379
2484
|
for (const file of ["auth.json", "config.toml"]) {
|
|
2380
|
-
const target =
|
|
2381
|
-
const link2 =
|
|
2485
|
+
const target = join6(real, file);
|
|
2486
|
+
const link2 = join6(home, file);
|
|
2382
2487
|
if (!existsSync4(target) || existsSync4(link2)) continue;
|
|
2383
2488
|
try {
|
|
2384
2489
|
symlinkSync(target, link2);
|
|
@@ -2437,16 +2542,16 @@ function parseCodexStream(raw) {
|
|
|
2437
2542
|
function newestRolloutMtime(dir, depth = 4) {
|
|
2438
2543
|
let entries;
|
|
2439
2544
|
try {
|
|
2440
|
-
entries =
|
|
2545
|
+
entries = readdirSync4(dir);
|
|
2441
2546
|
} catch {
|
|
2442
2547
|
return null;
|
|
2443
2548
|
}
|
|
2444
2549
|
let latest = null;
|
|
2445
2550
|
for (const name of entries) {
|
|
2446
|
-
const full =
|
|
2551
|
+
const full = join6(dir, name);
|
|
2447
2552
|
let st;
|
|
2448
2553
|
try {
|
|
2449
|
-
st =
|
|
2554
|
+
st = statSync3(full);
|
|
2450
2555
|
} catch {
|
|
2451
2556
|
continue;
|
|
2452
2557
|
}
|
|
@@ -2517,8 +2622,8 @@ ${notice}` : spec.prompt;
|
|
|
2517
2622
|
return est.warning ? { costUsd: est.costUsd, warning: est.warning } : { costUsd: est.costUsd };
|
|
2518
2623
|
},
|
|
2519
2624
|
session: {
|
|
2520
|
-
exists: (worktree) => newestRolloutMtime(
|
|
2521
|
-
mtimeMs: (worktree) => newestRolloutMtime(
|
|
2625
|
+
exists: (worktree) => newestRolloutMtime(join6(codexHomePath(worktree), "sessions")) !== null,
|
|
2626
|
+
mtimeMs: (worktree) => newestRolloutMtime(join6(codexHomePath(worktree), "sessions"))
|
|
2522
2627
|
},
|
|
2523
2628
|
probe: {
|
|
2524
2629
|
// Тот же принцип, что у claude: дешёвый реальный прогон — он проверяет и auth, и живость
|
|
@@ -2666,8 +2771,8 @@ __export(config_exports, {
|
|
|
2666
2771
|
restEndpoint: () => restEndpoint,
|
|
2667
2772
|
reviewCutoffAt: () => reviewCutoffAt
|
|
2668
2773
|
});
|
|
2669
|
-
import { basename, join as
|
|
2670
|
-
import { readFileSync as
|
|
2774
|
+
import { basename, join as join7, resolve } from "path";
|
|
2775
|
+
import { readFileSync as readFileSync4, realpathSync, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2671
2776
|
function normalizeRepo(repo) {
|
|
2672
2777
|
const r = (repo ?? "").trim();
|
|
2673
2778
|
if (!r) return "";
|
|
@@ -2681,10 +2786,10 @@ function effectiveRepoOf(repo, fallback) {
|
|
|
2681
2786
|
return normalizeRepo(repo || fallback || "");
|
|
2682
2787
|
}
|
|
2683
2788
|
function droneWorktreePath(worktreesDir, repo, slug) {
|
|
2684
|
-
return
|
|
2789
|
+
return join7(droneWorktreeRoot(worktreesDir, repo), slug);
|
|
2685
2790
|
}
|
|
2686
2791
|
function droneWorktreeRoot(worktreesDir, repo) {
|
|
2687
|
-
return
|
|
2792
|
+
return join7(worktreesDir, basename(normalizeRepo(repo)));
|
|
2688
2793
|
}
|
|
2689
2794
|
function resolveEffectiveWorktreesDir(deps) {
|
|
2690
2795
|
if (deps.envOverride) return deps.envOverride;
|
|
@@ -2732,14 +2837,14 @@ function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
|
|
|
2732
2837
|
}
|
|
2733
2838
|
function tokenFromFile() {
|
|
2734
2839
|
try {
|
|
2735
|
-
return
|
|
2840
|
+
return readFileSync4(join7(SWARM_HOME, "token"), "utf8").trim();
|
|
2736
2841
|
} catch {
|
|
2737
2842
|
return "";
|
|
2738
2843
|
}
|
|
2739
2844
|
}
|
|
2740
2845
|
function reviewCutoffAt() {
|
|
2741
2846
|
try {
|
|
2742
|
-
const raw = JSON.parse(
|
|
2847
|
+
const raw = JSON.parse(readFileSync4(REVIEW_CUTOFF_PATH, "utf8"));
|
|
2743
2848
|
if (raw.at) return raw.at;
|
|
2744
2849
|
} catch {
|
|
2745
2850
|
}
|
|
@@ -2760,25 +2865,25 @@ var init_config = __esm({
|
|
|
2760
2865
|
init_executors();
|
|
2761
2866
|
init_swarmHome();
|
|
2762
2867
|
init_swarmHome();
|
|
2763
|
-
DB_PATH = process.env.SWARM_DB ||
|
|
2868
|
+
DB_PATH = process.env.SWARM_DB || join7(SWARM_HOME, "swarm.db");
|
|
2764
2869
|
EXECUTOR = resolveExecutor(process.env.SWARM_EXECUTOR);
|
|
2765
|
-
DRONE_HOME = process.env.SWARM_DRONE_HOME ||
|
|
2870
|
+
DRONE_HOME = process.env.SWARM_DRONE_HOME || join7(SWARM_HOME, "drone-home");
|
|
2766
2871
|
DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
|
|
2767
2872
|
DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
|
|
2768
|
-
WORKTREES_NOINDEX_DIR =
|
|
2769
|
-
WORKTREES_LEGACY_DIR =
|
|
2873
|
+
WORKTREES_NOINDEX_DIR = join7(SWARM_HOME, "worktrees.noindex");
|
|
2874
|
+
WORKTREES_LEGACY_DIR = join7(SWARM_HOME, "worktrees");
|
|
2770
2875
|
WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
|
|
2771
|
-
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS ||
|
|
2772
|
-
DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG ||
|
|
2876
|
+
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join7(SWARM_HOME, "run-logs");
|
|
2877
|
+
DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join7(SWARM_HOME, "overmind.log");
|
|
2773
2878
|
DAEMON_LOG_MAX_BYTES = envNum("SWARM_DAEMON_LOG_MAX_BYTES", 16 * 1024 * 1024);
|
|
2774
2879
|
DAEMON_LOG_KEEP = envNum("SWARM_DAEMON_LOG_KEEP", 2);
|
|
2775
2880
|
DAEMON_LOG_CHECK_MS = envNum("SWARM_DAEMON_LOG_CHECK_MS", 6e4);
|
|
2776
2881
|
DIST_BUILD = true;
|
|
2777
2882
|
SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
|
|
2778
|
-
SWARM_OUTBOX = process.env.SWARM_OUTBOX ||
|
|
2883
|
+
SWARM_OUTBOX = process.env.SWARM_OUTBOX || join7(SWARM_HOME, "outbox.jsonl");
|
|
2779
2884
|
SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
|
|
2780
2885
|
SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
|
|
2781
|
-
ENV_CACHE_DIR = process.env.SWARM_ENV_CACHE_DIR ||
|
|
2886
|
+
ENV_CACHE_DIR = process.env.SWARM_ENV_CACHE_DIR || join7(SWARM_HOME, "env-cache");
|
|
2782
2887
|
ENV_CACHE_ENABLED = process.env.SWARM_ENV_CACHE !== "0";
|
|
2783
2888
|
ENV_CACHE_KEEP = Math.trunc(envNum("SWARM_ENV_CACHE_KEEP", 4));
|
|
2784
2889
|
SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
|
|
@@ -2808,15 +2913,15 @@ var init_config = __esm({
|
|
|
2808
2913
|
DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
|
|
2809
2914
|
DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
|
|
2810
2915
|
DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
|
|
2811
|
-
DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET ||
|
|
2916
|
+
DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join7(SWARM_HOME, "drone-channel.key");
|
|
2812
2917
|
DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
|
|
2813
|
-
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT ||
|
|
2918
|
+
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join7(SWARM_HOME, "overmind.heartbeat");
|
|
2814
2919
|
HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
2815
2920
|
SWARM_REPO = process.env.SWARM_REPO || "";
|
|
2816
2921
|
SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
|
|
2817
2922
|
SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
|
|
2818
2923
|
SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
|
|
2819
|
-
REPO_MAP_PATH = process.env.SWARM_REPO_MAP ||
|
|
2924
|
+
REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join7(SWARM_HOME, "repo-map.json");
|
|
2820
2925
|
SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
|
|
2821
2926
|
SWARM_REPO_SCAN_TTL_MS = envNum("SWARM_REPO_SCAN_TTL_MS", 24 * 60 * 6e4);
|
|
2822
2927
|
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
@@ -2831,7 +2936,7 @@ var init_config = __esm({
|
|
|
2831
2936
|
SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
|
|
2832
2937
|
SWARM_AUTO_VERIFY = process.env.SWARM_AUTO_VERIFY !== "0";
|
|
2833
2938
|
SWARM_MERGE_QUEUE = process.env.SWARM_MERGE_QUEUE !== "0";
|
|
2834
|
-
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF ||
|
|
2939
|
+
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join7(SWARM_HOME, "review-cutoff.json");
|
|
2835
2940
|
SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
|
|
2836
2941
|
SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
|
|
2837
2942
|
WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
|
|
@@ -2871,7 +2976,7 @@ var init_quietExec = __esm({
|
|
|
2871
2976
|
|
|
2872
2977
|
// src/resources.ts
|
|
2873
2978
|
import os from "os";
|
|
2874
|
-
import { readFileSync as
|
|
2979
|
+
import { readFileSync as readFileSync5, statfsSync } from "fs";
|
|
2875
2980
|
function isTransientResourceError(e) {
|
|
2876
2981
|
const code = e?.code;
|
|
2877
2982
|
return code === "ENFILE" || code === "EMFILE" || code === "EAGAIN";
|
|
@@ -2929,7 +3034,7 @@ function fileDescriptorLoadRatio() {
|
|
|
2929
3034
|
}
|
|
2930
3035
|
if (process.platform === "linux") {
|
|
2931
3036
|
try {
|
|
2932
|
-
const [allocated, , max] =
|
|
3037
|
+
const [allocated, , max] = readFileSync5("/proc/sys/fs/file-nr", "utf8").trim().split(/\s+/).map(Number);
|
|
2933
3038
|
if (Number.isFinite(allocated ?? NaN) && Number.isFinite(max ?? NaN) && max) return allocated / max;
|
|
2934
3039
|
} catch {
|
|
2935
3040
|
}
|
|
@@ -3031,6 +3136,14 @@ function hostResourcesOk() {
|
|
|
3031
3136
|
function reliefDecision(r, minFreeGb = SWARM_RELIEF_MIN_FREE_GB, maxCpuBusy = SWARM_RELIEF_CPU_BUSY, maxFdLoadFactor = SWARM_RELIEF_MAX_FD_LOAD_FACTOR) {
|
|
3032
3137
|
const cores = r.cpus > 0 ? r.cpus : 1;
|
|
3033
3138
|
const pct = (v) => `${Math.round(v * 100)}%`;
|
|
3139
|
+
const pctPair = (v, threshold) => {
|
|
3140
|
+
for (const digits of [0, 1, 2, 3, 4]) {
|
|
3141
|
+
const a = (v * 100).toFixed(digits);
|
|
3142
|
+
const b = (threshold * 100).toFixed(digits);
|
|
3143
|
+
if (a !== b) return `${a}% \u043F\u0440\u0438 \u043F\u043E\u0440\u043E\u0433\u0435 ${b}%`;
|
|
3144
|
+
}
|
|
3145
|
+
return `${pct(v)} \u043F\u0440\u0438 \u043F\u043E\u0440\u043E\u0433\u0435 ${pct(threshold)}`;
|
|
3146
|
+
};
|
|
3034
3147
|
const signals = [];
|
|
3035
3148
|
signals.push(
|
|
3036
3149
|
r.cpuBusy == null ? {
|
|
@@ -3039,7 +3152,7 @@ function reliefDecision(r, minFreeGb = SWARM_RELIEF_MIN_FREE_GB, maxCpuBusy = SW
|
|
|
3039
3152
|
tripped: false
|
|
3040
3153
|
} : {
|
|
3041
3154
|
key: "cpu",
|
|
3042
|
-
text: `CPU \u0437\u0430\u043D\u044F\u0442 ${
|
|
3155
|
+
text: `CPU \u0437\u0430\u043D\u044F\u0442 ${pctPair(r.cpuBusy, maxCpuBusy)}`,
|
|
3043
3156
|
tripped: r.cpuBusy > maxCpuBusy
|
|
3044
3157
|
}
|
|
3045
3158
|
);
|
|
@@ -3051,7 +3164,7 @@ function reliefDecision(r, minFreeGb = SWARM_RELIEF_MIN_FREE_GB, maxCpuBusy = SW
|
|
|
3051
3164
|
signals.push(
|
|
3052
3165
|
r.fdLoadRatio == null ? { key: "fd", text: "\u0434\u0435\u0441\u043A\u0440\u0438\u043F\u0442\u043E\u0440\u044B: \u0438\u0437\u043C\u0435\u0440\u0438\u0442\u044C \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C", tripped: false } : {
|
|
3053
3166
|
key: "fd",
|
|
3054
|
-
text: `\u0434\u0435\u0441\u043A\u0440\u0438\u043F\u0442\u043E\u0440\u044B \u0437\u0430\u043D\u044F\u0442\u044B ${
|
|
3167
|
+
text: `\u0434\u0435\u0441\u043A\u0440\u0438\u043F\u0442\u043E\u0440\u044B \u0437\u0430\u043D\u044F\u0442\u044B ${pctPair(r.fdLoadRatio, maxFdLoadFactor)}`,
|
|
3055
3168
|
tripped: r.fdLoadRatio > maxFdLoadFactor
|
|
3056
3169
|
}
|
|
3057
3170
|
);
|
|
@@ -3335,11 +3448,11 @@ var init_zoneNotes = __esm({
|
|
|
3335
3448
|
});
|
|
3336
3449
|
|
|
3337
3450
|
// src/agentToken.ts
|
|
3338
|
-
import { readFileSync as
|
|
3339
|
-
import { join as
|
|
3451
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
3452
|
+
import { join as join8 } from "path";
|
|
3340
3453
|
function loadAgentToken() {
|
|
3341
3454
|
try {
|
|
3342
|
-
const s = JSON.parse(
|
|
3455
|
+
const s = JSON.parse(readFileSync6(STORE_PATH, "utf8"));
|
|
3343
3456
|
return s.token ? s : null;
|
|
3344
3457
|
} catch {
|
|
3345
3458
|
return null;
|
|
@@ -3392,7 +3505,7 @@ var init_agentToken = __esm({
|
|
|
3392
3505
|
"src/agentToken.ts"() {
|
|
3393
3506
|
"use strict";
|
|
3394
3507
|
init_config();
|
|
3395
|
-
STORE_PATH =
|
|
3508
|
+
STORE_PATH = join8(SWARM_HOME, "agent.json");
|
|
3396
3509
|
base = (api) => api.replace(/\/$/, "");
|
|
3397
3510
|
TOKEN_PROBE_TIMEOUT_MS = 8e3;
|
|
3398
3511
|
}
|
|
@@ -3506,7 +3619,7 @@ var init_toolFailure = __esm({
|
|
|
3506
3619
|
});
|
|
3507
3620
|
|
|
3508
3621
|
// src/heartbeat.ts
|
|
3509
|
-
import { readFileSync as
|
|
3622
|
+
import { readFileSync as readFileSync8, renameSync as renameSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
3510
3623
|
function writeHeartbeat(path, now) {
|
|
3511
3624
|
const tmp = `${path}.tmp`;
|
|
3512
3625
|
writeFileSync7(tmp, String(now));
|
|
@@ -3514,7 +3627,7 @@ function writeHeartbeat(path, now) {
|
|
|
3514
3627
|
}
|
|
3515
3628
|
function readHeartbeat(path) {
|
|
3516
3629
|
try {
|
|
3517
|
-
const ms = Number(
|
|
3630
|
+
const ms = Number(readFileSync8(path, "utf8").trim());
|
|
3518
3631
|
return Number.isFinite(ms) ? ms : null;
|
|
3519
3632
|
} catch {
|
|
3520
3633
|
return null;
|
|
@@ -3782,7 +3895,7 @@ var init_procList = __esm({
|
|
|
3782
3895
|
|
|
3783
3896
|
// src/procCleanup.ts
|
|
3784
3897
|
function sleep(ms) {
|
|
3785
|
-
return new Promise((
|
|
3898
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
3786
3899
|
}
|
|
3787
3900
|
function parseLsofCwd(output) {
|
|
3788
3901
|
const rows = [];
|
|
@@ -4000,9 +4113,9 @@ var init_procCleanup = __esm({
|
|
|
4000
4113
|
});
|
|
4001
4114
|
|
|
4002
4115
|
// src/git.ts
|
|
4003
|
-
import { existsSync as existsSync12, readdirSync as
|
|
4004
|
-
import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
|
|
4005
|
-
import { isAbsolute as isAbsolute2, join as
|
|
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";
|
|
4006
4119
|
import { $ as $2 } from "execa";
|
|
4007
4120
|
function gitNet(repo, args, timeoutMs) {
|
|
4008
4121
|
return withHardTimeout(
|
|
@@ -4023,9 +4136,75 @@ function isUnrecoverableRefError(e) {
|
|
|
4023
4136
|
async function reclaimStaleWorktree(repo, worktree) {
|
|
4024
4137
|
await detachWorktree(repo, worktree);
|
|
4025
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
|
+
}
|
|
4026
4205
|
function isLinkedWorktree(dir) {
|
|
4027
4206
|
try {
|
|
4028
|
-
return
|
|
4207
|
+
return statSync10(join20(dir, ".git")).isFile();
|
|
4029
4208
|
} catch {
|
|
4030
4209
|
return false;
|
|
4031
4210
|
}
|
|
@@ -4095,6 +4274,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
|
|
|
4095
4274
|
const originSha = await revParse(repo, `refs/remotes/origin/${branch}`);
|
|
4096
4275
|
if (!localSha && !originSha) {
|
|
4097
4276
|
const freshBase = opts.base ?? await freshWorktreeBase(repo);
|
|
4277
|
+
await clearWorktreePath(repo, path, branch, opts);
|
|
4098
4278
|
await $2`git -C ${repo} worktree add -b ${branch} ${path} ${freshBase}`;
|
|
4099
4279
|
return { reused: false };
|
|
4100
4280
|
}
|
|
@@ -4107,6 +4287,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
|
|
|
4107
4287
|
await reclaimStaleWorktree(repo, busyAt);
|
|
4108
4288
|
opts.onStaleReclaim?.(busyAt);
|
|
4109
4289
|
}
|
|
4290
|
+
await clearWorktreePath(repo, path, branch, opts);
|
|
4110
4291
|
if (originSha) {
|
|
4111
4292
|
await $2`git -C ${repo} worktree add -B ${branch} ${path} ${`origin/${branch}`}`;
|
|
4112
4293
|
return { reused: true, from: "origin", sha: originSha };
|
|
@@ -4135,8 +4316,8 @@ async function ensureGitExcluded(worktree, pattern) {
|
|
|
4135
4316
|
try {
|
|
4136
4317
|
const commonDir = await gitPath(worktree, "--git-common-dir");
|
|
4137
4318
|
if (!commonDir) return false;
|
|
4138
|
-
const infoDir =
|
|
4139
|
-
const excludeFile =
|
|
4319
|
+
const infoDir = join20(commonDir, "info");
|
|
4320
|
+
const excludeFile = join20(infoDir, "exclude");
|
|
4140
4321
|
let current = "";
|
|
4141
4322
|
try {
|
|
4142
4323
|
current = await readFile(excludeFile, "utf8");
|
|
@@ -4156,7 +4337,7 @@ async function gitPath(worktree, flag) {
|
|
|
4156
4337
|
const out = await $2({ reject: false })`git -C ${worktree} rev-parse ${flag}`;
|
|
4157
4338
|
const raw = out.stdout.trim();
|
|
4158
4339
|
if (out.exitCode !== 0 || !raw) return null;
|
|
4159
|
-
return isAbsolute2(raw) ? raw :
|
|
4340
|
+
return isAbsolute2(raw) ? raw : join20(worktree, raw);
|
|
4160
4341
|
}
|
|
4161
4342
|
async function ensureWorktreeHygiene(worktree) {
|
|
4162
4343
|
for (const pattern of SWARM_EXCLUDE_PATTERNS) await ensureGitExcluded(worktree, pattern);
|
|
@@ -4166,12 +4347,12 @@ async function installGuardHooks(worktree) {
|
|
|
4166
4347
|
try {
|
|
4167
4348
|
const gitDir = await gitPath(worktree, "--git-dir");
|
|
4168
4349
|
if (!gitDir) return false;
|
|
4169
|
-
const hooksDir =
|
|
4350
|
+
const hooksDir = join20(gitDir, "swarm-hooks");
|
|
4170
4351
|
const prev = await previousHooksPath(worktree);
|
|
4171
4352
|
await mkdir(hooksDir, { recursive: true });
|
|
4172
4353
|
for (const hook of GUARD_HOOKS) {
|
|
4173
|
-
await writeFile(
|
|
4174
|
-
await chmod(
|
|
4354
|
+
await writeFile(join20(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
|
|
4355
|
+
await chmod(join20(hooksDir, hook), 493);
|
|
4175
4356
|
}
|
|
4176
4357
|
await $2`git -C ${worktree} config extensions.worktreeConfig true`;
|
|
4177
4358
|
await $2`git -C ${worktree} config --worktree core.hooksPath ${posix(hooksDir)}`;
|
|
@@ -4190,7 +4371,7 @@ async function previousHooksPath(worktree) {
|
|
|
4190
4371
|
if (out.exitCode === 0 && value) return value;
|
|
4191
4372
|
}
|
|
4192
4373
|
const commonDir = await gitPath(worktree, "--git-common-dir");
|
|
4193
|
-
return commonDir ?
|
|
4374
|
+
return commonDir ? join20(commonDir, "hooks") : null;
|
|
4194
4375
|
}
|
|
4195
4376
|
async function removeWorktree(repo, path) {
|
|
4196
4377
|
const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
|
|
@@ -4215,7 +4396,7 @@ async function worktreeDirty(path) {
|
|
|
4215
4396
|
async function repairMovedWorktrees(targetDir) {
|
|
4216
4397
|
const subdirs = (dir) => {
|
|
4217
4398
|
try {
|
|
4218
|
-
return
|
|
4399
|
+
return readdirSync12(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join20(dir, d.name));
|
|
4219
4400
|
} catch {
|
|
4220
4401
|
return [];
|
|
4221
4402
|
}
|
|
@@ -4258,10 +4439,11 @@ async function journalForBranch(repo, branch, slug) {
|
|
|
4258
4439
|
const out = await $2({ reject: false })`git -C ${repo} show ${`origin/${branch}:journal/${slug}.md`}`;
|
|
4259
4440
|
return out.exitCode === 0 && out.stdout ? out.stdout : null;
|
|
4260
4441
|
}
|
|
4261
|
-
async function prepareDetachedWorktree(repo, branch, worktree) {
|
|
4442
|
+
async function prepareDetachedWorktree(repo, branch, worktree, opts = {}) {
|
|
4262
4443
|
await gitNetStrict(repo, ["fetch", "--quiet", "origin"], GIT_HEAVY_NET_TIMEOUT_MS);
|
|
4263
4444
|
const ref = `origin/${branch}`;
|
|
4264
4445
|
if (!await revParse(repo, `${ref}^{commit}`)) throw new MissingRefError(ref, repo);
|
|
4446
|
+
await clearWorktreePath(repo, worktree, branch, opts);
|
|
4265
4447
|
await $2`git -C ${repo} worktree add --detach --force ${worktree} ${ref}`;
|
|
4266
4448
|
await ensureWorktreeHygiene(worktree);
|
|
4267
4449
|
}
|
|
@@ -4304,8 +4486,14 @@ var init_git = __esm({
|
|
|
4304
4486
|
init_netTimeout();
|
|
4305
4487
|
init_swarmFiles();
|
|
4306
4488
|
WorktreeBusyError = class extends Error {
|
|
4307
|
-
|
|
4308
|
-
|
|
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})`);
|
|
4309
4497
|
this.branch = branch;
|
|
4310
4498
|
this.worktreePath = worktreePath;
|
|
4311
4499
|
this.name = "WorktreeBusyError";
|
|
@@ -4345,12 +4533,12 @@ __export(droneChannel_exports, {
|
|
|
4345
4533
|
verifyLocalCliToken: () => verifyLocalCliToken
|
|
4346
4534
|
});
|
|
4347
4535
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
4348
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync9, readFileSync as
|
|
4536
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync9, readFileSync as readFileSync19, writeFileSync as writeFileSync12 } from "fs";
|
|
4349
4537
|
import { dirname as dirname4 } from "path";
|
|
4350
4538
|
import { serve } from "@hono/node-server";
|
|
4351
4539
|
function loadChannelSecret(path) {
|
|
4352
4540
|
try {
|
|
4353
|
-
const s =
|
|
4541
|
+
const s = readFileSync19(path, "utf8").trim();
|
|
4354
4542
|
if (s.length >= 32) return s;
|
|
4355
4543
|
} catch {
|
|
4356
4544
|
}
|
|
@@ -4592,8 +4780,8 @@ function startDroneChannel(deps, port) {
|
|
|
4592
4780
|
return { port, server };
|
|
4593
4781
|
}
|
|
4594
4782
|
function closeDroneChannel(server) {
|
|
4595
|
-
return new Promise((
|
|
4596
|
-
server.close(() =>
|
|
4783
|
+
return new Promise((resolve5) => {
|
|
4784
|
+
server.close(() => resolve5());
|
|
4597
4785
|
server.closeAllConnections?.();
|
|
4598
4786
|
});
|
|
4599
4787
|
}
|
|
@@ -4746,7 +4934,7 @@ var init_droneChannel = __esm({
|
|
|
4746
4934
|
|
|
4747
4935
|
// src/attachments.ts
|
|
4748
4936
|
import { mkdir as mkdir3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
4749
|
-
import { join as
|
|
4937
|
+
import { join as join24 } from "path";
|
|
4750
4938
|
function attachmentsFromDoc(doc, origin) {
|
|
4751
4939
|
const out = [];
|
|
4752
4940
|
const walk2 = (node) => {
|
|
@@ -4837,7 +5025,7 @@ async function materializeAttachments(opts) {
|
|
|
4837
5025
|
};
|
|
4838
5026
|
const result = { files: [], skipped: [] };
|
|
4839
5027
|
if (opts.refs.length === 0) return result;
|
|
4840
|
-
const dir =
|
|
5028
|
+
const dir = join24(opts.worktree, ATTACHMENTS_DIR);
|
|
4841
5029
|
await io.removeDir(dir);
|
|
4842
5030
|
let dirReady = false;
|
|
4843
5031
|
let total = 0;
|
|
@@ -4883,7 +5071,7 @@ async function materializeAttachments(opts) {
|
|
|
4883
5071
|
await io.mkdir(dir);
|
|
4884
5072
|
dirReady = true;
|
|
4885
5073
|
}
|
|
4886
|
-
await io.writeFile(
|
|
5074
|
+
await io.writeFile(join24(dir, name), file.bytes);
|
|
4887
5075
|
} catch (e) {
|
|
4888
5076
|
result.skipped.push(`${label2} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText2(e)})`);
|
|
4889
5077
|
continue;
|
|
@@ -5036,7 +5224,7 @@ var init_attachments = __esm({
|
|
|
5036
5224
|
|
|
5037
5225
|
// src/shotUpload.ts
|
|
5038
5226
|
import { readFile as fsReadFile } from "fs/promises";
|
|
5039
|
-
import { basename as basename4, isAbsolute as isAbsolute3, join as
|
|
5227
|
+
import { basename as basename4, isAbsolute as isAbsolute3, join as join25 } from "path";
|
|
5040
5228
|
function shotMimetype(path) {
|
|
5041
5229
|
const dot = path.lastIndexOf(".");
|
|
5042
5230
|
if (dot < 0) return null;
|
|
@@ -5062,7 +5250,7 @@ async function readShots(paths, opts) {
|
|
|
5062
5250
|
}
|
|
5063
5251
|
let bytes;
|
|
5064
5252
|
try {
|
|
5065
|
-
bytes = await read(isAbsolute3(path) ? path :
|
|
5253
|
+
bytes = await read(isAbsolute3(path) ? path : join25(opts.worktree, path));
|
|
5066
5254
|
} catch (e) {
|
|
5067
5255
|
skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
|
|
5068
5256
|
continue;
|
|
@@ -5143,8 +5331,8 @@ var init_shotUpload = __esm({
|
|
|
5143
5331
|
|
|
5144
5332
|
// src/oauth.ts
|
|
5145
5333
|
import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
|
|
5146
|
-
import { readFileSync as
|
|
5147
|
-
import { join as
|
|
5334
|
+
import { readFileSync as readFileSync20, writeFileSync as writeFileSync13, unlinkSync as unlinkSync4 } from "fs";
|
|
5335
|
+
import { join as join26 } from "path";
|
|
5148
5336
|
function makePkce() {
|
|
5149
5337
|
const verifier = randomBytes2(48).toString("base64url");
|
|
5150
5338
|
const challenge = createHash3("sha256").update(verifier).digest("base64url");
|
|
@@ -5228,7 +5416,7 @@ function toStore(clientId, t, now) {
|
|
|
5228
5416
|
}
|
|
5229
5417
|
function loadStore() {
|
|
5230
5418
|
try {
|
|
5231
|
-
return JSON.parse(
|
|
5419
|
+
return JSON.parse(readFileSync20(STORE_PATH2, "utf8"));
|
|
5232
5420
|
} catch {
|
|
5233
5421
|
return null;
|
|
5234
5422
|
}
|
|
@@ -5244,7 +5432,7 @@ function clearStore() {
|
|
|
5244
5432
|
}
|
|
5245
5433
|
function loadClientId() {
|
|
5246
5434
|
try {
|
|
5247
|
-
return JSON.parse(
|
|
5435
|
+
return JSON.parse(readFileSync20(CLIENT_PATH, "utf8")).clientId;
|
|
5248
5436
|
} catch {
|
|
5249
5437
|
return null;
|
|
5250
5438
|
}
|
|
@@ -5279,8 +5467,8 @@ var init_oauth = __esm({
|
|
|
5279
5467
|
AUTHORIZE = "/api/oauth/authorize";
|
|
5280
5468
|
TOKEN = "/api/oauth/token";
|
|
5281
5469
|
SCOPE = "openid profile email taskless:full";
|
|
5282
|
-
STORE_PATH2 =
|
|
5283
|
-
CLIENT_PATH =
|
|
5470
|
+
STORE_PATH2 = join26(SWARM_HOME, "oauth.json");
|
|
5471
|
+
CLIENT_PATH = join26(SWARM_HOME, "oauth-client.json");
|
|
5284
5472
|
base2 = (api) => api.replace(/\/$/, "");
|
|
5285
5473
|
}
|
|
5286
5474
|
});
|
|
@@ -5326,11 +5514,11 @@ var init_authState = __esm({
|
|
|
5326
5514
|
});
|
|
5327
5515
|
|
|
5328
5516
|
// src/workspace.ts
|
|
5329
|
-
import { readFileSync as
|
|
5330
|
-
import { join as
|
|
5517
|
+
import { readFileSync as readFileSync21, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5 } from "fs";
|
|
5518
|
+
import { join as join27 } from "path";
|
|
5331
5519
|
function loadWorkspace() {
|
|
5332
5520
|
try {
|
|
5333
|
-
const w = JSON.parse(
|
|
5521
|
+
const w = JSON.parse(readFileSync21(WORKSPACE_PATH, "utf8"));
|
|
5334
5522
|
return w && typeof w.id === "string" && w.id ? w : null;
|
|
5335
5523
|
} catch {
|
|
5336
5524
|
return null;
|
|
@@ -5344,7 +5532,7 @@ var init_workspace = __esm({
|
|
|
5344
5532
|
"src/workspace.ts"() {
|
|
5345
5533
|
"use strict";
|
|
5346
5534
|
init_config();
|
|
5347
|
-
WORKSPACE_PATH =
|
|
5535
|
+
WORKSPACE_PATH = join27(SWARM_HOME, "workspace.json");
|
|
5348
5536
|
}
|
|
5349
5537
|
});
|
|
5350
5538
|
|
|
@@ -5948,7 +6136,7 @@ __export(db_exports, {
|
|
|
5948
6136
|
import { createRequire as createRequire2 } from "module";
|
|
5949
6137
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
5950
6138
|
import { existsSync as existsSync15 } from "fs";
|
|
5951
|
-
import { resolve as
|
|
6139
|
+
import { resolve as resolve4 } from "path";
|
|
5952
6140
|
function rowToQuestion(row) {
|
|
5953
6141
|
return {
|
|
5954
6142
|
id: row.id,
|
|
@@ -5990,6 +6178,7 @@ function rowToRun(row) {
|
|
|
5990
6178
|
tokens_in: row.tokens_in ?? 0,
|
|
5991
6179
|
tokens_out: row.tokens_out ?? 0,
|
|
5992
6180
|
duration_ms: row.duration_ms ?? 0,
|
|
6181
|
+
cost_unmeasured: row.cost_unmeasured ?? 0,
|
|
5993
6182
|
pr_url: row.pr_url ?? null,
|
|
5994
6183
|
// TASWM-506: файлы, занятые прогоном с открытым PR — JSON-массив, как depends_on.
|
|
5995
6184
|
changed_paths: row.changed_paths ? JSON.parse(row.changed_paths) : [],
|
|
@@ -6070,6 +6259,7 @@ CREATE TABLE IF NOT EXISTS runs (
|
|
|
6070
6259
|
tokens_in INTEGER NOT NULL DEFAULT 0,
|
|
6071
6260
|
tokens_out INTEGER NOT NULL DEFAULT 0,
|
|
6072
6261
|
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
6262
|
+
cost_unmeasured INTEGER NOT NULL DEFAULT 0,
|
|
6073
6263
|
pr_url TEXT,
|
|
6074
6264
|
superseded_by INTEGER,
|
|
6075
6265
|
decision TEXT,
|
|
@@ -6185,6 +6375,10 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
6185
6375
|
"ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
|
|
6186
6376
|
"ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
|
|
6187
6377
|
"ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0",
|
|
6378
|
+
// TASWM-548: сколько запусков прогона остались без снятой цены — отличает «бесплатно»
|
|
6379
|
+
// от «не измерено». Прогоны до фичи получают 0: их нули мы перепроверить уже не можем,
|
|
6380
|
+
// а задним числом объявлять их неизмеренными значило бы утверждать непроверенное.
|
|
6381
|
+
"ALTER TABLE runs ADD COLUMN cost_unmeasured INTEGER NOT NULL DEFAULT 0",
|
|
6188
6382
|
// TASWM-395: усилие рассуждения прогона (`claude --effort`). Nullable без DEFAULT —
|
|
6189
6383
|
// NULL это и все прогоны до фичи, и прогоны на дефолте харнесса.
|
|
6190
6384
|
"ALTER TABLE runs ADD COLUMN effort TEXT",
|
|
@@ -6571,7 +6765,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
6571
6765
|
* транзакции: прервётся — маркер не встанет, повтор безопасен.
|
|
6572
6766
|
*/
|
|
6573
6767
|
importDb(sourcePath, repo) {
|
|
6574
|
-
const abs =
|
|
6768
|
+
const abs = resolve4(sourcePath);
|
|
6575
6769
|
const repoN = normalizeRepo(repo);
|
|
6576
6770
|
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
6577
6771
|
if (already) return { imported: false, runs: already.run_count };
|
|
@@ -6943,9 +7137,9 @@ var init_droneStore = __esm({
|
|
|
6943
7137
|
});
|
|
6944
7138
|
|
|
6945
7139
|
// src/overmind.ts
|
|
6946
|
-
import { homedir as
|
|
6947
|
-
import { existsSync as existsSync20, readFileSync as
|
|
6948
|
-
import { basename as basename6, dirname as dirname5, join as
|
|
7140
|
+
import { homedir as homedir7 } from "os";
|
|
7141
|
+
import { existsSync as existsSync20, readFileSync as readFileSync25, readdirSync as readdirSync14, renameSync as renameSync12 } from "fs";
|
|
7142
|
+
import { basename as basename6, dirname as dirname5, join as join34 } from "path";
|
|
6949
7143
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
|
|
6950
7144
|
import { spawn as spawn4 } from "child_process";
|
|
6951
7145
|
|
|
@@ -7025,7 +7219,7 @@ init_config();
|
|
|
7025
7219
|
init_resources();
|
|
7026
7220
|
init_names2();
|
|
7027
7221
|
init_storm();
|
|
7028
|
-
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as
|
|
7222
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "fs";
|
|
7029
7223
|
import { dirname as dirname2 } from "path";
|
|
7030
7224
|
import { randomUUID } from "crypto";
|
|
7031
7225
|
|
|
@@ -7082,12 +7276,12 @@ init_zoneNotes();
|
|
|
7082
7276
|
init_toolFailure();
|
|
7083
7277
|
|
|
7084
7278
|
// src/logTail.ts
|
|
7085
|
-
import { closeSync, existsSync as existsSync6, openSync, readSync, readdirSync as
|
|
7086
|
-
import { join as
|
|
7279
|
+
import { closeSync, existsSync as existsSync6, openSync, readSync, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
|
|
7280
|
+
import { join as join10 } from "path";
|
|
7087
7281
|
|
|
7088
7282
|
// src/runLogDir.ts
|
|
7089
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync as
|
|
7090
|
-
import { join as
|
|
7283
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync as readdirSync5, renameSync as renameSync3, statSync as statSync4 } from "fs";
|
|
7284
|
+
import { join as join9 } from "path";
|
|
7091
7285
|
var LEGACY_RUN_LOGS_SUBDIR = "legacy";
|
|
7092
7286
|
function runLogDirName(runLogKey) {
|
|
7093
7287
|
const safe2 = runLogKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
|
|
@@ -7095,7 +7289,7 @@ function runLogDirName(runLogKey) {
|
|
|
7095
7289
|
return /^\d+$/.test(safe2) ? `k-${safe2}` : safe2;
|
|
7096
7290
|
}
|
|
7097
7291
|
function runLogDirPath(runLogsDir, runLogKey) {
|
|
7098
|
-
return
|
|
7292
|
+
return join9(runLogsDir, runLogDirName(runLogKey));
|
|
7099
7293
|
}
|
|
7100
7294
|
function findRunLogDir(runLogsDir, keys) {
|
|
7101
7295
|
for (const key of keys) {
|
|
@@ -7110,10 +7304,10 @@ function runLogDirForWrite(runLogsDir, keys) {
|
|
|
7110
7304
|
return findRunLogDir(runLogsDir, keys) ?? runLogDirPath(runLogsDir, canonical);
|
|
7111
7305
|
}
|
|
7112
7306
|
function sweepLegacyRunLogs(runLogsDir) {
|
|
7113
|
-
const legacyDir =
|
|
7307
|
+
const legacyDir = join9(runLogsDir, LEGACY_RUN_LOGS_SUBDIR);
|
|
7114
7308
|
let entries;
|
|
7115
7309
|
try {
|
|
7116
|
-
entries =
|
|
7310
|
+
entries = readdirSync5(runLogsDir);
|
|
7117
7311
|
} catch {
|
|
7118
7312
|
return { moved: 0, failed: 0, legacyDir };
|
|
7119
7313
|
}
|
|
@@ -7121,12 +7315,12 @@ function sweepLegacyRunLogs(runLogsDir) {
|
|
|
7121
7315
|
let failed = 0;
|
|
7122
7316
|
for (const name of entries) {
|
|
7123
7317
|
if (!/^\d+$/.test(name)) continue;
|
|
7124
|
-
const from =
|
|
7318
|
+
const from = join9(runLogsDir, name);
|
|
7125
7319
|
try {
|
|
7126
|
-
if (!
|
|
7320
|
+
if (!statSync4(from).isDirectory()) continue;
|
|
7127
7321
|
mkdirSync5(legacyDir, { recursive: true });
|
|
7128
|
-
let to =
|
|
7129
|
-
for (let n = 2; existsSync5(to) && n < 100; n++) to =
|
|
7322
|
+
let to = join9(legacyDir, name);
|
|
7323
|
+
for (let n = 2; existsSync5(to) && n < 100; n++) to = join9(legacyDir, `${name}-${n}`);
|
|
7130
7324
|
renameSync3(from, to);
|
|
7131
7325
|
moved++;
|
|
7132
7326
|
} catch {
|
|
@@ -7146,7 +7340,7 @@ function fileHeader(name, size, shown) {
|
|
|
7146
7340
|
function readTail(path, opts) {
|
|
7147
7341
|
let size;
|
|
7148
7342
|
try {
|
|
7149
|
-
size =
|
|
7343
|
+
size = statSync5(path).size;
|
|
7150
7344
|
} catch {
|
|
7151
7345
|
return null;
|
|
7152
7346
|
}
|
|
@@ -7181,7 +7375,7 @@ function latestRunLaunch(runLogsDir, runLogKeys) {
|
|
|
7181
7375
|
if (!dir) return null;
|
|
7182
7376
|
let files;
|
|
7183
7377
|
try {
|
|
7184
|
-
files =
|
|
7378
|
+
files = readdirSync6(dir);
|
|
7185
7379
|
} catch {
|
|
7186
7380
|
return null;
|
|
7187
7381
|
}
|
|
@@ -7192,8 +7386,8 @@ function latestRunLaunch(runLogsDir, runLogKeys) {
|
|
|
7192
7386
|
}
|
|
7193
7387
|
if (indexes.size === 0) return null;
|
|
7194
7388
|
const index = Math.max(...indexes);
|
|
7195
|
-
const out = [
|
|
7196
|
-
const err = existsSync6(
|
|
7389
|
+
const out = [join10(dir, `${index}.json`), join10(dir, `${index}.json.done`)].find((p) => existsSync6(p)) ?? null;
|
|
7390
|
+
const err = existsSync6(join10(dir, `${index}.err.log`)) ? join10(dir, `${index}.err.log`) : null;
|
|
7197
7391
|
return { index, total: indexes.size, out, err };
|
|
7198
7392
|
}
|
|
7199
7393
|
function runLogTail(runLogsDir, runLogKeys, opts) {
|
|
@@ -7464,13 +7658,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
7464
7658
|
recover() {
|
|
7465
7659
|
if (existsSync7(this.mapPath)) {
|
|
7466
7660
|
try {
|
|
7467
|
-
const obj = JSON.parse(
|
|
7661
|
+
const obj = JSON.parse(readFileSync7(this.mapPath, "utf8"));
|
|
7468
7662
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
7469
7663
|
} catch {
|
|
7470
7664
|
}
|
|
7471
7665
|
}
|
|
7472
7666
|
if (existsSync7(this.outboxPath)) {
|
|
7473
|
-
const txt =
|
|
7667
|
+
const txt = readFileSync7(this.outboxPath, "utf8");
|
|
7474
7668
|
for (const line of txt.split("\n")) {
|
|
7475
7669
|
const s = line.trim();
|
|
7476
7670
|
if (!s) continue;
|
|
@@ -7493,7 +7687,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
7493
7687
|
if (!existsSync7(this.stormPath)) return;
|
|
7494
7688
|
let txt;
|
|
7495
7689
|
try {
|
|
7496
|
-
txt =
|
|
7690
|
+
txt = readFileSync7(this.stormPath, "utf8");
|
|
7497
7691
|
} catch {
|
|
7498
7692
|
return;
|
|
7499
7693
|
}
|
|
@@ -7577,6 +7771,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
7577
7771
|
if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
|
|
7578
7772
|
if ("tokens_out" in patch && patch.tokens_out !== void 0) out.tokensOut = patch.tokens_out;
|
|
7579
7773
|
if ("duration_ms" in patch && patch.duration_ms !== void 0) out.durationMs = patch.duration_ms;
|
|
7774
|
+
if ("cost_unmeasured" in patch && patch.cost_unmeasured !== void 0) out.costUnmeasured = patch.cost_unmeasured;
|
|
7580
7775
|
if ("respawns" in patch && patch.respawns !== void 0) out.respawns = patch.respawns;
|
|
7581
7776
|
if ("pr_url" in patch) out.prUrl = patch.pr_url ?? null;
|
|
7582
7777
|
if ("changed_paths" in patch && patch.changed_paths) out.changedPaths = patch.changed_paths;
|
|
@@ -7659,6 +7854,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
7659
7854
|
// копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
|
|
7660
7855
|
tokens_out: 0,
|
|
7661
7856
|
duration_ms: 0,
|
|
7857
|
+
cost_unmeasured: 0,
|
|
7858
|
+
// запусков без снятой цены (TASWM-548) — копится тем же учётом
|
|
7662
7859
|
pr_url: null,
|
|
7663
7860
|
changed_paths: [],
|
|
7664
7861
|
// файлы прогона снимаются при открытии PR (TASWM-506)
|
|
@@ -9279,6 +9476,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
9279
9476
|
// токены и время работы (TASWM-353) — прод-поля, заполняются ниже
|
|
9280
9477
|
tokens_out: 0,
|
|
9281
9478
|
duration_ms: 0,
|
|
9479
|
+
cost_unmeasured: 0,
|
|
9480
|
+
// запусков без снятой цены (TASWM-548) — прод-поле, заполняется ниже
|
|
9282
9481
|
pr_url: null,
|
|
9283
9482
|
changed_paths: [],
|
|
9284
9483
|
// занятые файлы (TASWM-506) — прод-поле, заполняется ниже
|
|
@@ -9360,6 +9559,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
9360
9559
|
local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
|
|
9361
9560
|
local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
|
|
9362
9561
|
local.duration_ms = typeof pr.durationMs === "number" ? pr.durationMs : 0;
|
|
9562
|
+
local.cost_unmeasured = typeof pr.costUnmeasured === "number" ? pr.costUnmeasured : 0;
|
|
9363
9563
|
local.pr_url = pr.prUrl ?? null;
|
|
9364
9564
|
local.changed_paths = Array.isArray(pr.changedPaths) ? pr.changedPaths : local.changed_paths ?? [];
|
|
9365
9565
|
local.stop_reason = pr.stopReason ?? null;
|
|
@@ -9694,11 +9894,11 @@ function tickPulse(health, now, intervalMs) {
|
|
|
9694
9894
|
}
|
|
9695
9895
|
|
|
9696
9896
|
// src/logRotate.ts
|
|
9697
|
-
import { copyFileSync as copyFileSync2, renameSync as renameSync5, statSync as
|
|
9897
|
+
import { copyFileSync as copyFileSync2, renameSync as renameSync5, statSync as statSync6, fstatSync, truncateSync, unlinkSync as unlinkSync3, existsSync as existsSync8 } from "fs";
|
|
9698
9898
|
var realIo = {
|
|
9699
9899
|
size: (path) => {
|
|
9700
9900
|
try {
|
|
9701
|
-
return
|
|
9901
|
+
return statSync6(path).size;
|
|
9702
9902
|
} catch {
|
|
9703
9903
|
return null;
|
|
9704
9904
|
}
|
|
@@ -9720,7 +9920,7 @@ var realOwnerIo = {
|
|
|
9720
9920
|
},
|
|
9721
9921
|
statPath: (path) => {
|
|
9722
9922
|
try {
|
|
9723
|
-
const s =
|
|
9923
|
+
const s = statSync6(path);
|
|
9724
9924
|
return { dev: s.dev, ino: s.ino };
|
|
9725
9925
|
} catch {
|
|
9726
9926
|
return null;
|
|
@@ -9754,7 +9954,7 @@ function rotateDaemonLog(path, opts, io = realIo) {
|
|
|
9754
9954
|
}
|
|
9755
9955
|
|
|
9756
9956
|
// src/daemonLaunch.ts
|
|
9757
|
-
import { openSync as openSync2, closeSync as closeSync2, readFileSync as
|
|
9957
|
+
import { openSync as openSync2, closeSync as closeSync2, readFileSync as readFileSync9 } from "fs";
|
|
9758
9958
|
import { spawn } from "child_process";
|
|
9759
9959
|
var realRespawnIo = {
|
|
9760
9960
|
isTty: () => process.stdout.isTTY === true,
|
|
@@ -9854,13 +10054,13 @@ async function buildRepoRegistry(paths) {
|
|
|
9854
10054
|
}
|
|
9855
10055
|
|
|
9856
10056
|
// src/repoMemory.ts
|
|
9857
|
-
import { readFileSync as
|
|
9858
|
-
import { join as
|
|
10057
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
|
|
10058
|
+
import { join as join11 } from "path";
|
|
9859
10059
|
var FILE = "repos.json";
|
|
9860
10060
|
function loadRememberedRepos(home) {
|
|
9861
10061
|
let raw;
|
|
9862
10062
|
try {
|
|
9863
|
-
raw =
|
|
10063
|
+
raw = readFileSync10(join11(home, FILE), "utf8");
|
|
9864
10064
|
} catch {
|
|
9865
10065
|
return [];
|
|
9866
10066
|
}
|
|
@@ -9875,7 +10075,7 @@ function loadRememberedRepos(home) {
|
|
|
9875
10075
|
function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
|
|
9876
10076
|
const clean = paths.filter((p) => p.trim().length > 0);
|
|
9877
10077
|
if (clean.length === 0) return;
|
|
9878
|
-
writeFileSync8(
|
|
10078
|
+
writeFileSync8(join11(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
|
|
9879
10079
|
`);
|
|
9880
10080
|
}
|
|
9881
10081
|
function pickRepoPaths(args) {
|
|
@@ -9889,13 +10089,13 @@ function pickRepoPaths(args) {
|
|
|
9889
10089
|
init_src();
|
|
9890
10090
|
init_config();
|
|
9891
10091
|
init_linkFailure();
|
|
9892
|
-
import { homedir as
|
|
9893
|
-
import { readFileSync as
|
|
9894
|
-
import { isAbsolute, join as
|
|
10092
|
+
import { homedir as homedir5 } from "os";
|
|
10093
|
+
import { readFileSync as readFileSync12, renameSync as renameSync6, writeFileSync as writeFileSync9 } from "fs";
|
|
10094
|
+
import { isAbsolute, join as join13, resolve as resolve2 } from "path";
|
|
9895
10095
|
|
|
9896
10096
|
// src/repoScan.ts
|
|
9897
|
-
import { readdirSync as
|
|
9898
|
-
import { join as
|
|
10097
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync11, statSync as statSync7 } from "fs";
|
|
10098
|
+
import { join as join12 } from "path";
|
|
9899
10099
|
var SKIP = /* @__PURE__ */ new Set([
|
|
9900
10100
|
"node_modules",
|
|
9901
10101
|
"vendor",
|
|
@@ -9925,7 +10125,7 @@ function parseOriginFromGitConfig(text) {
|
|
|
9925
10125
|
}
|
|
9926
10126
|
function originOf(dir) {
|
|
9927
10127
|
try {
|
|
9928
|
-
return parseOriginFromGitConfig(
|
|
10128
|
+
return parseOriginFromGitConfig(readFileSync11(join12(dir, ".git", "config"), "utf8"));
|
|
9929
10129
|
} catch {
|
|
9930
10130
|
return null;
|
|
9931
10131
|
}
|
|
@@ -9954,15 +10154,15 @@ function walk(dir, depth, maxDepth, found) {
|
|
|
9954
10154
|
}
|
|
9955
10155
|
let entries;
|
|
9956
10156
|
try {
|
|
9957
|
-
entries =
|
|
10157
|
+
entries = readdirSync7(dir);
|
|
9958
10158
|
} catch {
|
|
9959
10159
|
return;
|
|
9960
10160
|
}
|
|
9961
10161
|
for (const name of entries) {
|
|
9962
10162
|
if (name.startsWith(".") || SKIP.has(name)) continue;
|
|
9963
|
-
const child =
|
|
10163
|
+
const child = join12(dir, name);
|
|
9964
10164
|
try {
|
|
9965
|
-
if (!
|
|
10165
|
+
if (!statSync7(child).isDirectory()) continue;
|
|
9966
10166
|
} catch {
|
|
9967
10167
|
continue;
|
|
9968
10168
|
}
|
|
@@ -9982,13 +10182,13 @@ function defaultScanRoots(home, knownPaths = []) {
|
|
|
9982
10182
|
if (parent && parent !== p) add(parent);
|
|
9983
10183
|
}
|
|
9984
10184
|
for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
|
|
9985
|
-
add(
|
|
10185
|
+
add(join12(home, name));
|
|
9986
10186
|
}
|
|
9987
10187
|
return out;
|
|
9988
10188
|
}
|
|
9989
10189
|
|
|
9990
10190
|
// src/repoDiscovery.ts
|
|
9991
|
-
function resolveScanRoots(raw, home =
|
|
10191
|
+
function resolveScanRoots(raw, home = homedir5(), knownPaths = []) {
|
|
9992
10192
|
const v = (raw ?? "").trim();
|
|
9993
10193
|
if (!v) return defaultScanRoots(home, knownPaths);
|
|
9994
10194
|
if (["off", "none", "0"].includes(v.toLowerCase())) return [];
|
|
@@ -9996,7 +10196,7 @@ function resolveScanRoots(raw, home = homedir4(), knownPaths = []) {
|
|
|
9996
10196
|
for (const part of v.split(",")) {
|
|
9997
10197
|
const p = part.trim();
|
|
9998
10198
|
if (!p) continue;
|
|
9999
|
-
const abs = p === "~" ? home : p.startsWith("~/") ?
|
|
10199
|
+
const abs = p === "~" ? home : p.startsWith("~/") ? join13(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
|
|
10000
10200
|
if (!out.includes(abs)) out.push(abs);
|
|
10001
10201
|
}
|
|
10002
10202
|
return out;
|
|
@@ -10210,7 +10410,7 @@ function repoMapRows(state) {
|
|
|
10210
10410
|
}
|
|
10211
10411
|
function loadRepoMapCache(path) {
|
|
10212
10412
|
try {
|
|
10213
|
-
const raw = JSON.parse(
|
|
10413
|
+
const raw = JSON.parse(readFileSync12(path, "utf8"));
|
|
10214
10414
|
if (!Array.isArray(raw?.entries)) return { scannedAt: null, entries: [] };
|
|
10215
10415
|
return {
|
|
10216
10416
|
scannedAt: typeof raw.scannedAt === "string" && raw.scannedAt ? raw.scannedAt : null,
|
|
@@ -10233,15 +10433,15 @@ function writeRepoMapCache(path, entries, scannedAt = stamp()) {
|
|
|
10233
10433
|
|
|
10234
10434
|
// src/hostIdentity.ts
|
|
10235
10435
|
import { hostname } from "os";
|
|
10236
|
-
import { existsSync as existsSync9, readFileSync as
|
|
10237
|
-
import { join as
|
|
10436
|
+
import { existsSync as existsSync9, readFileSync as readFileSync13, renameSync as renameSync7, writeFileSync as writeFileSync10 } from "fs";
|
|
10437
|
+
import { join as join14 } from "path";
|
|
10238
10438
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
10239
10439
|
function hostIdentityPath(home) {
|
|
10240
|
-
return
|
|
10440
|
+
return join14(home, "host.json");
|
|
10241
10441
|
}
|
|
10242
10442
|
function loadHostIdentity(deps) {
|
|
10243
10443
|
const exists = deps.exists ?? existsSync9;
|
|
10244
|
-
const readFile2 = deps.readFile ?? ((p) =>
|
|
10444
|
+
const readFile2 = deps.readFile ?? ((p) => readFileSync13(p, "utf8"));
|
|
10245
10445
|
const writeFile4 = deps.writeFile ?? ((p, data) => {
|
|
10246
10446
|
writeFileSync10(`${p}.tmp`, data);
|
|
10247
10447
|
renameSync7(`${p}.tmp`, p);
|
|
@@ -10305,7 +10505,18 @@ function pauseDue(reliefSince, lastPauseAt, now, cfg) {
|
|
|
10305
10505
|
if (reliefSince == null || now - reliefSince < cfg.sustainMs) return false;
|
|
10306
10506
|
return lastPauseAt == null || now - lastPauseAt >= cfg.pauseWindowMs;
|
|
10307
10507
|
}
|
|
10508
|
+
function loadBearingRuns(runs) {
|
|
10509
|
+
return runs.filter((r) => r.status === "running" && r.pid != null);
|
|
10510
|
+
}
|
|
10511
|
+
function reliefWouldStrangleSoleWorker(runs) {
|
|
10512
|
+
return loadBearingRuns(runs).length <= 1;
|
|
10513
|
+
}
|
|
10514
|
+
function reliefSoleWorkerReason(decision) {
|
|
10515
|
+
const head = `${RELIEF_PAUSE_MARK}: \u041D\u0415 \u0441\u043D\u0438\u043C\u0430\u044E \u2014 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043E\u0434\u0438\u043D \u0434\u0440\u043E\u043D, \u0438 \u0432\u0441\u044F \u043D\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0435\u0433\u043E \u0436\u0435 (\u0441\u043D\u044F\u0442\u044C \u0435\u0433\u043E \u0437\u043D\u0430\u0447\u0438\u0442 \u043E\u0441\u0432\u043E\u0431\u043E\u0434\u0438\u0442\u044C \u043C\u0430\u0448\u0438\u043D\u0443 \u043E\u0442 \u0442\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B, \u0440\u0430\u0434\u0438 \u043A\u043E\u0442\u043E\u0440\u043E\u0439 \u043E\u043D\u0430 \u043D\u0443\u0436\u043D\u0430; TASWM-550)`;
|
|
10516
|
+
return decision ? `${head} \xB7 ${decision}` : head;
|
|
10517
|
+
}
|
|
10308
10518
|
function pickPauseCandidate(runs, protectedRunIds) {
|
|
10519
|
+
if (reliefWouldStrangleSoleWorker(runs)) return null;
|
|
10309
10520
|
const candidates = runs.filter(
|
|
10310
10521
|
(r) => r.kind === "drone" && r.status === "running" && r.pid != null && r.resolves_run_id == null && r.reviews_run_id == null && // TASWM-535: прогон, признанный ТЯЖЁЛЫМ, разгрузка не снимает. Правильная реакция на
|
|
10311
10522
|
// «тяжело» — уменьшить число соседей, а не убить того, кто работает: 27.08 перегруженная
|
|
@@ -10325,8 +10536,8 @@ function pickResumeCandidate(pausedForRelief) {
|
|
|
10325
10536
|
}
|
|
10326
10537
|
|
|
10327
10538
|
// src/reliefLoop.ts
|
|
10328
|
-
import { existsSync as existsSync10, readFileSync as
|
|
10329
|
-
import { join as
|
|
10539
|
+
import { existsSync as existsSync10, readFileSync as readFileSync14, renameSync as renameSync8, writeFileSync as writeFileSync11 } from "fs";
|
|
10540
|
+
import { join as join15 } from "path";
|
|
10330
10541
|
var numEnv3 = (name, def) => {
|
|
10331
10542
|
const v = Number(process.env[name]);
|
|
10332
10543
|
return Number.isFinite(v) && v > 0 ? v : def;
|
|
@@ -10361,11 +10572,11 @@ function reliefLoopTripped(entry, cfg = DEFAULT_RELIEF_LOOP_CONFIG) {
|
|
|
10361
10572
|
return !!entry && entry.pauses >= cfg.limit;
|
|
10362
10573
|
}
|
|
10363
10574
|
function reliefLoopStatePath(home) {
|
|
10364
|
-
return
|
|
10575
|
+
return join15(home, "relief-loop.json");
|
|
10365
10576
|
}
|
|
10366
10577
|
function loadReliefLoopState(home, now = Date.now(), io = {}, cfg = DEFAULT_RELIEF_LOOP_CONFIG) {
|
|
10367
10578
|
const exists = io.exists ?? existsSync10;
|
|
10368
|
-
const readFile2 = io.readFile ?? ((p) =>
|
|
10579
|
+
const readFile2 = io.readFile ?? ((p) => readFileSync14(p, "utf8"));
|
|
10369
10580
|
const path = reliefLoopStatePath(home);
|
|
10370
10581
|
if (!exists(path)) return {};
|
|
10371
10582
|
try {
|
|
@@ -10549,15 +10760,15 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
|
|
|
10549
10760
|
|
|
10550
10761
|
// src/launcher.ts
|
|
10551
10762
|
import { spawn as spawn2 } from "child_process";
|
|
10552
|
-
import { closeSync as closeSync3, existsSync as existsSync11, mkdirSync as mkdirSync8, openSync as openSync3, readdirSync as
|
|
10763
|
+
import { closeSync as closeSync3, existsSync as existsSync11, mkdirSync as mkdirSync8, openSync as openSync3, readdirSync as readdirSync9 } from "fs";
|
|
10553
10764
|
import { createRequire } from "module";
|
|
10554
|
-
import { homedir as
|
|
10555
|
-
import { dirname as dirname3, join as
|
|
10765
|
+
import { homedir as homedir6 } from "os";
|
|
10766
|
+
import { dirname as dirname3, join as join17 } from "path";
|
|
10556
10767
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10557
10768
|
|
|
10558
10769
|
// src/droneHome.ts
|
|
10559
|
-
import { lstatSync, mkdirSync as mkdirSync7, readdirSync as
|
|
10560
|
-
import { join as
|
|
10770
|
+
import { lstatSync, mkdirSync as mkdirSync7, readdirSync as readdirSync8, rmSync, statSync as statSync8, symlinkSync as symlinkSync2 } from "fs";
|
|
10771
|
+
import { join as join16, relative, sep } from "path";
|
|
10561
10772
|
var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
|
|
10562
10773
|
function droneHomeDeny(env = process.env) {
|
|
10563
10774
|
const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -10576,28 +10787,28 @@ function buildDroneHome(opts) {
|
|
|
10576
10787
|
const deny = opts.deny ?? droneHomeDeny();
|
|
10577
10788
|
const platform = opts.platform ?? process.platform;
|
|
10578
10789
|
mkdirSync7(droneHome, { recursive: true, mode: 448 });
|
|
10579
|
-
const wanted = new Set(mirroredEntries(
|
|
10790
|
+
const wanted = new Set(mirroredEntries(readdirSync8(realHome), deny, platform));
|
|
10580
10791
|
const rel = relative(realHome, droneHome);
|
|
10581
10792
|
if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
|
|
10582
|
-
for (const existing of
|
|
10793
|
+
for (const existing of readdirSync8(droneHome)) {
|
|
10583
10794
|
if (wanted.has(existing)) continue;
|
|
10584
|
-
const p =
|
|
10795
|
+
const p = join16(droneHome, existing);
|
|
10585
10796
|
try {
|
|
10586
10797
|
if (lstatSync(p).isSymbolicLink()) rmSync(p);
|
|
10587
10798
|
} catch {
|
|
10588
10799
|
}
|
|
10589
10800
|
}
|
|
10590
10801
|
for (const name of wanted) {
|
|
10591
|
-
const link2 =
|
|
10802
|
+
const link2 = join16(droneHome, name);
|
|
10592
10803
|
try {
|
|
10593
10804
|
lstatSync(link2);
|
|
10594
10805
|
continue;
|
|
10595
10806
|
} catch {
|
|
10596
10807
|
}
|
|
10597
|
-
const target =
|
|
10808
|
+
const target = join16(realHome, name);
|
|
10598
10809
|
let isDir;
|
|
10599
10810
|
try {
|
|
10600
|
-
isDir =
|
|
10811
|
+
isDir = statSync8(target).isDirectory();
|
|
10601
10812
|
} catch {
|
|
10602
10813
|
continue;
|
|
10603
10814
|
}
|
|
@@ -10615,7 +10826,7 @@ var REAL_PATH_ENV_PINS = [
|
|
|
10615
10826
|
{ env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
|
|
10616
10827
|
];
|
|
10617
10828
|
function realPathEnvPins(realHome) {
|
|
10618
|
-
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env,
|
|
10829
|
+
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join16(realHome, ...segments)]));
|
|
10619
10830
|
}
|
|
10620
10831
|
function dronePrivatePaths(realHome, swarmHome) {
|
|
10621
10832
|
return [
|
|
@@ -10727,7 +10938,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
|
|
|
10727
10938
|
const env = { ...base3 };
|
|
10728
10939
|
const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
|
|
10729
10940
|
if (isolate && !base3.TURBO_CACHE_DIR) {
|
|
10730
|
-
env.TURBO_CACHE_DIR =
|
|
10941
|
+
env.TURBO_CACHE_DIR = join17(worktree, ".turbo", "cache");
|
|
10731
10942
|
}
|
|
10732
10943
|
Object.assign(env, droneTestEnv(droneCap, base3));
|
|
10733
10944
|
for (const key of DAEMON_STATE_ENV) delete env[key];
|
|
@@ -10755,10 +10966,10 @@ var DAEMON_STATE_ENV = [
|
|
|
10755
10966
|
"TASKLESS_TOKEN"
|
|
10756
10967
|
];
|
|
10757
10968
|
function bundledRoleDir(kind) {
|
|
10758
|
-
return
|
|
10969
|
+
return join17(dirname3(fileURLToPath2(import.meta.url)), "..", "roles", kind);
|
|
10759
10970
|
}
|
|
10760
10971
|
function roleDir(worktree, kind) {
|
|
10761
|
-
return
|
|
10972
|
+
return join17(worktree, ".claude", "roles", kind);
|
|
10762
10973
|
}
|
|
10763
10974
|
function roleDirs(worktree, kind) {
|
|
10764
10975
|
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync11(dir));
|
|
@@ -10769,7 +10980,7 @@ function nextRunLogPath(runLogsDir, runLogKeys) {
|
|
|
10769
10980
|
mkdirSync8(dir, { recursive: true });
|
|
10770
10981
|
let files = [];
|
|
10771
10982
|
try {
|
|
10772
|
-
files =
|
|
10983
|
+
files = readdirSync9(dir);
|
|
10773
10984
|
} catch {
|
|
10774
10985
|
}
|
|
10775
10986
|
let next = 0;
|
|
@@ -10777,7 +10988,7 @@ function nextRunLogPath(runLogsDir, runLogKeys) {
|
|
|
10777
10988
|
const m = /^(\d+)\.json/.exec(f);
|
|
10778
10989
|
if (m) next = Math.max(next, Number(m[1]) + 1);
|
|
10779
10990
|
}
|
|
10780
|
-
return
|
|
10991
|
+
return join17(dir, `${next}.json`);
|
|
10781
10992
|
}
|
|
10782
10993
|
function openNextRunLog(runLogsDir, runLogKeys) {
|
|
10783
10994
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
@@ -10858,11 +11069,11 @@ function makeLauncher(cfg) {
|
|
|
10858
11069
|
} catch (e) {
|
|
10859
11070
|
console.error(`[launcher] \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u0443\u0441\u0438\u043B\u0438\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${String(e)}`);
|
|
10860
11071
|
}
|
|
10861
|
-
const realHome = cfg.droneIsolation?.realHome ??
|
|
11072
|
+
const realHome = cfg.droneIsolation?.realHome ?? homedir6();
|
|
10862
11073
|
const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
|
|
10863
|
-
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync11(
|
|
11074
|
+
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync11(join17(cfg.droneIsolation.swarmHome, "token"))) {
|
|
10864
11075
|
console.error(
|
|
10865
|
-
`[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${
|
|
11076
|
+
`[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${join17(cfg.droneIsolation.swarmHome, "token")} \u043D\u0435\u0442. read_task \u0434\u0440\u043E\u043D\u0430 \u0431\u0443\u0434\u0435\u0442 slug-only. \u041F\u043E\u0447\u0438\u043D\u0438: \`swarm login\` (oauth.json) \u0438\u043B\u0438 \u043F\u043E\u043B\u043E\u0436\u0438 \u0442\u043E\u043A\u0435\u043D \u0432 \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B.`
|
|
10866
11077
|
);
|
|
10867
11078
|
}
|
|
10868
11079
|
const executor = cfg.executor ?? resolveExecutor();
|
|
@@ -10923,39 +11134,94 @@ function makeLauncher(cfg) {
|
|
|
10923
11134
|
init_procCleanup();
|
|
10924
11135
|
|
|
10925
11136
|
// src/cost.ts
|
|
10926
|
-
import { readFileSync as
|
|
10927
|
-
import { join as
|
|
11137
|
+
import { readFileSync as readFileSync15, readdirSync as readdirSync10, renameSync as renameSync9, statSync as statSync9 } from "fs";
|
|
11138
|
+
import { join as join18 } from "path";
|
|
10928
11139
|
init_executors();
|
|
10929
11140
|
var UNPROCESSED_LOG_RE = /^\d+\.json$/;
|
|
11141
|
+
var SEGMENT_LOG_RE = /^(\d+)\.json(\.done)?$/;
|
|
11142
|
+
var RESCUE_OPEN_WINDOW_MAX_MS = 12 * 60 * 60 * 1e3;
|
|
10930
11143
|
function unprocessedLogFiles(dir) {
|
|
10931
11144
|
try {
|
|
10932
|
-
return
|
|
11145
|
+
return readdirSync10(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
|
|
10933
11146
|
} catch {
|
|
10934
11147
|
return [];
|
|
10935
11148
|
}
|
|
10936
11149
|
}
|
|
11150
|
+
function segmentStarts(dir) {
|
|
11151
|
+
const starts = /* @__PURE__ */ new Map();
|
|
11152
|
+
let entries;
|
|
11153
|
+
try {
|
|
11154
|
+
entries = readdirSync10(dir);
|
|
11155
|
+
} catch {
|
|
11156
|
+
return starts;
|
|
11157
|
+
}
|
|
11158
|
+
for (const f of entries) {
|
|
11159
|
+
const m = SEGMENT_LOG_RE.exec(f);
|
|
11160
|
+
if (!m) continue;
|
|
11161
|
+
try {
|
|
11162
|
+
const st = statSync9(join18(dir, f));
|
|
11163
|
+
starts.set(Number(m[1]), st.birthtimeMs > 0 ? st.birthtimeMs : st.ctimeMs);
|
|
11164
|
+
} catch {
|
|
11165
|
+
}
|
|
11166
|
+
}
|
|
11167
|
+
return starts;
|
|
11168
|
+
}
|
|
11169
|
+
function segmentWindow(starts, index, nowMs) {
|
|
11170
|
+
const from = starts.get(index);
|
|
11171
|
+
if (from === void 0) return null;
|
|
11172
|
+
let to = null;
|
|
11173
|
+
for (const [i, at] of starts) {
|
|
11174
|
+
if (i > index && (to === null || at < to)) to = at;
|
|
11175
|
+
}
|
|
11176
|
+
if (to !== null) return { from, to: Math.max(to, from) };
|
|
11177
|
+
if (nowMs - from > RESCUE_OPEN_WINDOW_MAX_MS) return null;
|
|
11178
|
+
return { from, to: nowMs };
|
|
11179
|
+
}
|
|
11180
|
+
function rescueSegmentSpend(deps, run, executor, starts, file, nowMs) {
|
|
11181
|
+
if (!executor.rescueSpend || !run.worktree_path) return null;
|
|
11182
|
+
const index = Number(/^(\d+)\.json$/.exec(file)?.[1]);
|
|
11183
|
+
if (!Number.isFinite(index)) return null;
|
|
11184
|
+
const window = segmentWindow(starts, index, nowMs);
|
|
11185
|
+
if (!window) return null;
|
|
11186
|
+
let spend = null;
|
|
11187
|
+
try {
|
|
11188
|
+
spend = executor.rescueSpend(run.worktree_path, window.from, window.to);
|
|
11189
|
+
} catch (e) {
|
|
11190
|
+
console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: \u0437\u0430\u043F\u0438\u0441\u044C \u0441\u0435\u0441\u0441\u0438\u0438 \u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043B\u0430\u0441\u044C (${file}) \u2014 ${String(e)}`);
|
|
11191
|
+
return null;
|
|
11192
|
+
}
|
|
11193
|
+
if (!spend) return null;
|
|
11194
|
+
console.warn(
|
|
11195
|
+
`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: \u0438\u0442\u043E\u0433 ${file} \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D (\u0443\u0431\u0438\u0442 \u043D\u0430 \u043F\u043E\u043B\u043F\u0443\u0442\u0438) \u2014 \u0442\u0440\u0430\u0442\u0430 \u0441\u043D\u044F\u0442\u0430 \u0438\u0437 \u0437\u0430\u043F\u0438\u0441\u0438 \u0441\u0435\u0441\u0441\u0438\u0438: $${spend.costUsd.toFixed(4)}, \u0442\u043E\u043A\u0435\u043D\u043E\u0432 ${spend.tokensIn ?? 0}/${spend.tokensOut ?? 0} (TASWM-548)`
|
|
11196
|
+
);
|
|
11197
|
+
return spend;
|
|
11198
|
+
}
|
|
10937
11199
|
function accountRunCost(deps, run) {
|
|
10938
11200
|
const dir = findRunLogDir(deps.runLogsDir, deps.db.runLogKeys(run.id));
|
|
10939
11201
|
const files = dir ? unprocessedLogFiles(dir) : [];
|
|
10940
11202
|
if (!dir || files.length === 0) return;
|
|
10941
11203
|
const model = resolveRunModel(run, deps.swarmModels?.() ?? null);
|
|
10942
11204
|
const executor = deps.executor ?? resolveExecutor();
|
|
11205
|
+
const now = deps.now?.() ?? Date.now();
|
|
11206
|
+
const starts = segmentStarts(dir);
|
|
10943
11207
|
let added = 0;
|
|
10944
11208
|
let tokensIn = 0;
|
|
10945
11209
|
let tokensOut = 0;
|
|
10946
11210
|
let durationMs = 0;
|
|
11211
|
+
let unmeasured = 0;
|
|
10947
11212
|
const processed = [];
|
|
10948
11213
|
for (const f of files) {
|
|
10949
|
-
const full =
|
|
11214
|
+
const full = join18(dir, f);
|
|
10950
11215
|
let raw = null;
|
|
10951
11216
|
try {
|
|
10952
|
-
raw =
|
|
11217
|
+
raw = readFileSync15(full, "utf8");
|
|
10953
11218
|
} catch {
|
|
10954
11219
|
continue;
|
|
10955
11220
|
}
|
|
10956
|
-
const result = executor.parseResult(raw, model);
|
|
11221
|
+
const result = executor.parseResult(raw, model) ?? rescueSegmentSpend(deps, run, executor, starts, f, now);
|
|
10957
11222
|
if (!result) {
|
|
10958
|
-
|
|
11223
|
+
unmeasured += 1;
|
|
11224
|
+
console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: \u043D\u0435 \u0440\u0430\u0441\u043F\u0430\u0440\u0441\u0438\u043B\u0441\u044F ${full} \u0438 \u043D\u0435 \u0441\u043F\u0430\u0441\u0451\u043D \u0438\u0437 \u0437\u0430\u043F\u0438\u0441\u0438 \u0441\u0435\u0441\u0441\u0438\u0438 \u2014 \u043F\u043E\u043C\u0435\u0447\u0435\u043D \xAB\u043D\u0435 \u0438\u0437\u043C\u0435\u0440\u0435\u043D\u043E\xBB (TASWM-548)`);
|
|
10959
11225
|
} else {
|
|
10960
11226
|
if (result.warning) console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${result.warning}`);
|
|
10961
11227
|
added += result.costUsd;
|
|
@@ -10965,12 +11231,16 @@ function accountRunCost(deps, run) {
|
|
|
10965
11231
|
}
|
|
10966
11232
|
processed.push(full);
|
|
10967
11233
|
}
|
|
10968
|
-
if (added > 0 || tokensIn > 0 || tokensOut > 0 || durationMs > 0) {
|
|
11234
|
+
if (added > 0 || tokensIn > 0 || tokensOut > 0 || durationMs > 0 || unmeasured > 0) {
|
|
10969
11235
|
deps.db.updateRun(run.id, {
|
|
10970
11236
|
cost_usd: run.cost_usd + added,
|
|
10971
11237
|
tokens_in: run.tokens_in + tokensIn,
|
|
10972
11238
|
tokens_out: run.tokens_out + tokensOut,
|
|
10973
|
-
duration_ms: run.duration_ms + durationMs
|
|
11239
|
+
duration_ms: run.duration_ms + durationMs,
|
|
11240
|
+
// Отметка «не измерено» копится тем же единственным updateRun, что и деньги: разъехаться
|
|
11241
|
+
// «цену дописали, а признак неполноты потеряли» эти величины не должны — отчёт читает их
|
|
11242
|
+
// ПАРОЙ, и ноль при cost_unmeasured=0 значит «бесплатно», а при >0 — «не знаю».
|
|
11243
|
+
cost_unmeasured: run.cost_unmeasured + unmeasured
|
|
10974
11244
|
});
|
|
10975
11245
|
}
|
|
10976
11246
|
for (const full of processed) {
|
|
@@ -10998,8 +11268,8 @@ function makeCostSweeper(deps) {
|
|
|
10998
11268
|
}
|
|
10999
11269
|
|
|
11000
11270
|
// src/runOutcome.ts
|
|
11001
|
-
import { readdirSync as
|
|
11002
|
-
import { join as
|
|
11271
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync16 } from "fs";
|
|
11272
|
+
import { join as join19 } from "path";
|
|
11003
11273
|
init_executors();
|
|
11004
11274
|
function endedItself(outcome) {
|
|
11005
11275
|
return outcome === "graceful" || outcome === "budget" || outcome === "aborted";
|
|
@@ -11013,7 +11283,7 @@ function readLatestResultRaw(runLogsDir, runLogKeys) {
|
|
|
11013
11283
|
if (!dir) return null;
|
|
11014
11284
|
let files;
|
|
11015
11285
|
try {
|
|
11016
|
-
files =
|
|
11286
|
+
files = readdirSync11(dir);
|
|
11017
11287
|
} catch {
|
|
11018
11288
|
return null;
|
|
11019
11289
|
}
|
|
@@ -11026,7 +11296,7 @@ function readLatestResultRaw(runLogsDir, runLogKeys) {
|
|
|
11026
11296
|
}
|
|
11027
11297
|
if (!latest) return null;
|
|
11028
11298
|
try {
|
|
11029
|
-
return
|
|
11299
|
+
return readFileSync16(join19(dir, latest.name), "utf8");
|
|
11030
11300
|
} catch {
|
|
11031
11301
|
return null;
|
|
11032
11302
|
}
|
|
@@ -11059,20 +11329,20 @@ init_src();
|
|
|
11059
11329
|
init_git();
|
|
11060
11330
|
|
|
11061
11331
|
// src/deps.ts
|
|
11062
|
-
import { existsSync as existsSync14, readdirSync as
|
|
11063
|
-
import { join as
|
|
11332
|
+
import { existsSync as existsSync14, readdirSync as readdirSync13, readFileSync as readFileSync18 } from "fs";
|
|
11333
|
+
import { join as join23 } from "path";
|
|
11064
11334
|
import { execa } from "execa";
|
|
11065
11335
|
|
|
11066
11336
|
// src/prepareLock.ts
|
|
11067
11337
|
import { createHash } from "crypto";
|
|
11068
|
-
import { closeSync as closeSync4, openSync as openSync4, rmSync as rmSync2, statSync as
|
|
11069
|
-
import { join as
|
|
11338
|
+
import { closeSync as closeSync4, openSync as openSync4, rmSync as rmSync2, statSync as statSync11, writeSync } from "fs";
|
|
11339
|
+
import { join as join21 } from "path";
|
|
11070
11340
|
import { tmpdir } from "os";
|
|
11071
11341
|
var WAIT_TIMEOUT_MS = 5 * 6e4;
|
|
11072
11342
|
var POLL_MS = 300;
|
|
11073
11343
|
var STALE_MS = 20 * 6e4;
|
|
11074
11344
|
function machineLockPath(key, dir = tmpdir()) {
|
|
11075
|
-
return
|
|
11345
|
+
return join21(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
|
|
11076
11346
|
}
|
|
11077
11347
|
function defaultTryCreate(path, content) {
|
|
11078
11348
|
try {
|
|
@@ -11089,7 +11359,7 @@ function defaultTryCreate(path, content) {
|
|
|
11089
11359
|
}
|
|
11090
11360
|
function defaultAgeMs(path) {
|
|
11091
11361
|
try {
|
|
11092
|
-
return Date.now() -
|
|
11362
|
+
return Date.now() - statSync11(path).mtimeMs;
|
|
11093
11363
|
} catch {
|
|
11094
11364
|
return null;
|
|
11095
11365
|
}
|
|
@@ -11132,9 +11402,9 @@ init_config();
|
|
|
11132
11402
|
// src/envCache.ts
|
|
11133
11403
|
init_config();
|
|
11134
11404
|
import { createHash as createHash2 } from "crypto";
|
|
11135
|
-
import { cp, link, lstat, mkdir as mkdir2, readdir, readlink, rename, rm as rm2, stat, symlink, utimes, writeFile as writeFile2 } from "fs/promises";
|
|
11136
|
-
import { existsSync as existsSync13, readFileSync as
|
|
11137
|
-
import { join as
|
|
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";
|
|
11406
|
+
import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
|
|
11407
|
+
import { join as join22 } from "path";
|
|
11138
11408
|
function fingerprint(parts) {
|
|
11139
11409
|
const h = createHash2("sha256");
|
|
11140
11410
|
for (const p of parts) {
|
|
@@ -11148,8 +11418,8 @@ async function cloneTree(src, dst) {
|
|
|
11148
11418
|
const entries = await readdir(src, { withFileTypes: true });
|
|
11149
11419
|
await Promise.all(
|
|
11150
11420
|
entries.map(async (e) => {
|
|
11151
|
-
const from =
|
|
11152
|
-
const to =
|
|
11421
|
+
const from = join22(src, e.name);
|
|
11422
|
+
const to = join22(dst, e.name);
|
|
11153
11423
|
if (e.isDirectory()) return cloneTree(from, to);
|
|
11154
11424
|
if (e.isSymbolicLink()) {
|
|
11155
11425
|
return symlink(await readlink(from), to).catch(() => cp(from, to, { recursive: true, verbatimSymlinks: true }));
|
|
@@ -11162,11 +11432,11 @@ function cacheRoot(io) {
|
|
|
11162
11432
|
return io.root ?? ENV_CACHE_DIR;
|
|
11163
11433
|
}
|
|
11164
11434
|
function snapshotDir(io, key) {
|
|
11165
|
-
return
|
|
11435
|
+
return join22(cacheRoot(io), key);
|
|
11166
11436
|
}
|
|
11167
11437
|
function readManifest(dir) {
|
|
11168
11438
|
try {
|
|
11169
|
-
const parsed = JSON.parse(
|
|
11439
|
+
const parsed = JSON.parse(readFileSync17(join22(dir, "manifest.json"), "utf8"));
|
|
11170
11440
|
if (!parsed || typeof parsed !== "object") return null;
|
|
11171
11441
|
const m = parsed;
|
|
11172
11442
|
if (typeof m.key !== "string" || !Array.isArray(m.dirs)) return null;
|
|
@@ -11179,7 +11449,7 @@ function readManifest(dir) {
|
|
|
11179
11449
|
}
|
|
11180
11450
|
function hasEnvSnapshot(key, io = {}) {
|
|
11181
11451
|
const exists = io.exists ?? existsSync13;
|
|
11182
|
-
return exists(
|
|
11452
|
+
return exists(join22(snapshotDir(io, key), "manifest.json"));
|
|
11183
11453
|
}
|
|
11184
11454
|
async function restoreEnvSnapshot(worktree, key, io = {}) {
|
|
11185
11455
|
const exists = io.exists ?? existsSync13;
|
|
@@ -11191,13 +11461,13 @@ async function restoreEnvSnapshot(worktree, key, io = {}) {
|
|
|
11191
11461
|
try {
|
|
11192
11462
|
for (let i = 0; i < manifest.dirs.length; i++) {
|
|
11193
11463
|
const rel = manifest.dirs[i] ?? "";
|
|
11194
|
-
const target =
|
|
11464
|
+
const target = join22(worktree, rel);
|
|
11195
11465
|
if (exists(target)) continue;
|
|
11196
|
-
await clone(
|
|
11466
|
+
await clone(join22(dir, "trees", String(i)), target);
|
|
11197
11467
|
made.push(rel);
|
|
11198
11468
|
}
|
|
11199
11469
|
} catch {
|
|
11200
|
-
for (const rel of made) await rm2(
|
|
11470
|
+
for (const rel of made) await rm2(join22(worktree, rel), { recursive: true, force: true }).catch(() => {
|
|
11201
11471
|
});
|
|
11202
11472
|
return null;
|
|
11203
11473
|
}
|
|
@@ -11213,17 +11483,17 @@ async function saveEnvSnapshot(worktree, key, dirs, io = {}) {
|
|
|
11213
11483
|
const exists = io.exists ?? existsSync13;
|
|
11214
11484
|
const clone = io.clone ?? cloneTree;
|
|
11215
11485
|
if (hasEnvSnapshot(key, io)) return true;
|
|
11216
|
-
const present = dirs.filter((rel) => exists(
|
|
11486
|
+
const present = dirs.filter((rel) => exists(join22(worktree, rel)));
|
|
11217
11487
|
if (!present.length) return false;
|
|
11218
11488
|
const root = cacheRoot(io);
|
|
11219
|
-
const tmp =
|
|
11489
|
+
const tmp = join22(root, `.tmp-${key}-${process.pid}`);
|
|
11220
11490
|
try {
|
|
11221
11491
|
await rm2(tmp, { recursive: true, force: true });
|
|
11222
|
-
await mkdir2(
|
|
11223
|
-
for (let i = 0; i < present.length; i++) await clone(
|
|
11492
|
+
await mkdir2(join22(tmp, "trees"), { recursive: true });
|
|
11493
|
+
for (let i = 0; i < present.length; i++) await clone(join22(worktree, present[i] ?? ""), join22(tmp, "trees", String(i)));
|
|
11224
11494
|
const manifest = { key, dirs: present, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
11225
|
-
await writeFile2(
|
|
11226
|
-
await
|
|
11495
|
+
await writeFile2(join22(tmp, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
11496
|
+
await rename2(tmp, snapshotDir(io, key));
|
|
11227
11497
|
return true;
|
|
11228
11498
|
} catch {
|
|
11229
11499
|
await rm2(tmp, { recursive: true, force: true }).catch(() => {
|
|
@@ -11242,13 +11512,13 @@ async function pruneEnvSnapshots(io = {}) {
|
|
|
11242
11512
|
}
|
|
11243
11513
|
const withTime = [];
|
|
11244
11514
|
for (const name of names) {
|
|
11245
|
-
const s = await stat(
|
|
11515
|
+
const s = await stat(join22(root, name)).catch(() => null);
|
|
11246
11516
|
if (s) withTime.push({ name, mtime: s.mtimeMs });
|
|
11247
11517
|
}
|
|
11248
11518
|
withTime.sort((a, b) => b.mtime - a.mtime);
|
|
11249
11519
|
let removed = 0;
|
|
11250
11520
|
for (const { name } of withTime.slice(Math.max(0, keep))) {
|
|
11251
|
-
await rm2(
|
|
11521
|
+
await rm2(join22(root, name), { recursive: true, force: true }).catch(() => {
|
|
11252
11522
|
});
|
|
11253
11523
|
removed++;
|
|
11254
11524
|
}
|
|
@@ -11264,9 +11534,9 @@ async function sweepEnvCacheTemp(io = {}) {
|
|
|
11264
11534
|
}
|
|
11265
11535
|
let removed = 0;
|
|
11266
11536
|
for (const name of names) {
|
|
11267
|
-
const s = await lstat(
|
|
11537
|
+
const s = await lstat(join22(root, name)).catch(() => null);
|
|
11268
11538
|
if (!s || Date.now() - s.mtimeMs < 60 * 6e4) continue;
|
|
11269
|
-
await rm2(
|
|
11539
|
+
await rm2(join22(root, name), { recursive: true, force: true }).catch(() => {
|
|
11270
11540
|
});
|
|
11271
11541
|
removed++;
|
|
11272
11542
|
}
|
|
@@ -11293,21 +11563,21 @@ async function runCommand(cmd, worktree) {
|
|
|
11293
11563
|
}
|
|
11294
11564
|
function safeReadText(p) {
|
|
11295
11565
|
try {
|
|
11296
|
-
return
|
|
11566
|
+
return readFileSync18(p, "utf8");
|
|
11297
11567
|
} catch {
|
|
11298
11568
|
return null;
|
|
11299
11569
|
}
|
|
11300
11570
|
}
|
|
11301
11571
|
function safeListDirs(p) {
|
|
11302
11572
|
try {
|
|
11303
|
-
return
|
|
11573
|
+
return readdirSync13(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
11304
11574
|
} catch {
|
|
11305
11575
|
return [];
|
|
11306
11576
|
}
|
|
11307
11577
|
}
|
|
11308
11578
|
function safeListFiles(p) {
|
|
11309
11579
|
try {
|
|
11310
|
-
return
|
|
11580
|
+
return readdirSync13(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
|
|
11311
11581
|
} catch {
|
|
11312
11582
|
return [];
|
|
11313
11583
|
}
|
|
@@ -11335,19 +11605,19 @@ function overridePlan(override) {
|
|
|
11335
11605
|
function detectPackageManager(worktree, io = {}) {
|
|
11336
11606
|
const exists = io.exists ?? existsSync14;
|
|
11337
11607
|
const readText = io.readText ?? safeReadText;
|
|
11338
|
-
const hasPkgJson = exists(
|
|
11608
|
+
const hasPkgJson = exists(join23(worktree, "package.json"));
|
|
11339
11609
|
if (!hasPkgJson) {
|
|
11340
|
-
if (exists(
|
|
11610
|
+
if (exists(join23(worktree, "deno.json")) || exists(join23(worktree, "deno.jsonc")) || exists(join23(worktree, "deno.lock"))) return "deno";
|
|
11341
11611
|
return null;
|
|
11342
11612
|
}
|
|
11343
|
-
const pkg = readJson(readText,
|
|
11613
|
+
const pkg = readJson(readText, join23(worktree, "package.json"));
|
|
11344
11614
|
const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
|
|
11345
11615
|
if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
|
|
11346
|
-
if (exists(
|
|
11347
|
-
if (exists(
|
|
11348
|
-
if (exists(
|
|
11349
|
-
if (exists(
|
|
11350
|
-
if (exists(
|
|
11616
|
+
if (exists(join23(worktree, "bun.lock")) || exists(join23(worktree, "bun.lockb"))) return "bun";
|
|
11617
|
+
if (exists(join23(worktree, "pnpm-lock.yaml"))) return "pnpm";
|
|
11618
|
+
if (exists(join23(worktree, "yarn.lock"))) return "yarn";
|
|
11619
|
+
if (exists(join23(worktree, "package-lock.json"))) return "npm";
|
|
11620
|
+
if (exists(join23(worktree, "deno.lock"))) return "deno";
|
|
11351
11621
|
return "npm";
|
|
11352
11622
|
}
|
|
11353
11623
|
function jsInstallCommand(pm) {
|
|
@@ -11411,7 +11681,7 @@ function detectPlan(worktree, io) {
|
|
|
11411
11681
|
// node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
|
|
11412
11682
|
// У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
|
|
11413
11683
|
// заново, но он дешёвый: тянет из того же общего кэша.
|
|
11414
|
-
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists:
|
|
11684
|
+
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join23(worktree, "node_modules"), source: "detect" }
|
|
11415
11685
|
];
|
|
11416
11686
|
for (const target of workspaceBuildTargets(worktree, io)) {
|
|
11417
11687
|
commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
|
|
@@ -11420,12 +11690,12 @@ function detectPlan(worktree, io) {
|
|
|
11420
11690
|
return { source: "detect", ecosystem: `node/${pm}`, commands };
|
|
11421
11691
|
}
|
|
11422
11692
|
for (const marker of ECOSYSTEM_MARKERS) {
|
|
11423
|
-
const hit = marker.files.some((f) => exists(
|
|
11693
|
+
const hit = marker.files.some((f) => exists(join23(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
|
|
11424
11694
|
if (!hit) continue;
|
|
11425
11695
|
let ecosystem = marker.ecosystem;
|
|
11426
11696
|
let command = marker.command;
|
|
11427
11697
|
for (const refine of marker.refine ?? []) {
|
|
11428
|
-
if (!exists(
|
|
11698
|
+
if (!exists(join23(worktree, refine.file))) continue;
|
|
11429
11699
|
ecosystem = refine.ecosystem;
|
|
11430
11700
|
command = refine.command;
|
|
11431
11701
|
break;
|
|
@@ -11482,12 +11752,12 @@ var CI_INSTALL_PATTERNS = [
|
|
|
11482
11752
|
function ciWorkflowPlan(worktree, io) {
|
|
11483
11753
|
const readText = io.readText ?? safeReadText;
|
|
11484
11754
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
11485
|
-
const dir =
|
|
11755
|
+
const dir = join23(worktree, ".github", "workflows");
|
|
11486
11756
|
const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
|
|
11487
11757
|
const seen = /* @__PURE__ */ new Set();
|
|
11488
11758
|
const commands = [];
|
|
11489
11759
|
for (const name of files.sort()) {
|
|
11490
|
-
const yaml = readText(
|
|
11760
|
+
const yaml = readText(join23(dir, name));
|
|
11491
11761
|
if (yaml === null) continue;
|
|
11492
11762
|
for (const raw of workflowRunLines(yaml)) {
|
|
11493
11763
|
const line = raw.replace(/\s+/g, " ").trim();
|
|
@@ -11502,7 +11772,7 @@ function ciWorkflowPlan(worktree, io) {
|
|
|
11502
11772
|
return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
|
|
11503
11773
|
}
|
|
11504
11774
|
function workspaceGlobs(worktree, readText) {
|
|
11505
|
-
const yaml = readText(
|
|
11775
|
+
const yaml = readText(join23(worktree, "pnpm-workspace.yaml"));
|
|
11506
11776
|
if (yaml !== null) {
|
|
11507
11777
|
const globs = [];
|
|
11508
11778
|
let inPackages = false;
|
|
@@ -11522,7 +11792,7 @@ function workspaceGlobs(worktree, readText) {
|
|
|
11522
11792
|
}
|
|
11523
11793
|
if (globs.length) return globs;
|
|
11524
11794
|
}
|
|
11525
|
-
const pkg = readJson(readText,
|
|
11795
|
+
const pkg = readJson(readText, join23(worktree, "package.json"));
|
|
11526
11796
|
const ws = pkg?.workspaces;
|
|
11527
11797
|
const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
|
|
11528
11798
|
return list.filter((g) => typeof g === "string");
|
|
@@ -11533,14 +11803,14 @@ function workspaceDirs(worktree, globs, io) {
|
|
|
11533
11803
|
if (glob.startsWith("!")) continue;
|
|
11534
11804
|
const star = glob.indexOf("*");
|
|
11535
11805
|
if (star === -1) {
|
|
11536
|
-
const dir =
|
|
11537
|
-
if (io.exists(
|
|
11806
|
+
const dir = join23(worktree, glob);
|
|
11807
|
+
if (io.exists(join23(dir, "package.json"))) dirs.add(dir);
|
|
11538
11808
|
continue;
|
|
11539
11809
|
}
|
|
11540
|
-
const base3 =
|
|
11810
|
+
const base3 = join23(worktree, glob.slice(0, star).replace(/\/$/, ""));
|
|
11541
11811
|
for (const name of io.listDirs(base3)) {
|
|
11542
|
-
const dir =
|
|
11543
|
-
if (io.exists(
|
|
11812
|
+
const dir = join23(base3, name);
|
|
11813
|
+
if (io.exists(join23(dir, "package.json"))) dirs.add(dir);
|
|
11544
11814
|
}
|
|
11545
11815
|
}
|
|
11546
11816
|
return [...dirs];
|
|
@@ -11568,11 +11838,11 @@ function workspaceBuildTargets(worktree, io = {}) {
|
|
|
11568
11838
|
if (!globs.length) return [];
|
|
11569
11839
|
const pkgs = [];
|
|
11570
11840
|
for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
|
|
11571
|
-
const pkg = readJson(readText,
|
|
11841
|
+
const pkg = readJson(readText, join23(dir, "package.json"));
|
|
11572
11842
|
if (!pkg || typeof pkg.name !== "string") continue;
|
|
11573
11843
|
const scripts = pkg.scripts ?? {};
|
|
11574
11844
|
const entries = entryPaths(pkg);
|
|
11575
|
-
const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(
|
|
11845
|
+
const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join23(dir, e)));
|
|
11576
11846
|
const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
|
|
11577
11847
|
pkgs.push({ name: pkg.name, dir, deps, needsBuild });
|
|
11578
11848
|
}
|
|
@@ -11600,7 +11870,7 @@ function devDatabaseUrl(worktree, io = {}) {
|
|
|
11600
11870
|
const fromEnv = (io.env ?? process.env).DATABASE_URL;
|
|
11601
11871
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
11602
11872
|
for (const file of [".env", ".env.example"]) {
|
|
11603
|
-
const raw = readText(
|
|
11873
|
+
const raw = readText(join23(worktree, file));
|
|
11604
11874
|
if (raw === null) continue;
|
|
11605
11875
|
const m = /^\s*(?:export\s+)?DATABASE_URL\s*=\s*(.*)$/m.exec(raw);
|
|
11606
11876
|
const value = (m?.[1] ?? "").trim().replace(/^['"]|['"]$/g, "");
|
|
@@ -11620,7 +11890,7 @@ function prismaPackages(worktree, io) {
|
|
|
11620
11890
|
const readText = io.readText ?? safeReadText;
|
|
11621
11891
|
const listDirs = io.listDirs ?? safeListDirs;
|
|
11622
11892
|
const dirs = [worktree, ...workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs })];
|
|
11623
|
-
return dirs.filter((dir) => exists(
|
|
11893
|
+
return dirs.filter((dir) => exists(join23(dir, "prisma", "schema.prisma")) && listDirs(join23(dir, "prisma", "migrations")).length > 0);
|
|
11624
11894
|
}
|
|
11625
11895
|
function jsExecBin(pm, command) {
|
|
11626
11896
|
switch (pm) {
|
|
@@ -11657,18 +11927,18 @@ function envCachePlan(worktree, io = {}) {
|
|
|
11657
11927
|
if (!pm) return null;
|
|
11658
11928
|
const parts = [`pm:${pm}`];
|
|
11659
11929
|
for (const name of ENV_DEFINING_FILES) {
|
|
11660
|
-
const raw = readText(
|
|
11930
|
+
const raw = readText(join23(worktree, name));
|
|
11661
11931
|
if (raw !== null) parts.push(`${name}
|
|
11662
11932
|
${raw}`);
|
|
11663
11933
|
}
|
|
11664
11934
|
const pkgDirs = workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs });
|
|
11665
11935
|
const rels = pkgDirs.map((dir) => dir.slice(worktree.length + 1)).sort();
|
|
11666
11936
|
for (const rel of ["", ...rels]) {
|
|
11667
|
-
const raw = readText(
|
|
11937
|
+
const raw = readText(join23(worktree, rel, "package.json"));
|
|
11668
11938
|
if (raw !== null) parts.push(`${rel}/package.json
|
|
11669
11939
|
${raw}`);
|
|
11670
11940
|
}
|
|
11671
|
-
return { key: fingerprint(parts), dirs: ["node_modules", ...rels.map((rel) =>
|
|
11941
|
+
return { key: fingerprint(parts), dirs: ["node_modules", ...rels.map((rel) => join23(rel, "node_modules"))] };
|
|
11672
11942
|
}
|
|
11673
11943
|
function planPrepare(worktree, io = {}) {
|
|
11674
11944
|
const plan = overridePlan(io.override) ?? detectPlan(worktree, io) ?? ciWorkflowPlan(worktree, io);
|
|
@@ -11693,7 +11963,7 @@ async function ensureWorktreeDeps(worktree, io = {}) {
|
|
|
11693
11963
|
restored: [],
|
|
11694
11964
|
cached: false
|
|
11695
11965
|
};
|
|
11696
|
-
if (cachePlan && !exists(
|
|
11966
|
+
if (cachePlan && !exists(join23(worktree, "node_modules"))) {
|
|
11697
11967
|
report.restored = await restoreEnvSnapshot(worktree, cachePlan.key, cacheIO) ?? [];
|
|
11698
11968
|
}
|
|
11699
11969
|
const plan = planPrepare(worktree, io);
|
|
@@ -11880,6 +12150,48 @@ function pruneDefers(episodes, live) {
|
|
|
11880
12150
|
for (const key of episodes.keys()) if (!live.has(key)) episodes.delete(key);
|
|
11881
12151
|
}
|
|
11882
12152
|
|
|
12153
|
+
// src/spawnRetry.ts
|
|
12154
|
+
var numEnv4 = (name, def) => {
|
|
12155
|
+
const v = Number(process.env[name]);
|
|
12156
|
+
return Number.isFinite(v) && v > 0 ? v : def;
|
|
12157
|
+
};
|
|
12158
|
+
var DEFAULT_SPAWN_RETRY_CONFIG = {
|
|
12159
|
+
baseMs: numEnv4("SWARM_SPAWN_RETRY_BASE_MS", 3e4),
|
|
12160
|
+
maxMs: numEnv4("SWARM_SPAWN_RETRY_MAX_MS", 12e4)
|
|
12161
|
+
};
|
|
12162
|
+
function spawnRetryBackoffMs(attempts, cfg = DEFAULT_SPAWN_RETRY_CONFIG) {
|
|
12163
|
+
const n = Math.max(0, Math.trunc(attempts));
|
|
12164
|
+
const grown = n >= 30 ? cfg.maxMs : cfg.baseMs * 2 ** n;
|
|
12165
|
+
return Math.min(cfg.maxMs, grown);
|
|
12166
|
+
}
|
|
12167
|
+
function noteSpawnWaiting(state, runId, now) {
|
|
12168
|
+
const prev = state.get(runId);
|
|
12169
|
+
if (prev) return prev;
|
|
12170
|
+
const entry = { since: now, attempts: 0, lastAt: now };
|
|
12171
|
+
state.set(runId, entry);
|
|
12172
|
+
return entry;
|
|
12173
|
+
}
|
|
12174
|
+
function spawnRetryDue(state, runId, now, cfg = DEFAULT_SPAWN_RETRY_CONFIG) {
|
|
12175
|
+
const entry = state.get(runId);
|
|
12176
|
+
if (!entry) return false;
|
|
12177
|
+
return now - entry.lastAt >= spawnRetryBackoffMs(entry.attempts, cfg);
|
|
12178
|
+
}
|
|
12179
|
+
function markSpawnRetry(state, runId, now) {
|
|
12180
|
+
const entry = noteSpawnWaiting(state, runId, now);
|
|
12181
|
+
entry.attempts += 1;
|
|
12182
|
+
entry.lastAt = now;
|
|
12183
|
+
return entry.attempts;
|
|
12184
|
+
}
|
|
12185
|
+
function clearSpawnRetry(state, runId) {
|
|
12186
|
+
state.delete(runId);
|
|
12187
|
+
}
|
|
12188
|
+
function holdSpawnRetries(state, now) {
|
|
12189
|
+
for (const entry of state.values()) entry.lastAt = now;
|
|
12190
|
+
}
|
|
12191
|
+
function pruneSpawnRetries(state, live) {
|
|
12192
|
+
for (const id of [...state.keys()]) if (!live.has(id)) state.delete(id);
|
|
12193
|
+
}
|
|
12194
|
+
|
|
11883
12195
|
// src/spawnAttempts.ts
|
|
11884
12196
|
init_src();
|
|
11885
12197
|
var PRE_START_ATTEMPT_LIMIT = 3;
|
|
@@ -11982,7 +12294,7 @@ function parkReasonUnretriable(reason) {
|
|
|
11982
12294
|
// package.json
|
|
11983
12295
|
var package_default = {
|
|
11984
12296
|
name: "@taskless-app/swarm",
|
|
11985
|
-
version: "0.6.
|
|
12297
|
+
version: "0.6.40",
|
|
11986
12298
|
type: "module",
|
|
11987
12299
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
11988
12300
|
keywords: [
|
|
@@ -12033,7 +12345,7 @@ var package_default = {
|
|
|
12033
12345
|
execa: "^10.0.0",
|
|
12034
12346
|
zod: "3.25.76"
|
|
12035
12347
|
},
|
|
12036
|
-
gitHead: "
|
|
12348
|
+
gitHead: "b58ef18678e8c0e03e0602a40bf76d6f0a6975c8"
|
|
12037
12349
|
};
|
|
12038
12350
|
|
|
12039
12351
|
// src/version.ts
|
|
@@ -12637,6 +12949,58 @@ ${out.slice(-CI_SUMMARY_LOG_CHARS)}` : out);
|
|
|
12637
12949
|
|
|
12638
12950
|
${logs.join("\n---\n")}` : header;
|
|
12639
12951
|
}
|
|
12952
|
+
var CI_INFRA_MARKERS = [
|
|
12953
|
+
/ENOTEMPTY/,
|
|
12954
|
+
/ENOENT: process.cwd/,
|
|
12955
|
+
/working directory was likely removed/,
|
|
12956
|
+
// Любой ENOENT, назвавший путь установки pnpm. Четвёртая грань той же гонки (02.09, 19:14,
|
|
12957
|
+
// PR #1074): «ENOENT: no such file or directory, chmod '…/setup-pnpm/…/pnpm/bin/pnpx.cjs'» —
|
|
12958
|
+
// установщик распаковал pnpm и пошёл выставлять права, а файла уже нет. Окно гонки тут не
|
|
12959
|
+
// «две джобы стартуют вместе», а миллисекунды посреди работы одного установщика.
|
|
12960
|
+
/ENOENT[^\n]*setup-pnpm/,
|
|
12961
|
+
/Cannot find module '[^']*setup-pnpm/,
|
|
12962
|
+
/Cannot find module '[^']*node_modules\/pnpm\/bin\/pnpm\.cjs'/,
|
|
12963
|
+
/self-installer exits with code/,
|
|
12964
|
+
// Сетевые отказы раннера. Тот же класс: чинить нечего, помогает перезапуск. Держатся
|
|
12965
|
+
// здесь потому, что ИМЕННО ЭТИ строки уже перечислены в `INFRA_MARKERS`
|
|
12966
|
+
// (.github/scripts/typecheck-verdict.sh) — там они не гасят прогон. Пока список был
|
|
12967
|
+
// только там, `ci.yml` вёл себя верно, а демон всё равно будил дрона и жёг попытку
|
|
12968
|
+
// CI_WAKE_LIMIT: разошедшиеся списки давали ровно ту потерю, ради которой заведена
|
|
12969
|
+
// CI-112. Расхождение теперь ловится тестом «списки маркеров сходятся с bash», а не
|
|
12970
|
+
// обещанием держать их одинаковыми.
|
|
12971
|
+
/EACCES/,
|
|
12972
|
+
/ETIMEDOUT/,
|
|
12973
|
+
/ECONNRESET/
|
|
12974
|
+
// Вердикта самой джобы (`::error title=Джоба typecheck не смогла запуститься…`,
|
|
12975
|
+
// .github/scripts/typecheck-verdict.sh) тут намеренно НЕТ, хотя соблазн есть: шаг вердикта
|
|
12976
|
+
// завершается успешно, а `gh run view --log-failed` отдаёт логи только УПАВШИХ шагов — до
|
|
12977
|
+
// роя эта строка не доезжает. Сырые следы из лога упавшего шага доезжают, по ним и судим.
|
|
12978
|
+
// Списки маркеров у скрипта и здесь держим одинаковыми руками: они судят один лог, но
|
|
12979
|
+
// общего кода у bash в `.github/` и пакета, уезжающего в npm, быть не может.
|
|
12980
|
+
];
|
|
12981
|
+
var CI_CODE_FAILURE_MARKERS = [
|
|
12982
|
+
/error TS\d/,
|
|
12983
|
+
/AssertionError/,
|
|
12984
|
+
/Test Files\s+\d+ failed/,
|
|
12985
|
+
/Tests\s+\d+ failed/,
|
|
12986
|
+
/ERR_PNPM_OUTDATED_LOCKFILE/,
|
|
12987
|
+
// Vitest-символы. `×` (U+00D7) сюда НЕ берём: он встречается в обычном тексте логов
|
|
12988
|
+
// (размеры вида 1920×1080) и делал бы «есть дефект кода» из любого скриншот-прогона.
|
|
12989
|
+
/✕|✗/,
|
|
12990
|
+
// Playwright: у него свои символы и своя сводка — ни `Test Files`, ни `✕` он не печатает.
|
|
12991
|
+
// Без этих двух строк упавший e2e рядом с инфра-падением соседней джобы уезжал в
|
|
12992
|
+
// «это окружение», и дрона не будили вовсе (логи ВСЕХ упавших джоб прогона приезжают
|
|
12993
|
+
// одним куском: `gh run view <run-id> --log-failed`, см. ciFailureSummary).
|
|
12994
|
+
/✘/,
|
|
12995
|
+
// НЕ якорим на начало строки: `gh run view --log-failed` печатает каждую строку с
|
|
12996
|
+
// префиксом «джоба<TAB>шаг<TAB>время», и якорь `^\s*` не совпал бы никогда.
|
|
12997
|
+
/\b\d+ failed\b/
|
|
12998
|
+
];
|
|
12999
|
+
function isInfraCiFailure(summary) {
|
|
13000
|
+
if (!summary) return false;
|
|
13001
|
+
if (CI_CODE_FAILURE_MARKERS.some((re) => re.test(summary))) return false;
|
|
13002
|
+
return CI_INFRA_MARKERS.some((re) => re.test(summary));
|
|
13003
|
+
}
|
|
12640
13004
|
|
|
12641
13005
|
// src/droneServer.ts
|
|
12642
13006
|
init_droneChannel();
|
|
@@ -12649,8 +13013,8 @@ import { z } from "zod";
|
|
|
12649
13013
|
init_git();
|
|
12650
13014
|
|
|
12651
13015
|
// src/diff.ts
|
|
12652
|
-
import { statSync as
|
|
12653
|
-
import { join as
|
|
13016
|
+
import { statSync as statSync12 } from "fs";
|
|
13017
|
+
import { join as join28 } from "path";
|
|
12654
13018
|
import { $ as $4 } from "execa";
|
|
12655
13019
|
var MAX_FILES = 200;
|
|
12656
13020
|
var MAX_LINES_PER_FILE = 1500;
|
|
@@ -12793,7 +13157,7 @@ async function resolveBase(wt, baseBranch) {
|
|
|
12793
13157
|
}
|
|
12794
13158
|
function mtime(path) {
|
|
12795
13159
|
try {
|
|
12796
|
-
return
|
|
13160
|
+
return statSync12(path).mtimeMs;
|
|
12797
13161
|
} catch {
|
|
12798
13162
|
return 0;
|
|
12799
13163
|
}
|
|
@@ -12818,7 +13182,7 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
12818
13182
|
files.push(pf);
|
|
12819
13183
|
}
|
|
12820
13184
|
}
|
|
12821
|
-
files.sort((a, b) => mtime(
|
|
13185
|
+
files.sort((a, b) => mtime(join28(worktree, b.path)) - mtime(join28(worktree, a.path)));
|
|
12822
13186
|
const truncatedFiles = files.length > MAX_FILES;
|
|
12823
13187
|
if (truncatedFiles) files = files.slice(0, MAX_FILES);
|
|
12824
13188
|
const added = files.reduce((s, f) => s + f.added, 0);
|
|
@@ -14898,20 +15262,20 @@ function makeMergeGateParts(deps) {
|
|
|
14898
15262
|
|
|
14899
15263
|
// src/envGate.ts
|
|
14900
15264
|
import { existsSync as existsSync17 } from "fs";
|
|
14901
|
-
import { join as
|
|
15265
|
+
import { join as join30 } from "path";
|
|
14902
15266
|
import { createConnection } from "net";
|
|
14903
15267
|
import { execa as execa2 } from "execa";
|
|
14904
15268
|
|
|
14905
15269
|
// src/platform.ts
|
|
14906
|
-
import { existsSync as existsSync16, readFileSync as
|
|
14907
|
-
import { delimiter, join as
|
|
15270
|
+
import { existsSync as existsSync16, readFileSync as readFileSync22 } from "fs";
|
|
15271
|
+
import { delimiter, join as join29 } from "path";
|
|
14908
15272
|
function resolveCommandPath(name, env = process.env, platform = process.platform) {
|
|
14909
15273
|
if (name.includes("/") || name.includes("\\")) return existsSync16(name) ? name : null;
|
|
14910
15274
|
const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
|
|
14911
15275
|
const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
|
|
14912
15276
|
for (const dir of dirs) {
|
|
14913
15277
|
for (const ext of exts) {
|
|
14914
|
-
const candidate =
|
|
15278
|
+
const candidate = join29(dir, name + ext);
|
|
14915
15279
|
if (existsSync16(candidate)) return candidate;
|
|
14916
15280
|
}
|
|
14917
15281
|
}
|
|
@@ -14996,7 +15360,7 @@ var COMPOSE_MARKERS = [
|
|
|
14996
15360
|
];
|
|
14997
15361
|
function detectRequirements(repo, io = {}) {
|
|
14998
15362
|
const exists = io.exists ?? existsSync17;
|
|
14999
|
-
const marker = COMPOSE_MARKERS.find((f) => exists(
|
|
15363
|
+
const marker = COMPOSE_MARKERS.find((f) => exists(join30(repo, f)));
|
|
15000
15364
|
return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
|
|
15001
15365
|
}
|
|
15002
15366
|
function resolveRequirements(repo, declared, io = {}) {
|
|
@@ -15018,11 +15382,11 @@ async function hasCommand(name, env) {
|
|
|
15018
15382
|
return hasCommandInPath(name, env);
|
|
15019
15383
|
}
|
|
15020
15384
|
async function portOpen(host, port) {
|
|
15021
|
-
return new Promise((
|
|
15385
|
+
return new Promise((resolve5) => {
|
|
15022
15386
|
const socket = createConnection({ host, port });
|
|
15023
15387
|
const finish = (ok) => {
|
|
15024
15388
|
socket.destroy();
|
|
15025
|
-
|
|
15389
|
+
resolve5(ok);
|
|
15026
15390
|
};
|
|
15027
15391
|
socket.setTimeout(PORT_TIMEOUT_MS);
|
|
15028
15392
|
socket.once("connect", () => finish(true));
|
|
@@ -16571,14 +16935,14 @@ function makeHandler(deps) {
|
|
|
16571
16935
|
const ref = url.searchParams.get("pr") ?? "";
|
|
16572
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 });
|
|
16573
16937
|
const ctxRepo = url.searchParams.get("repo");
|
|
16574
|
-
const
|
|
16938
|
+
const resolve5 = deps.mergeGate ?? makeMergeGateResolver({
|
|
16575
16939
|
listRuns: () => deps.db.listRuns(),
|
|
16576
16940
|
fallbackAutoReview: SWARM_AUTO_REVIEW,
|
|
16577
16941
|
autoVerify: SWARM_AUTO_VERIFY,
|
|
16578
16942
|
// TASWM-344
|
|
16579
16943
|
...defaultRepo ? { repoFallback: defaultRepo } : {}
|
|
16580
16944
|
});
|
|
16581
|
-
return Response.json(await
|
|
16945
|
+
return Response.json(await resolve5(ref, ctxRepo));
|
|
16582
16946
|
}
|
|
16583
16947
|
if (p === "/resources") return Response.json(hostResourceSnapshot());
|
|
16584
16948
|
if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
|
|
@@ -16731,22 +17095,22 @@ function startWeb(deps, port = WEB_PORT) {
|
|
|
16731
17095
|
return { port, server };
|
|
16732
17096
|
}
|
|
16733
17097
|
function closeWebServer(server) {
|
|
16734
|
-
return new Promise((
|
|
16735
|
-
server.close(() =>
|
|
17098
|
+
return new Promise((resolve5) => {
|
|
17099
|
+
server.close(() => resolve5());
|
|
16736
17100
|
server.closeAllConnections?.();
|
|
16737
17101
|
});
|
|
16738
17102
|
}
|
|
16739
17103
|
|
|
16740
17104
|
// src/hostCommands.ts
|
|
16741
17105
|
init_src();
|
|
16742
|
-
import { readFileSync as
|
|
16743
|
-
import { join as
|
|
17106
|
+
import { readFileSync as readFileSync23, renameSync as renameSync10, writeFileSync as writeFileSync15 } from "fs";
|
|
17107
|
+
import { join as join31 } from "path";
|
|
16744
17108
|
function hostCommandStatePath(home) {
|
|
16745
|
-
return
|
|
17109
|
+
return join31(home, "host-command.json");
|
|
16746
17110
|
}
|
|
16747
17111
|
function readState(path) {
|
|
16748
17112
|
try {
|
|
16749
|
-
return JSON.parse(
|
|
17113
|
+
return JSON.parse(readFileSync23(path, "utf8"));
|
|
16750
17114
|
} catch {
|
|
16751
17115
|
return {};
|
|
16752
17116
|
}
|
|
@@ -16917,7 +17281,7 @@ async function doRestart(cmd, opts, note) {
|
|
|
16917
17281
|
// src/selfUpdate.ts
|
|
16918
17282
|
init_git();
|
|
16919
17283
|
import { existsSync as existsSync18 } from "fs";
|
|
16920
|
-
import { join as
|
|
17284
|
+
import { join as join32 } from "path";
|
|
16921
17285
|
import { $ as $5 } from "execa";
|
|
16922
17286
|
var DAEMON_PACKAGE = "@taskless-app/swarm";
|
|
16923
17287
|
var defaultRun = async (cmd, args, cwd) => {
|
|
@@ -16959,7 +17323,7 @@ async function runSelfUpdate(io) {
|
|
|
16959
17323
|
};
|
|
16960
17324
|
}
|
|
16961
17325
|
await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
|
|
16962
|
-
if (!exists(
|
|
17326
|
+
if (!exists(join32(repo, "pnpm-lock.yaml"))) {
|
|
16963
17327
|
return { ok: false, step: "install", message: "\u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442 pnpm-lock.yaml \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u0435\u0433\u043E \u044D\u0442\u0438\u043C \u043F\u0443\u0442\u0451\u043C \u043D\u0435 \u0431\u0435\u0440\u0443\u0441\u044C" };
|
|
16964
17328
|
}
|
|
16965
17329
|
const installed2 = await run("pnpm", ["install", "--frozen-lockfile"], repo);
|
|
@@ -17066,7 +17430,7 @@ function executorHealthy(opts = {}) {
|
|
|
17066
17430
|
const executor = opts.executor ?? resolveExecutor();
|
|
17067
17431
|
const bin = opts.bin ?? executor.resolveBin();
|
|
17068
17432
|
const timeoutMs = opts.timeoutMs ?? 2e4;
|
|
17069
|
-
return new Promise((
|
|
17433
|
+
return new Promise((resolve5) => {
|
|
17070
17434
|
let done = false;
|
|
17071
17435
|
let out = "";
|
|
17072
17436
|
const finish = (r) => {
|
|
@@ -17077,7 +17441,7 @@ function executorHealthy(opts = {}) {
|
|
|
17077
17441
|
child.kill("SIGKILL");
|
|
17078
17442
|
} catch {
|
|
17079
17443
|
}
|
|
17080
|
-
|
|
17444
|
+
resolve5(r);
|
|
17081
17445
|
};
|
|
17082
17446
|
const child = spawn3(bin, [...executor.probe.args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
17083
17447
|
child.stdout?.on("data", (d) => {
|
|
@@ -17118,19 +17482,19 @@ async function fetchExternalStorm(url, opts = {}) {
|
|
|
17118
17482
|
}
|
|
17119
17483
|
|
|
17120
17484
|
// src/anomalyState.ts
|
|
17121
|
-
import { existsSync as existsSync19, readFileSync as
|
|
17122
|
-
import { join as
|
|
17485
|
+
import { existsSync as existsSync19, readFileSync as readFileSync24, renameSync as renameSync11, writeFileSync as writeFileSync16 } from "fs";
|
|
17486
|
+
import { join as join33 } from "path";
|
|
17123
17487
|
var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
17124
17488
|
var MAX_RESOLVED = 200;
|
|
17125
17489
|
function emptyState() {
|
|
17126
17490
|
return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
|
|
17127
17491
|
}
|
|
17128
17492
|
function anomalyStatePath(home) {
|
|
17129
|
-
return
|
|
17493
|
+
return join33(home, "anomaly-state.json");
|
|
17130
17494
|
}
|
|
17131
17495
|
function loadAnomalyState(home, now = Date.now(), io = {}) {
|
|
17132
17496
|
const exists = io.exists ?? existsSync19;
|
|
17133
|
-
const readFile2 = io.readFile ?? ((p) =>
|
|
17497
|
+
const readFile2 = io.readFile ?? ((p) => readFileSync24(p, "utf8"));
|
|
17134
17498
|
const path = anomalyStatePath(home);
|
|
17135
17499
|
if (!exists(path)) return emptyState();
|
|
17136
17500
|
try {
|
|
@@ -17642,6 +18006,7 @@ var CI_POLL_LIMIT_MARK = "\u043E\u043F\u0440\u043E\u0441 CI \u043E\u0441\u0442\u
|
|
|
17642
18006
|
var CI_DENIED_LIMIT = Number(process.env.SWARM_CI_DENIED_LIMIT) || 3;
|
|
17643
18007
|
var CI_DENIED_MARK = "\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u0447\u0435\u043A\u0430\u043C (403)";
|
|
17644
18008
|
var CI_DENIED_STOP_MARK = "\u043E\u043F\u0440\u043E\u0441 CI \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D: \u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 (403)";
|
|
18009
|
+
var CI_INFRA_MARK = "\u0443\u043F\u0430\u043B \u043D\u0435 \u043A\u043E\u0434, \u0430 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u0434\u0436\u043E\u0431\u044B";
|
|
17645
18010
|
var CI_WAIT_LIMIT_MS = Number(process.env.SWARM_CI_WAIT_LIMIT_MS) || 45 * 60 * 1e3;
|
|
17646
18011
|
var CI_POLL_LIMIT = Math.max(
|
|
17647
18012
|
Number(process.env.SWARM_CI_POLL_LIMIT) || 90,
|
|
@@ -18326,10 +18691,10 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
18326
18691
|
}
|
|
18327
18692
|
return parts.join("\n");
|
|
18328
18693
|
}
|
|
18329
|
-
var HERO_CHARTER_PATH =
|
|
18694
|
+
var HERO_CHARTER_PATH = join34(dirname5(THIS_FILE), "..", "docs", "HERO.md");
|
|
18330
18695
|
function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
|
|
18331
18696
|
const name = run.name || `#${run.id}`;
|
|
18332
|
-
const charter =
|
|
18697
|
+
const charter = readFileSync25(HERO_CHARTER_PATH, "utf8");
|
|
18333
18698
|
const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
|
|
18334
18699
|
|
|
18335
18700
|
${task.description}` : `\u042D\u041F\u0418\u041A ${run.task_slug} (\u0442\u0435\u043B\u043E \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E slug \u0438 \u0437\u0434\u0440\u0430\u0432\u043E\u043C\u0443 \u0441\u043C\u044B\u0441\u043B\u0443).`;
|
|
@@ -18757,9 +19122,9 @@ function repoHasCi(deps, run) {
|
|
|
18757
19122
|
function repoHasCiWorkflows(worktreePath, repo) {
|
|
18758
19123
|
for (const base3 of [worktreePath, repo]) {
|
|
18759
19124
|
if (!base3) continue;
|
|
18760
|
-
const dir =
|
|
19125
|
+
const dir = join34(base3, ".github", "workflows");
|
|
18761
19126
|
try {
|
|
18762
|
-
if (
|
|
19127
|
+
if (readdirSync14(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
|
|
18763
19128
|
} catch {
|
|
18764
19129
|
}
|
|
18765
19130
|
}
|
|
@@ -18850,6 +19215,21 @@ async function wakeAuthorOnRedCi(deps, run, now, ci, worktreeExists, hasTranscri
|
|
|
18850
19215
|
const wt = run.worktree_path;
|
|
18851
19216
|
if (!wt || !worktreeExists(wt) || !hasTranscript(wt)) return false;
|
|
18852
19217
|
const summary = await deps.ci.failureSummary(ci.failing, ci.failingRunIds).catch(() => null) ?? `\u274C CI \u0443\u043F\u0430\u043B: ${ci.failing.join(", ") || "(\u0441\u043C. gh pr checks)"}`;
|
|
19218
|
+
if (isInfraCiFailure(summary)) {
|
|
19219
|
+
const seen = ciStreak(deps.db, run.id, CI_INFRA_MARK).count;
|
|
19220
|
+
deps.db.addEvent(
|
|
19221
|
+
run.id,
|
|
19222
|
+
"ci_checked",
|
|
19223
|
+
`CI fail (${ci.failing.join(", ") || "?"}) \u2014 ${CI_INFRA_MARK}; \u0434\u0440\u043E\u043D\u0430 \u043D\u0435 \u0431\u0443\u0436\u0443, \u0447\u0438\u043D\u0438\u0442\u0441\u044F \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u043E\u043C \u0434\u0436\u043E\u0431 [${CI_WAIT_POLL_MARK}]`
|
|
19224
|
+
);
|
|
19225
|
+
if (seen === 0) {
|
|
19226
|
+
narrate(
|
|
19227
|
+
deps,
|
|
19228
|
+
`CI \u0443 ${droneLabel(run)} \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u043D\u0435 \u043F\u043E \u043A\u043E\u0434\u0443 \u2014 \u0434\u0436\u043E\u0431\u0430 \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u0430 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C\u0441\u044F (\u0440\u0430\u043D\u043D\u0435\u0440\u044B). \u0414\u0440\u043E\u043D\u0430 \u043D\u0435 \u0431\u0443\u0436\u0443: \u0447\u0438\u043D\u0438\u0442\u044C \u0435\u043C\u0443 \u0442\u0430\u043C \u043D\u0435\u0447\u0435\u0433\u043E, \u043D\u0443\u0436\u0435\u043D \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0443\u043F\u0430\u0432\u0448\u0438\u0445 \u0434\u0436\u043E\u0431 (gh run rerun --failed).`
|
|
19229
|
+
);
|
|
19230
|
+
}
|
|
19231
|
+
return true;
|
|
19232
|
+
}
|
|
18853
19233
|
try {
|
|
18854
19234
|
await deps.respawn(run, "continue", buildCiWakeNudge(summary));
|
|
18855
19235
|
deps.db.addEvent(run.id, "ci_wake", `CI \u0443\u043F\u0430\u043B (${ci.failing.join(", ") || "?"}) \u2014 \u0431\u0443\u0436\u0443 \u0434\u0440\u043E\u043D\u0430 \u0441\u043E \u0441\u0436\u0430\u0442\u044B\u043C summary`);
|
|
@@ -19638,9 +20018,22 @@ async function manageRelief(deps, now, droneCap, stormActive) {
|
|
|
19638
20018
|
const needsRelief = deps.reliefNeeded ? deps.reliefNeeded() : false;
|
|
19639
20019
|
const resourcesOk2 = deps.resourcesOk();
|
|
19640
20020
|
state.reliefSince = needsRelief ? state.reliefSince ?? now : null;
|
|
20021
|
+
if (state.reliefSince == null) state.soleWorkerNoted = false;
|
|
19641
20022
|
if (!resourcesOk2 || needsRelief) state.overloadedAt = now;
|
|
19642
20023
|
if (needsRelief && deps.kill && pauseDue(state.reliefSince, state.lastPauseAt ?? null, now, cfg)) {
|
|
19643
|
-
const
|
|
20024
|
+
const running = deps.db.listRuns("running");
|
|
20025
|
+
if (reliefWouldStrangleSoleWorker(running)) {
|
|
20026
|
+
const alone = loadBearingRuns(running)[0];
|
|
20027
|
+
if (alone && !state.soleWorkerNoted) {
|
|
20028
|
+
state.soleWorkerNoted = true;
|
|
20029
|
+
const why = reliefSoleWorkerReason(deps.reliefDecisionText?.() ?? null);
|
|
20030
|
+
deps.db.addEvent(alone.id, "note", why);
|
|
20031
|
+
narrate(deps, `${droneLabel(alone)} \u043E\u0434\u0438\u043D \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u2014 \u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u0435\u0433\u043E \u043D\u0435 \u0441\u043D\u0438\u043C\u0430\u0435\u0442. ${why}`);
|
|
20032
|
+
console.warn(`[overmind] \u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430: ${why}`);
|
|
20033
|
+
}
|
|
20034
|
+
return;
|
|
20035
|
+
}
|
|
20036
|
+
const candidate = pickPauseCandidate(running, new Set(heavyRunsInFlight(deps).map((r) => r.id)));
|
|
19644
20037
|
if (candidate) {
|
|
19645
20038
|
const decisionText = deps.reliefDecisionText?.() ?? null;
|
|
19646
20039
|
let stepsSinceResume = null;
|
|
@@ -19777,6 +20170,43 @@ async function manageRelief(deps, now, droneCap, stormActive) {
|
|
|
19777
20170
|
return;
|
|
19778
20171
|
}
|
|
19779
20172
|
}
|
|
20173
|
+
function hostNotAcceptingReason(deps, droneCap, stormActive) {
|
|
20174
|
+
if (stormActive) return "\u0440\u043E\u0439 \u0432 \u0448\u0442\u043E\u0440\u043C\u043E\u0432\u043E\u043C \u0440\u0435\u0436\u0438\u043C\u0435";
|
|
20175
|
+
if (deps.control?.paused) return "\u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043B \u043F\u0440\u0438\u0451\u043C \u043D\u043E\u0432\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435";
|
|
20176
|
+
if (deps.resourcesOk && !deps.resourcesOk()) return deps.gateReason?.() ?? "\u0440\u0435\u0441\u0443\u0440\u0441-\u0433\u0435\u0439\u0442 \u0437\u0430\u043A\u0440\u044B\u0442";
|
|
20177
|
+
const active = countActiveDrones(deps.db);
|
|
20178
|
+
if (active >= droneCap) return `\u0441\u043B\u043E\u0442\u043E\u0432 \u043D\u0435\u0442 (\u0434\u0440\u043E\u043D\u043E\u0432 ${active} \u0438\u0437 ${droneCap})`;
|
|
20179
|
+
return null;
|
|
20180
|
+
}
|
|
20181
|
+
async function retryDeferredSpawns(deps, now, droneCap, stormActive) {
|
|
20182
|
+
const state = deps.spawnRetry ??= /* @__PURE__ */ new Map();
|
|
20183
|
+
const cfg = deps.spawnRetryConfig ? { ...DEFAULT_SPAWN_RETRY_CONFIG, ...deps.spawnRetryConfig } : DEFAULT_SPAWN_RETRY_CONFIG;
|
|
20184
|
+
const waiting = orderQueue(deps.db.listRuns("queued").filter((r) => deps.db.unmetDeps(r.depends_on).length === 0));
|
|
20185
|
+
pruneSpawnRetries(state, new Set(waiting.map((r) => r.id)));
|
|
20186
|
+
if (!waiting.length) return;
|
|
20187
|
+
for (const run of waiting) noteSpawnWaiting(state, run.id, now);
|
|
20188
|
+
const closed = hostNotAcceptingReason(deps, droneCap, stormActive);
|
|
20189
|
+
if (closed) {
|
|
20190
|
+
holdSpawnRetries(state, now);
|
|
20191
|
+
return;
|
|
20192
|
+
}
|
|
20193
|
+
const relief = deps.relief ?? (deps.relief = {});
|
|
20194
|
+
const reliefCfg = relief.config ? resolveReliefConfig(relief.config) : DEFAULT_RELIEF_CONFIG;
|
|
20195
|
+
if (!rampDue(relief.lastRampAt ?? null, now, reliefCfg)) return;
|
|
20196
|
+
for (const run of waiting) {
|
|
20197
|
+
if (!spawnRetryDue(state, run.id, now, cfg)) continue;
|
|
20198
|
+
const entry = state.get(run.id);
|
|
20199
|
+
const attempt = markSpawnRetry(state, run.id, now);
|
|
20200
|
+
console.error(
|
|
20201
|
+
`[overmind] \u043F\u043E\u0432\u0442\u043E\u0440 \u043E\u0442\u043B\u043E\u0436\u0435\u043D\u043D\u043E\u0433\u043E \u0441\u043F\u0430\u0432\u043D\u0430 \u2116${attempt}: ${droneLabel(run)} (${run.task_slug}) \u0436\u0434\u0451\u0442 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 ${humanMinutes(now - entry.since)}, \u0445\u043E\u0441\u0442 \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0435\u0442 \u2014 \u043F\u0440\u043E\u0431\u0443\u044E \u0441\u043D\u043E\u0432\u0430 (\u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u043E\u0442\u0441\u0442\u0443\u043F ${Math.round(spawnRetryBackoffMs(attempt, cfg) / 1e3)}\u0441, TASWM-549)`
|
|
20202
|
+
);
|
|
20203
|
+
const outcome = await trySpawnQueuedRun(deps, run);
|
|
20204
|
+
if (outcome !== "skipped") clearSpawnRetry(state, run.id);
|
|
20205
|
+
if (outcome === "closed") continue;
|
|
20206
|
+
if (outcome !== "skipped") relief.lastRampAt = now;
|
|
20207
|
+
return;
|
|
20208
|
+
}
|
|
20209
|
+
}
|
|
19780
20210
|
async function buildReliefCostCandidatesFor(deps, now, droneCap) {
|
|
19781
20211
|
const state = deps.relief?.cost;
|
|
19782
20212
|
if (!state) return [];
|
|
@@ -20162,6 +20592,11 @@ async function tickOnce(deps) {
|
|
|
20162
20592
|
if (!stormActive) {
|
|
20163
20593
|
await resumeWatchdogParked(deps, droneCap);
|
|
20164
20594
|
}
|
|
20595
|
+
try {
|
|
20596
|
+
await retryDeferredSpawns(deps, now, droneCap, stormActive);
|
|
20597
|
+
} catch (e) {
|
|
20598
|
+
console.error("[overmind] \u043F\u043E\u0432\u0442\u043E\u0440 \u043E\u0442\u043B\u043E\u0436\u0435\u043D\u043D\u043E\u0433\u043E \u0441\u043F\u0430\u0432\u043D\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
20599
|
+
}
|
|
20165
20600
|
for (let run of deps.db.listRuns("review")) {
|
|
20166
20601
|
try {
|
|
20167
20602
|
if (run.resolves_run_id != null) {
|
|
@@ -20800,6 +21235,27 @@ async function replayDecision(deps, qid, chosen, by = "human") {
|
|
|
20800
21235
|
deps.db.supersede(old.id, fresh.id, by);
|
|
20801
21236
|
return fresh;
|
|
20802
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
|
+
}
|
|
20803
21259
|
async function cancelRun(deps, slug, reason) {
|
|
20804
21260
|
const run = deps.db.activeRunForSlug(slug);
|
|
20805
21261
|
if (!run) return null;
|
|
@@ -20808,11 +21264,21 @@ async function cancelRun(deps, slug, reason) {
|
|
|
20808
21264
|
deps.kill(run.pid);
|
|
20809
21265
|
} catch {
|
|
20810
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
|
+
}
|
|
20811
21274
|
}
|
|
20812
21275
|
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
20813
21276
|
});
|
|
20814
21277
|
return deps.db.cancel(run.id, reason);
|
|
20815
21278
|
}
|
|
21279
|
+
function detachedHelperWorktree(db) {
|
|
21280
|
+
return (repo, branch, wt) => prepareDetachedWorktree(repo, branch, wt, { isWorktreeHeldLive: (p) => worktreePathHeldLive(db, p) });
|
|
21281
|
+
}
|
|
20816
21282
|
async function resolveConflict(deps, reviewRun) {
|
|
20817
21283
|
const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
|
|
20818
21284
|
const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
|
|
@@ -20830,7 +21296,7 @@ async function resolveConflict(deps, reviewRun) {
|
|
|
20830
21296
|
model: reviewRun.model,
|
|
20831
21297
|
review_model: reviewRun.review_model
|
|
20832
21298
|
});
|
|
20833
|
-
const wt =
|
|
21299
|
+
const wt = join34(deps.worktreesDir, basename6(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
|
|
20834
21300
|
try {
|
|
20835
21301
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
20836
21302
|
await deps.git.setWorktreeIdentity(wt, resolver.name);
|
|
@@ -20909,7 +21375,7 @@ async function spawnReviewer(deps, reviewRun, files) {
|
|
|
20909
21375
|
// (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
|
|
20910
21376
|
workspace_id: reviewRun.workspace_id
|
|
20911
21377
|
});
|
|
20912
|
-
const wt =
|
|
21378
|
+
const wt = join34(deps.worktreesDir, basename6(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
|
|
20913
21379
|
try {
|
|
20914
21380
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
20915
21381
|
await deps.git.setWorktreeIdentity(wt, reviewer.name);
|
|
@@ -20990,7 +21456,7 @@ async function spawnVerifier(deps, verifiedRun, files) {
|
|
|
20990
21456
|
// штамп цикла: фикс-итерацию проверяет свежий верификатор
|
|
20991
21457
|
workspace_id: verifiedRun.workspace_id
|
|
20992
21458
|
});
|
|
20993
|
-
const wt =
|
|
21459
|
+
const wt = join34(deps.worktreesDir, basename6(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
|
|
20994
21460
|
try {
|
|
20995
21461
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
20996
21462
|
await deps.git.setWorktreeIdentity(wt, verifier.name);
|
|
@@ -21142,7 +21608,7 @@ async function makeStore(repo, worktreesDir, repoFlag) {
|
|
|
21142
21608
|
console.warn(`[overmind] \u043D\u0435 \u0441\u043C\u043E\u0433 \u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u044C \u043F\u0443\u0442\u044C \u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044E (${e instanceof Error ? e.message : e}) \u2014 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0437\u0430\u043F\u0443\u0441\u043A \u0441\u043D\u043E\u0432\u0430 \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u0442 --repo`);
|
|
21143
21609
|
}
|
|
21144
21610
|
}
|
|
21145
|
-
const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW,
|
|
21611
|
+
const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW, homedir7(), repoPaths);
|
|
21146
21612
|
const repoMap = emptyRepoMapState(scanRootsList);
|
|
21147
21613
|
if (SWARM_BACKEND !== "client") {
|
|
21148
21614
|
const { SwarmDb: SwarmDb2 } = await loadSqlite();
|
|
@@ -21236,7 +21702,7 @@ async function migrateWorktreesAtStartup() {
|
|
|
21236
21702
|
exists: (d) => existsSync20(d),
|
|
21237
21703
|
isNonEmptyDir: (d) => {
|
|
21238
21704
|
try {
|
|
21239
|
-
return
|
|
21705
|
+
return readdirSync14(d).length > 0;
|
|
21240
21706
|
} catch {
|
|
21241
21707
|
return false;
|
|
21242
21708
|
}
|
|
@@ -21627,7 +22093,7 @@ async function runDaemon(opts) {
|
|
|
21627
22093
|
repoFallback: opts.repo,
|
|
21628
22094
|
worktreesDir,
|
|
21629
22095
|
launch,
|
|
21630
|
-
git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
|
|
22096
|
+
git: { prepareDetachedWorktree: detachedHelperWorktree(db), setWorktreeIdentity, removeWorktree },
|
|
21631
22097
|
// TASWM-356: ветка ревьюера — фактическая ветка PR. Штатно она уже в поле прогона
|
|
21632
22098
|
// (реконсайл синхронит её из headBranch), этот ридер — для прогонов без неё.
|
|
21633
22099
|
prHeadBranch,
|
|
@@ -21662,7 +22128,7 @@ async function runDaemon(opts) {
|
|
|
21662
22128
|
repoFallback: opts.repo,
|
|
21663
22129
|
worktreesDir,
|
|
21664
22130
|
launch,
|
|
21665
|
-
git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
|
|
22131
|
+
git: { prepareDetachedWorktree: detachedHelperWorktree(db), setWorktreeIdentity, removeWorktree },
|
|
21666
22132
|
prHeadBranch,
|
|
21667
22133
|
...verifyModeReader ? { verifyMode: verifyModeReader } : {},
|
|
21668
22134
|
...checkerAttachments ? { taskAttachments: checkerAttachments } : {}
|
|
@@ -22006,7 +22472,7 @@ async function runDaemon(opts) {
|
|
|
22006
22472
|
repoFallback: opts.repo,
|
|
22007
22473
|
worktreesDir,
|
|
22008
22474
|
launch,
|
|
22009
|
-
git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree },
|
|
22475
|
+
git: { prepareDetachedWorktree: detachedHelperWorktree(db), setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree },
|
|
22010
22476
|
prHeadBranch
|
|
22011
22477
|
// TASWM-356: ветка PR для прогонов без записанного поля branch
|
|
22012
22478
|
},
|
|
@@ -22204,6 +22670,10 @@ async function runDaemon(opts) {
|
|
|
22204
22670
|
{
|
|
22205
22671
|
db,
|
|
22206
22672
|
kill: (pid) => killProcessGroup(pid, "SIGTERM"),
|
|
22673
|
+
// TASWM-553: остановка обязана ОСВОБОДИТЬ машину — значит, дождаться смерти
|
|
22674
|
+
// процесса и только потом сносить worktree (иначе платит следующий спавн).
|
|
22675
|
+
isAlive: pidAlive,
|
|
22676
|
+
hardKill: (pid) => killProcessGroup(pid, "SIGKILL"),
|
|
22207
22677
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
|
|
22208
22678
|
},
|
|
22209
22679
|
slug,
|
|
@@ -22389,6 +22859,9 @@ async function runDaemon(opts) {
|
|
|
22389
22859
|
{
|
|
22390
22860
|
db,
|
|
22391
22861
|
kill: (pid) => killProcessGroup(pid, "SIGTERM"),
|
|
22862
|
+
isAlive: pidAlive,
|
|
22863
|
+
// TASWM-553: снос worktree — после смерти процесса, не следом за SIGTERM
|
|
22864
|
+
hardKill: (pid) => killProcessGroup(pid, "SIGKILL"),
|
|
22392
22865
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
|
|
22393
22866
|
},
|
|
22394
22867
|
slug,
|
|
@@ -22489,6 +22962,7 @@ export {
|
|
|
22489
22962
|
CI_DENIED_LIMIT,
|
|
22490
22963
|
CI_DENIED_MARK,
|
|
22491
22964
|
CI_DENIED_STOP_MARK,
|
|
22965
|
+
CI_INFRA_MARK,
|
|
22492
22966
|
CI_NO_CHECKS_MARK,
|
|
22493
22967
|
CI_POLL_LIMIT,
|
|
22494
22968
|
CI_POLL_LIMIT_MARK,
|