@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.
Files changed (3) hide show
  1. package/dist/cli.js +364 -168
  2. package/dist/overmind.js +752 -278
  3. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -1366,7 +1366,7 @@ function buildZoneModel(entries, opts = {}) {
1366
1366
  if (!m) labelVotes.set(z, m = /* @__PURE__ */ new Map());
1367
1367
  bump(m, manual);
1368
1368
  }
1369
- const rename = /* @__PURE__ */ new Map();
1369
+ const rename2 = /* @__PURE__ */ new Map();
1370
1370
  const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
1371
1371
  const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
1372
1372
  const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
@@ -1375,11 +1375,11 @@ function buildZoneModel(entries, opts = {}) {
1375
1375
  for (const [zone, votes] of clustersByEvidence) {
1376
1376
  const top = topKey(votes);
1377
1377
  if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
1378
- rename.set(zone, top.key);
1378
+ rename2.set(zone, top.key);
1379
1379
  takenNames.add(top.key);
1380
1380
  }
1381
- const renamed = (z) => rename.get(z) ?? z;
1382
- if (rename.size) {
1381
+ const renamed = (z) => rename2.get(z) ?? z;
1382
+ if (rename2.size) {
1383
1383
  for (const [f, z] of fileZone) fileZone.set(f, renamed(z));
1384
1384
  for (const [s, z] of taskZone) taskZone.set(s, renamed(z));
1385
1385
  }
@@ -2018,10 +2018,94 @@ var init_claudeResult = __esm({
2018
2018
  }
2019
2019
  });
2020
2020
 
2021
- // src/executors/claudeCode.ts
2022
- import { readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
2021
+ // src/transcriptSpend.ts
2022
+ import { readFileSync as readFileSync2, readdirSync as readdirSync2, statSync } from "fs";
2023
2023
  import { homedir as homedir2 } from "os";
2024
2024
  import { join as join2 } from "path";
2025
+ function transcriptDir(worktree, projectsDir) {
2026
+ return join2(projectsDir, worktree.replace(/[/.]/g, "-"));
2027
+ }
2028
+ function transcriptSpendInWindow(worktree, fromMs, toMs, opts = {}) {
2029
+ const projectsDir = opts.projectsDir ?? join2(homedir2(), ".claude", "projects");
2030
+ const empty = { costUsd: 0, tokensIn: 0, tokensOut: 0, durationMs: 0, replies: 0, unpriced: 0, unpricedModel: null };
2031
+ const dir = transcriptDir(worktree, projectsDir);
2032
+ let files;
2033
+ try {
2034
+ files = readdirSync2(dir).filter((f) => f.endsWith(".jsonl"));
2035
+ } catch {
2036
+ return empty;
2037
+ }
2038
+ const seen = /* @__PURE__ */ new Set();
2039
+ let costUsd = 0;
2040
+ let tokensIn = 0;
2041
+ let tokensOut = 0;
2042
+ let firstAt = null;
2043
+ let lastAt = null;
2044
+ let replies = 0;
2045
+ let unpriced = 0;
2046
+ let unpricedModel = null;
2047
+ for (const f of files) {
2048
+ const path = join2(dir, f);
2049
+ let raw;
2050
+ try {
2051
+ if (statSync(path).mtimeMs < fromMs) continue;
2052
+ raw = readFileSync2(path, "utf8");
2053
+ } catch {
2054
+ continue;
2055
+ }
2056
+ for (const line of raw.split("\n")) {
2057
+ if (!line.startsWith("{") || !line.includes('"usage"')) continue;
2058
+ let entry;
2059
+ try {
2060
+ entry = JSON.parse(line);
2061
+ } catch {
2062
+ continue;
2063
+ }
2064
+ if (entry.type !== "assistant") continue;
2065
+ const usage2 = entry.message?.usage;
2066
+ if (!usage2) continue;
2067
+ const at = entry.timestamp ? Date.parse(entry.timestamp) : NaN;
2068
+ if (!Number.isFinite(at) || at < fromMs || at >= toMs) continue;
2069
+ const key = entry.requestId ?? entry.message?.id ?? entry.uuid;
2070
+ if (!key || seen.has(key)) continue;
2071
+ seen.add(key);
2072
+ const model = entry.message?.model ?? null;
2073
+ const est = costFromUsage(usage2, model, opts.table);
2074
+ costUsd += est.costUsd;
2075
+ if (est.warning) {
2076
+ unpriced += 1;
2077
+ unpricedModel ??= model;
2078
+ }
2079
+ const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
2080
+ tokensIn += num(usage2.input_tokens) + num(usage2.cache_creation_input_tokens) + num(usage2.cache_read_input_tokens);
2081
+ tokensOut += num(usage2.output_tokens);
2082
+ if (firstAt === null || at < firstAt) firstAt = at;
2083
+ if (lastAt === null || at > lastAt) lastAt = at;
2084
+ replies += 1;
2085
+ }
2086
+ }
2087
+ if (replies === 0) return empty;
2088
+ return {
2089
+ costUsd,
2090
+ tokensIn,
2091
+ tokensOut,
2092
+ durationMs: firstAt !== null && lastAt !== null ? lastAt - firstAt : 0,
2093
+ replies,
2094
+ unpriced,
2095
+ unpricedModel
2096
+ };
2097
+ }
2098
+ var init_transcriptSpend = __esm({
2099
+ "src/transcriptSpend.ts"() {
2100
+ "use strict";
2101
+ init_pricing();
2102
+ }
2103
+ });
2104
+
2105
+ // src/executors/claudeCode.ts
2106
+ import { readdirSync as readdirSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
2107
+ import { homedir as homedir3 } from "os";
2108
+ import { join as join3 } from "path";
2025
2109
  function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings, maxBudgetUsd, effort) {
2026
2110
  const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
2027
2111
  const budgetFlags = typeof maxBudgetUsd === "number" && maxBudgetUsd > 0 ? ["--max-budget-usd", String(maxBudgetUsd)] : [];
@@ -2053,21 +2137,21 @@ function droneSettingsJson(paths, mergeGateHook = null) {
2053
2137
  if (mergeGateHook) settings.hooks = { PreToolUse: [{ matcher: "Bash", hooks: [{ type: "command", command: mergeGateHook }] }] };
2054
2138
  return JSON.stringify(settings);
2055
2139
  }
2056
- function transcriptDir(worktree, projectsDir) {
2057
- return join2(projectsDir, worktree.replace(/[/.]/g, "-"));
2140
+ function transcriptDir2(worktree, projectsDir) {
2141
+ return join3(projectsDir, worktree.replace(/[/.]/g, "-"));
2058
2142
  }
2059
- function sessionTranscriptExists(worktree, projectsDir = join2(homedir2(), ".claude", "projects")) {
2143
+ function sessionTranscriptExists(worktree, projectsDir = join3(homedir3(), ".claude", "projects")) {
2060
2144
  try {
2061
- return readdirSync2(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
2145
+ return readdirSync3(transcriptDir2(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
2062
2146
  } catch {
2063
2147
  return false;
2064
2148
  }
2065
2149
  }
2066
- function transcriptMtimeMs(worktree, projectsDir = join2(homedir2(), ".claude", "projects")) {
2067
- const dir = transcriptDir(worktree, projectsDir);
2150
+ function transcriptMtimeMs(worktree, projectsDir = join3(homedir3(), ".claude", "projects")) {
2151
+ const dir = transcriptDir2(worktree, projectsDir);
2068
2152
  let files;
2069
2153
  try {
2070
- files = readdirSync2(dir);
2154
+ files = readdirSync3(dir);
2071
2155
  } catch {
2072
2156
  return null;
2073
2157
  }
@@ -2075,7 +2159,7 @@ function transcriptMtimeMs(worktree, projectsDir = join2(homedir2(), ".claude",
2075
2159
  for (const f of files) {
2076
2160
  if (!f.endsWith(".jsonl")) continue;
2077
2161
  try {
2078
- const mtime2 = statSync(join2(dir, f)).mtimeMs;
2162
+ const mtime2 = statSync2(join3(dir, f)).mtimeMs;
2079
2163
  if (latest === null || mtime2 > latest) latest = mtime2;
2080
2164
  } catch {
2081
2165
  }
@@ -2088,7 +2172,7 @@ function writeMcpConfig(spec) {
2088
2172
  [spec.mcp.name]: { command: spec.mcp.command, args: spec.mcp.args, env: spec.mcp.env }
2089
2173
  }
2090
2174
  };
2091
- const path = join2(spec.worktree, MCP_CONFIG_FILE);
2175
+ const path = join3(spec.worktree, MCP_CONFIG_FILE);
2092
2176
  writeFileSync2(path, JSON.stringify(cfg, null, 2));
2093
2177
  return path;
2094
2178
  }
@@ -2103,6 +2187,7 @@ var init_claudeCode = __esm({
2103
2187
  init_swarmFiles();
2104
2188
  init_mergeGateHook();
2105
2189
  init_claudeResult();
2190
+ init_transcriptSpend();
2106
2191
  ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
2107
2192
  MCP_CONFIG_FILE = SWARM_MCP_CONFIG_FILE;
2108
2193
  claudeCodeExecutor = {
@@ -2141,6 +2226,26 @@ var init_claudeCode = __esm({
2141
2226
  ...abortField(sessionAbort(result))
2142
2227
  };
2143
2228
  },
2229
+ // TASWM-548: итога нет — берём трату из транскрипта сессии. Тот пишется по ходу работы и
2230
+ // переживает SIGTERM разгрузки, из-за которого итог и не был напечатан; формат usage там
2231
+ // тот же, что в итоге, поэтому цена считается тем же прайсингом (см. transcriptSpend.ts).
2232
+ rescueSpend(worktree, fromMs, toMs) {
2233
+ const spend = transcriptSpendInWindow(worktree, fromMs, toMs);
2234
+ if (spend.replies === 0) return null;
2235
+ return {
2236
+ costUsd: spend.costUsd,
2237
+ tokensIn: spend.tokensIn,
2238
+ tokensOut: spend.tokensOut,
2239
+ durationMs: spend.durationMs,
2240
+ // Часть ответов не по чему было тарифицировать (модели нет в MODEL_PRICING) — их
2241
+ // токены в трату вошли, а деньги НЕТ. Тот же канал, что и у warning штатного итога
2242
+ // (cost.accountRunCost его печатает): иначе спасённые $0 при сожжённых токенах
2243
+ // выглядели бы как честный ноль.
2244
+ ...spend.unpriced > 0 ? {
2245
+ 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`
2246
+ } : {}
2247
+ };
2248
+ },
2144
2249
  session: {
2145
2250
  exists: (worktree) => sessionTranscriptExists(worktree),
2146
2251
  mtimeMs: (worktree) => transcriptMtimeMs(worktree)
@@ -2158,34 +2263,34 @@ var init_claudeCode = __esm({
2158
2263
  });
2159
2264
 
2160
2265
  // src/swarmHome.ts
2161
- import { homedir as homedir3 } from "os";
2162
- import { join as join3 } from "path";
2266
+ import { homedir as homedir4 } from "os";
2267
+ import { join as join4 } from "path";
2163
2268
  import { mkdirSync as mkdirSync2 } from "fs";
2164
2269
  var SWARM_HOME;
2165
2270
  var init_swarmHome = __esm({
2166
2271
  "src/swarmHome.ts"() {
2167
2272
  "use strict";
2168
- SWARM_HOME = process.env.SWARM_HOME || join3(homedir3(), ".taskless-swarm");
2273
+ SWARM_HOME = process.env.SWARM_HOME || join4(homedir4(), ".taskless-swarm");
2169
2274
  mkdirSync2(SWARM_HOME, { recursive: true });
2170
2275
  }
2171
2276
  });
2172
2277
 
2173
2278
  // src/executors/codex.ts
2174
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, statSync as statSync2, symlinkSync } from "fs";
2175
- import { join as join4 } from "path";
2279
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync4, statSync as statSync3, symlinkSync } from "fs";
2280
+ import { join as join5 } from "path";
2176
2281
  function resolveCodexBin(env = process.env) {
2177
2282
  return (env.SWARM_CODEX_BIN ?? "").trim() || "codex";
2178
2283
  }
2179
2284
  function codexHomePath(worktree, swarmHome = SWARM_HOME) {
2180
- return join4(swarmHome, "executor-state", "codex", worktree.replace(/[/.]/g, "-"));
2285
+ return join5(swarmHome, "executor-state", "codex", worktree.replace(/[/.]/g, "-"));
2181
2286
  }
2182
2287
  function ensureCodexHome(worktree, realHome, env = process.env, swarmHome = SWARM_HOME) {
2183
2288
  const home = codexHomePath(worktree, swarmHome);
2184
2289
  mkdirSync3(home, { recursive: true });
2185
- const real = (env.CODEX_HOME ?? "").trim() || join4(realHome, ".codex");
2290
+ const real = (env.CODEX_HOME ?? "").trim() || join5(realHome, ".codex");
2186
2291
  for (const file of ["auth.json", "config.toml"]) {
2187
- const target = join4(real, file);
2188
- const link = join4(home, file);
2292
+ const target = join5(real, file);
2293
+ const link = join5(home, file);
2189
2294
  if (!existsSync3(target) || existsSync3(link)) continue;
2190
2295
  try {
2191
2296
  symlinkSync(target, link);
@@ -2244,16 +2349,16 @@ function parseCodexStream(raw) {
2244
2349
  function newestRolloutMtime(dir, depth = 4) {
2245
2350
  let entries;
2246
2351
  try {
2247
- entries = readdirSync3(dir);
2352
+ entries = readdirSync4(dir);
2248
2353
  } catch {
2249
2354
  return null;
2250
2355
  }
2251
2356
  let latest = null;
2252
2357
  for (const name of entries) {
2253
- const full = join4(dir, name);
2358
+ const full = join5(dir, name);
2254
2359
  let st;
2255
2360
  try {
2256
- st = statSync2(full);
2361
+ st = statSync3(full);
2257
2362
  } catch {
2258
2363
  continue;
2259
2364
  }
@@ -2324,8 +2429,8 @@ ${notice}` : spec.prompt;
2324
2429
  return est.warning ? { costUsd: est.costUsd, warning: est.warning } : { costUsd: est.costUsd };
2325
2430
  },
2326
2431
  session: {
2327
- exists: (worktree) => newestRolloutMtime(join4(codexHomePath(worktree), "sessions")) !== null,
2328
- mtimeMs: (worktree) => newestRolloutMtime(join4(codexHomePath(worktree), "sessions"))
2432
+ exists: (worktree) => newestRolloutMtime(join5(codexHomePath(worktree), "sessions")) !== null,
2433
+ mtimeMs: (worktree) => newestRolloutMtime(join5(codexHomePath(worktree), "sessions"))
2329
2434
  },
2330
2435
  probe: {
2331
2436
  // Тот же принцип, что у claude: дешёвый реальный прогон — он проверяет и auth, и живость
@@ -2372,8 +2477,8 @@ var init_executors = __esm({
2372
2477
  });
2373
2478
 
2374
2479
  // src/config.ts
2375
- import { basename, join as join5, resolve } from "path";
2376
- import { readFileSync as readFileSync2, realpathSync, renameSync, writeFileSync as writeFileSync3 } from "fs";
2480
+ import { basename, join as join6, resolve } from "path";
2481
+ import { readFileSync as readFileSync3, realpathSync, renameSync, writeFileSync as writeFileSync3 } from "fs";
2377
2482
  function normalizeRepo(repo) {
2378
2483
  const r = (repo ?? "").trim();
2379
2484
  if (!r) return "";
@@ -2387,10 +2492,10 @@ function effectiveRepoOf(repo, fallback) {
2387
2492
  return normalizeRepo(repo || fallback || "");
2388
2493
  }
2389
2494
  function droneWorktreePath(worktreesDir, repo, slug) {
2390
- return join5(droneWorktreeRoot(worktreesDir, repo), slug);
2495
+ return join6(droneWorktreeRoot(worktreesDir, repo), slug);
2391
2496
  }
2392
2497
  function droneWorktreeRoot(worktreesDir, repo) {
2393
- return join5(worktreesDir, basename(normalizeRepo(repo)));
2498
+ return join6(worktreesDir, basename(normalizeRepo(repo)));
2394
2499
  }
2395
2500
  function resolveEffectiveWorktreesDir(deps) {
2396
2501
  if (deps.envOverride) return deps.envOverride;
@@ -2417,7 +2522,7 @@ function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
2417
2522
  }
2418
2523
  function tokenFromFile() {
2419
2524
  try {
2420
- return readFileSync2(join5(SWARM_HOME, "token"), "utf8").trim();
2525
+ return readFileSync3(join6(SWARM_HOME, "token"), "utf8").trim();
2421
2526
  } catch {
2422
2527
  return "";
2423
2528
  }
@@ -2430,25 +2535,25 @@ var init_config = __esm({
2430
2535
  init_executors();
2431
2536
  init_swarmHome();
2432
2537
  init_swarmHome();
2433
- DB_PATH = process.env.SWARM_DB || join5(SWARM_HOME, "swarm.db");
2538
+ DB_PATH = process.env.SWARM_DB || join6(SWARM_HOME, "swarm.db");
2434
2539
  EXECUTOR = resolveExecutor(process.env.SWARM_EXECUTOR);
2435
- DRONE_HOME = process.env.SWARM_DRONE_HOME || join5(SWARM_HOME, "drone-home");
2540
+ DRONE_HOME = process.env.SWARM_DRONE_HOME || join6(SWARM_HOME, "drone-home");
2436
2541
  DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
2437
2542
  DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
2438
- WORKTREES_NOINDEX_DIR = join5(SWARM_HOME, "worktrees.noindex");
2439
- WORKTREES_LEGACY_DIR = join5(SWARM_HOME, "worktrees");
2543
+ WORKTREES_NOINDEX_DIR = join6(SWARM_HOME, "worktrees.noindex");
2544
+ WORKTREES_LEGACY_DIR = join6(SWARM_HOME, "worktrees");
2440
2545
  WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
2441
- RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join5(SWARM_HOME, "run-logs");
2442
- DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join5(SWARM_HOME, "overmind.log");
2546
+ RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join6(SWARM_HOME, "run-logs");
2547
+ DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join6(SWARM_HOME, "overmind.log");
2443
2548
  DAEMON_LOG_MAX_BYTES = envNum("SWARM_DAEMON_LOG_MAX_BYTES", 16 * 1024 * 1024);
2444
2549
  DAEMON_LOG_KEEP = envNum("SWARM_DAEMON_LOG_KEEP", 2);
2445
2550
  DAEMON_LOG_CHECK_MS = envNum("SWARM_DAEMON_LOG_CHECK_MS", 6e4);
2446
2551
  DIST_BUILD = true;
2447
2552
  SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
2448
- SWARM_OUTBOX = process.env.SWARM_OUTBOX || join5(SWARM_HOME, "outbox.jsonl");
2553
+ SWARM_OUTBOX = process.env.SWARM_OUTBOX || join6(SWARM_HOME, "outbox.jsonl");
2449
2554
  SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
2450
2555
  SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
2451
- ENV_CACHE_DIR = process.env.SWARM_ENV_CACHE_DIR || join5(SWARM_HOME, "env-cache");
2556
+ ENV_CACHE_DIR = process.env.SWARM_ENV_CACHE_DIR || join6(SWARM_HOME, "env-cache");
2452
2557
  ENV_CACHE_ENABLED = process.env.SWARM_ENV_CACHE !== "0";
2453
2558
  ENV_CACHE_KEEP = Math.trunc(envNum("SWARM_ENV_CACHE_KEEP", 4));
2454
2559
  SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
@@ -2478,15 +2583,15 @@ var init_config = __esm({
2478
2583
  DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
2479
2584
  DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
2480
2585
  DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
2481
- DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join5(SWARM_HOME, "drone-channel.key");
2586
+ DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join6(SWARM_HOME, "drone-channel.key");
2482
2587
  DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
2483
- HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join5(SWARM_HOME, "overmind.heartbeat");
2588
+ HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join6(SWARM_HOME, "overmind.heartbeat");
2484
2589
  HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
2485
2590
  SWARM_REPO = process.env.SWARM_REPO || "";
2486
2591
  SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
2487
2592
  SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
2488
2593
  SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
2489
- REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join5(SWARM_HOME, "repo-map.json");
2594
+ REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join6(SWARM_HOME, "repo-map.json");
2490
2595
  SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
2491
2596
  SWARM_REPO_SCAN_TTL_MS = envNum("SWARM_REPO_SCAN_TTL_MS", 24 * 60 * 6e4);
2492
2597
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
@@ -2501,7 +2606,7 @@ var init_config = __esm({
2501
2606
  SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
2502
2607
  SWARM_AUTO_VERIFY = process.env.SWARM_AUTO_VERIFY !== "0";
2503
2608
  SWARM_MERGE_QUEUE = process.env.SWARM_MERGE_QUEUE !== "0";
2504
- REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join5(SWARM_HOME, "review-cutoff.json");
2609
+ REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join6(SWARM_HOME, "review-cutoff.json");
2505
2610
  SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
2506
2611
  SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
2507
2612
  WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
@@ -2541,7 +2646,7 @@ var init_quietExec = __esm({
2541
2646
 
2542
2647
  // src/resources.ts
2543
2648
  import os from "os";
2544
- import { readFileSync as readFileSync3, statfsSync } from "fs";
2649
+ import { readFileSync as readFileSync4, statfsSync } from "fs";
2545
2650
  function isTransientResourceError(e) {
2546
2651
  const code = e?.code;
2547
2652
  return code === "ENFILE" || code === "EMFILE" || code === "EAGAIN";
@@ -2593,7 +2698,7 @@ function fileDescriptorLoadRatio() {
2593
2698
  }
2594
2699
  if (process.platform === "linux") {
2595
2700
  try {
2596
- const [allocated, , max] = readFileSync3("/proc/sys/fs/file-nr", "utf8").trim().split(/\s+/).map(Number);
2701
+ const [allocated, , max] = readFileSync4("/proc/sys/fs/file-nr", "utf8").trim().split(/\s+/).map(Number);
2597
2702
  if (Number.isFinite(allocated ?? NaN) && Number.isFinite(max ?? NaN) && max) return allocated / max;
2598
2703
  } catch {
2599
2704
  }
@@ -2817,11 +2922,11 @@ var init_zoneNotes = __esm({
2817
2922
  });
2818
2923
 
2819
2924
  // src/agentToken.ts
2820
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync } from "fs";
2821
- import { join as join6 } from "path";
2925
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, unlinkSync } from "fs";
2926
+ import { join as join7 } from "path";
2822
2927
  function loadAgentToken() {
2823
2928
  try {
2824
- const s = JSON.parse(readFileSync4(STORE_PATH, "utf8"));
2929
+ const s = JSON.parse(readFileSync5(STORE_PATH, "utf8"));
2825
2930
  return s.token ? s : null;
2826
2931
  } catch {
2827
2932
  return null;
@@ -2899,7 +3004,7 @@ var init_agentToken = __esm({
2899
3004
  "src/agentToken.ts"() {
2900
3005
  "use strict";
2901
3006
  init_config();
2902
- STORE_PATH = join6(SWARM_HOME, "agent.json");
3007
+ STORE_PATH = join7(SWARM_HOME, "agent.json");
2903
3008
  base = (api) => api.replace(/\/$/, "");
2904
3009
  TOKEN_PROBE_TIMEOUT_MS = 8e3;
2905
3010
  }
@@ -3086,7 +3191,7 @@ var init_procList = __esm({
3086
3191
 
3087
3192
  // src/procCleanup.ts
3088
3193
  function sleep(ms) {
3089
- return new Promise((resolve4) => setTimeout(resolve4, ms));
3194
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
3090
3195
  }
3091
3196
  function parseLsofCwd(output) {
3092
3197
  const rows = [];
@@ -3285,7 +3390,7 @@ var init_package = __esm({
3285
3390
  "package.json"() {
3286
3391
  package_default = {
3287
3392
  name: "@taskless-app/swarm",
3288
- version: "0.6.38",
3393
+ version: "0.6.40",
3289
3394
  type: "module",
3290
3395
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
3291
3396
  keywords: [
@@ -3336,7 +3441,7 @@ var init_package = __esm({
3336
3441
  execa: "^10.0.0",
3337
3442
  zod: "3.25.76"
3338
3443
  },
3339
- gitHead: "b605569670600b022ae6eec5d042c385cc4c77dd"
3444
+ gitHead: "b58ef18678e8c0e03e0602a40bf76d6f0a6975c8"
3340
3445
  };
3341
3446
  }
3342
3447
  });
@@ -3502,12 +3607,12 @@ var init_netTimeout = __esm({
3502
3607
 
3503
3608
  // src/droneChannel.ts
3504
3609
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
3505
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
3610
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
3506
3611
  import { dirname as dirname3 } from "path";
3507
3612
  import { serve } from "@hono/node-server";
3508
3613
  function loadChannelSecret(path) {
3509
3614
  try {
3510
- const s = readFileSync8(path, "utf8").trim();
3615
+ const s = readFileSync9(path, "utf8").trim();
3511
3616
  if (s.length >= 32) return s;
3512
3617
  } catch {
3513
3618
  }
@@ -3590,8 +3695,8 @@ var init_authState = __esm({
3590
3695
 
3591
3696
  // src/oauth.ts
3592
3697
  import { createHash, randomBytes as randomBytes2 } from "crypto";
3593
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3 } from "fs";
3594
- import { join as join11 } from "path";
3698
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3 } from "fs";
3699
+ import { join as join12 } from "path";
3595
3700
  function makePkce() {
3596
3701
  const verifier = randomBytes2(48).toString("base64url");
3597
3702
  const challenge = createHash("sha256").update(verifier).digest("base64url");
@@ -3675,7 +3780,7 @@ function toStore(clientId, t, now) {
3675
3780
  }
3676
3781
  function loadStore() {
3677
3782
  try {
3678
- return JSON.parse(readFileSync9(STORE_PATH2, "utf8"));
3783
+ return JSON.parse(readFileSync10(STORE_PATH2, "utf8"));
3679
3784
  } catch {
3680
3785
  return null;
3681
3786
  }
@@ -3691,7 +3796,7 @@ function clearStore() {
3691
3796
  }
3692
3797
  function loadClientId() {
3693
3798
  try {
3694
- return JSON.parse(readFileSync9(CLIENT_PATH, "utf8")).clientId;
3799
+ return JSON.parse(readFileSync10(CLIENT_PATH, "utf8")).clientId;
3695
3800
  } catch {
3696
3801
  return null;
3697
3802
  }
@@ -3726,16 +3831,16 @@ var init_oauth = __esm({
3726
3831
  AUTHORIZE = "/api/oauth/authorize";
3727
3832
  TOKEN = "/api/oauth/token";
3728
3833
  SCOPE = "openid profile email taskless:full";
3729
- STORE_PATH2 = join11(SWARM_HOME, "oauth.json");
3730
- CLIENT_PATH = join11(SWARM_HOME, "oauth-client.json");
3834
+ STORE_PATH2 = join12(SWARM_HOME, "oauth.json");
3835
+ CLIENT_PATH = join12(SWARM_HOME, "oauth-client.json");
3731
3836
  base2 = (api) => api.replace(/\/$/, "");
3732
3837
  }
3733
3838
  });
3734
3839
 
3735
3840
  // src/git.ts
3736
- import { existsSync as existsSync10, readdirSync as readdirSync8, statSync as statSync8 } from "fs";
3737
- import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
3738
- import { isAbsolute, join as join14, sep as sep2 } from "path";
3841
+ import { existsSync as existsSync10, readdirSync as readdirSync9, realpathSync as realpathSync2, statSync as statSync9 } from "fs";
3842
+ import { appendFile, chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
3843
+ import { isAbsolute, join as join15, resolve as resolve2, sep as sep2 } from "path";
3739
3844
  import { $ as $5 } from "execa";
3740
3845
  function gitNet(repo, args, timeoutMs) {
3741
3846
  return withHardTimeout(
@@ -3748,9 +3853,75 @@ function gitNet(repo, args, timeoutMs) {
3748
3853
  async function reclaimStaleWorktree(repo, worktree) {
3749
3854
  await detachWorktree(repo, worktree);
3750
3855
  }
3856
+ async function inspectWorktreePath(path) {
3857
+ if (!existsSync10(path)) return "free";
3858
+ try {
3859
+ if (readdirSync9(path).length === 0) return "free";
3860
+ } catch {
3861
+ return "orphan";
3862
+ }
3863
+ try {
3864
+ if (statSync9(join15(path, ".git")).isDirectory()) return "main-checkout";
3865
+ } catch {
3866
+ }
3867
+ const top = await $5({ reject: false })`git -C ${path} rev-parse --show-toplevel`;
3868
+ return top.exitCode === 0 && samePath(top.stdout.trim(), path) ? "worktree" : "orphan";
3869
+ }
3870
+ function samePath(a, b) {
3871
+ const norm = (p) => {
3872
+ try {
3873
+ return realpathSync2(p);
3874
+ } catch {
3875
+ return resolve2(p);
3876
+ }
3877
+ };
3878
+ const [x, y] = [norm(a), norm(b)];
3879
+ return process.platform === "win32" ? posix(x).toLowerCase() === posix(y).toLowerCase() : x === y;
3880
+ }
3881
+ async function clearWorktreePath(repo, path, branch, opts = {}) {
3882
+ const holder = await inspectWorktreePath(path);
3883
+ if (holder === "free") return;
3884
+ if (holder === "main-checkout") {
3885
+ throw new WorktreeBusyError(
3886
+ branch,
3887
+ path,
3888
+ `\u043F\u043E \u043F\u0443\u0442\u0438 worktree ${path} \u043B\u0435\u0436\u0438\u0442 \u041E\u0421\u041D\u041E\u0412\u041D\u041E\u0419 \u0447\u0435\u043A\u0430\u0443\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (\`.git\` \u2014 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u0430 \u043D\u0435 \u0444\u0430\u0439\u043B) \u2014 \u044F \u0435\u0433\u043E \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E. \u041F\u043E\u0445\u043E\u0436\u0435, \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree'\u0435\u0432 \u0440\u043E\u044F \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D \u043D\u0430 \u0440\u0430\u0431\u043E\u0447\u0443\u044E \u043A\u043E\u043F\u0438\u044E \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430; \u043F\u043E\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0442\u044C, \u0441\u043F\u0430\u0432\u043D \u0442\u0443\u0434\u0430 \u0437\u0430\u0441\u0435\u043B\u044F\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0435\u0442.`
3889
+ );
3890
+ }
3891
+ if (holder === "worktree") {
3892
+ if (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(path) : true) throw new WorktreeBusyError(branch, path);
3893
+ if (await worktreeDirty(path)) {
3894
+ throw new WorktreeBusyError(
3895
+ branch,
3896
+ path,
3897
+ `worktree ${path} \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B, \u043D\u043E \u0432\u043D\u0443\u0442\u0440\u0438 \u0435\u0441\u0442\u044C \u043D\u0435\u0441\u0434\u0430\u043D\u043D\u0430\u044F \u0440\u0430\u0431\u043E\u0442\u0430 (\u043D\u0435\u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0447\u0435\u043D\u043D\u043E\u0435 \u0438\u043B\u0438 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435) \u2014 \u043D\u0435 \u0441\u043D\u043E\u0448\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435 \u0441\u0430\u043C\u0438 (\`git -C ${path} status --untracked-files=all\`) \u0438 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u043D\u0435 \u0436\u0430\u043B\u043A\u043E.`
3898
+ );
3899
+ }
3900
+ await detachWorktree(repo, path);
3901
+ opts.onStaleReclaim?.(path);
3902
+ }
3903
+ if (!existsSync10(path)) return;
3904
+ await parkOrphanDir(repo, path, branch);
3905
+ }
3906
+ async function parkOrphanDir(repo, path, branch) {
3907
+ const parked = `${path}.orphan-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
3908
+ try {
3909
+ await rename(path, parked);
3910
+ } catch (e) {
3911
+ throw new WorktreeBusyError(
3912
+ branch,
3913
+ path,
3914
+ `\u043F\u0443\u0442\u044C ${path} \u0437\u0430\u043D\u044F\u0442 \u043F\u043E\u0441\u0442\u043E\u0440\u043E\u043D\u043D\u0438\u043C \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u043E\u043C, \u0438 \u043E\u0442\u043E\u0434\u0432\u0438\u043D\u0443\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${String(e)}) \u2014 \u0441\u043F\u0430\u0432\u043D ${branch} \u0442\u0443\u0434\u0430 \u043D\u0435 \u0432\u0441\u0442\u0430\u043D\u0435\u0442. \u0423\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u0441\u0434\u0435\u043B\u0430\u0439\u0442\u0435 \`git -C ${repo} worktree prune\`.`
3915
+ );
3916
+ }
3917
+ console.error(
3918
+ `[git] \u043F\u043E \u043F\u0443\u0442\u0438 worktree ${path} \u043B\u0435\u0436\u0430\u043B \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 git \u0441\u0432\u043E\u0438\u043C \u0440\u0430\u0431\u043E\u0447\u0438\u043C \u0434\u0435\u0440\u0435\u0432\u043E\u043C \u043D\u0435 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u2014 \u043E\u0442\u043E\u0434\u0432\u0438\u043D\u0443\u043B \u0432 ${parked}, \u0447\u0442\u043E\u0431\u044B \u0441\u043F\u0430\u0432\u043D ${branch} \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435, \u0447\u0442\u043E \u0432\u043D\u0443\u0442\u0440\u0438, \u0438 \u0443\u0434\u0430\u043B\u0438\u0442\u0435, \u0435\u0441\u043B\u0438 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`
3919
+ );
3920
+ await $5({ reject: false })`git -C ${repo} worktree prune`;
3921
+ }
3751
3922
  function isLinkedWorktree(dir) {
3752
3923
  try {
3753
- return statSync8(join14(dir, ".git")).isFile();
3924
+ return statSync9(join15(dir, ".git")).isFile();
3754
3925
  } catch {
3755
3926
  return false;
3756
3927
  }
@@ -3820,6 +3991,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
3820
3991
  const originSha = await revParse(repo, `refs/remotes/origin/${branch}`);
3821
3992
  if (!localSha && !originSha) {
3822
3993
  const freshBase = opts.base ?? await freshWorktreeBase(repo);
3994
+ await clearWorktreePath(repo, path, branch, opts);
3823
3995
  await $5`git -C ${repo} worktree add -b ${branch} ${path} ${freshBase}`;
3824
3996
  return { reused: false };
3825
3997
  }
@@ -3832,6 +4004,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
3832
4004
  await reclaimStaleWorktree(repo, busyAt);
3833
4005
  opts.onStaleReclaim?.(busyAt);
3834
4006
  }
4007
+ await clearWorktreePath(repo, path, branch, opts);
3835
4008
  if (originSha) {
3836
4009
  await $5`git -C ${repo} worktree add -B ${branch} ${path} ${`origin/${branch}`}`;
3837
4010
  return { reused: true, from: "origin", sha: originSha };
@@ -3855,8 +4028,8 @@ async function ensureGitExcluded(worktree, pattern) {
3855
4028
  try {
3856
4029
  const commonDir = await gitPath(worktree, "--git-common-dir");
3857
4030
  if (!commonDir) return false;
3858
- const infoDir = join14(commonDir, "info");
3859
- const excludeFile = join14(infoDir, "exclude");
4031
+ const infoDir = join15(commonDir, "info");
4032
+ const excludeFile = join15(infoDir, "exclude");
3860
4033
  let current = "";
3861
4034
  try {
3862
4035
  current = await readFile(excludeFile, "utf8");
@@ -3876,7 +4049,7 @@ async function gitPath(worktree, flag) {
3876
4049
  const out = await $5({ reject: false })`git -C ${worktree} rev-parse ${flag}`;
3877
4050
  const raw = out.stdout.trim();
3878
4051
  if (out.exitCode !== 0 || !raw) return null;
3879
- return isAbsolute(raw) ? raw : join14(worktree, raw);
4052
+ return isAbsolute(raw) ? raw : join15(worktree, raw);
3880
4053
  }
3881
4054
  async function ensureWorktreeHygiene(worktree) {
3882
4055
  for (const pattern of SWARM_EXCLUDE_PATTERNS) await ensureGitExcluded(worktree, pattern);
@@ -3886,12 +4059,12 @@ async function installGuardHooks(worktree) {
3886
4059
  try {
3887
4060
  const gitDir = await gitPath(worktree, "--git-dir");
3888
4061
  if (!gitDir) return false;
3889
- const hooksDir = join14(gitDir, "swarm-hooks");
4062
+ const hooksDir = join15(gitDir, "swarm-hooks");
3890
4063
  const prev = await previousHooksPath(worktree);
3891
4064
  await mkdir(hooksDir, { recursive: true });
3892
4065
  for (const hook of GUARD_HOOKS) {
3893
- await writeFile(join14(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
3894
- await chmod(join14(hooksDir, hook), 493);
4066
+ await writeFile(join15(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
4067
+ await chmod(join15(hooksDir, hook), 493);
3895
4068
  }
3896
4069
  await $5`git -C ${worktree} config extensions.worktreeConfig true`;
3897
4070
  await $5`git -C ${worktree} config --worktree core.hooksPath ${posix(hooksDir)}`;
@@ -3910,7 +4083,7 @@ async function previousHooksPath(worktree) {
3910
4083
  if (out.exitCode === 0 && value) return value;
3911
4084
  }
3912
4085
  const commonDir = await gitPath(worktree, "--git-common-dir");
3913
- return commonDir ? join14(commonDir, "hooks") : null;
4086
+ return commonDir ? join15(commonDir, "hooks") : null;
3914
4087
  }
3915
4088
  async function removeWorktree(repo, path) {
3916
4089
  const branch = (await $5({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
@@ -3952,8 +4125,14 @@ var init_git = __esm({
3952
4125
  init_netTimeout();
3953
4126
  init_swarmFiles();
3954
4127
  WorktreeBusyError = class extends Error {
3955
- constructor(branch, worktreePath) {
3956
- super(`worktree \u0437\u0430\u043D\u044F\u0442: ${worktreePath} (\u0432\u0435\u0442\u043A\u0430 ${branch}) \u2014 \u043F\u0440\u0438\u0431\u0435\u0440\u0438 worktree \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438 \u043F\u0440\u043E\u0433\u043E\u043D (swarm cancel ${branch})`);
4128
+ /**
4129
+ * `why` (TASWM-553) — ПОЧЕМУ путь не отдали, если причина не «его держит живой прогон»:
4130
+ * основной чекаут по пути worktree, каталог с несданной работой, каталог, который не
4131
+ * получилось убрать. Дефолтная формулировка («прибери worktree или отмени прогон»)
4132
+ * человеку в таких случаях не помогает — она отправляет его не туда.
4133
+ */
4134
+ constructor(branch, worktreePath, why) {
4135
+ super(why ?? `worktree \u0437\u0430\u043D\u044F\u0442: ${worktreePath} (\u0432\u0435\u0442\u043A\u0430 ${branch}) \u2014 \u043F\u0440\u0438\u0431\u0435\u0440\u0438 worktree \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438 \u043F\u0440\u043E\u0433\u043E\u043D (swarm cancel ${branch})`);
3957
4136
  this.branch = branch;
3958
4137
  this.worktreePath = worktreePath;
3959
4138
  this.name = "WorktreeBusyError";
@@ -3967,7 +4146,7 @@ var init_git = __esm({
3967
4146
 
3968
4147
  // src/attachments.ts
3969
4148
  import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3970
- import { join as join15 } from "path";
4149
+ import { join as join16 } from "path";
3971
4150
  var ATTACHMENTS_DIR, ATTACH_MAX_TOTAL_BYTES, ATTACH_MAX_FILE_BYTES, IMAGE_EXT, DOC_EXT, TEXT_EXT, KNOWN_EXT;
3972
4151
  var init_attachments = __esm({
3973
4152
  "src/attachments.ts"() {
@@ -3986,7 +4165,7 @@ var init_attachments = __esm({
3986
4165
 
3987
4166
  // src/shotUpload.ts
3988
4167
  import { readFile as fsReadFile } from "fs/promises";
3989
- import { basename as basename5, isAbsolute as isAbsolute2, join as join16 } from "path";
4168
+ import { basename as basename5, isAbsolute as isAbsolute2, join as join17 } from "path";
3990
4169
  var SHOT_MAX_FILE_BYTES;
3991
4170
  var init_shotUpload = __esm({
3992
4171
  "src/shotUpload.ts"() {
@@ -3996,11 +4175,11 @@ var init_shotUpload = __esm({
3996
4175
  });
3997
4176
 
3998
4177
  // src/workspace.ts
3999
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync4 } from "fs";
4000
- import { join as join17 } from "path";
4178
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync4 } from "fs";
4179
+ import { join as join18 } from "path";
4001
4180
  function loadWorkspace() {
4002
4181
  try {
4003
- const w = JSON.parse(readFileSync10(WORKSPACE_PATH, "utf8"));
4182
+ const w = JSON.parse(readFileSync11(WORKSPACE_PATH, "utf8"));
4004
4183
  return w && typeof w.id === "string" && w.id ? w : null;
4005
4184
  } catch {
4006
4185
  return null;
@@ -4023,7 +4202,7 @@ var init_workspace = __esm({
4023
4202
  "src/workspace.ts"() {
4024
4203
  "use strict";
4025
4204
  init_config();
4026
- WORKSPACE_PATH = join17(SWARM_HOME, "workspace.json");
4205
+ WORKSPACE_PATH = join18(SWARM_HOME, "workspace.json");
4027
4206
  }
4028
4207
  });
4029
4208
 
@@ -4248,14 +4427,14 @@ function createNetworkSettleTracker(target) {
4248
4427
  });
4249
4428
  target.on("requestfinished", onDone);
4250
4429
  target.on("requestfailed", onDone);
4251
- const settleOnce = (quietMs, budgetMs) => new Promise((resolve4) => {
4430
+ const settleOnce = (quietMs, budgetMs) => new Promise((resolve5) => {
4252
4431
  let quietTimer;
4253
4432
  const hardTimer = setTimeout(finish, Math.max(0, budgetMs));
4254
4433
  function finish() {
4255
4434
  clearTimeout(quietTimer);
4256
4435
  clearTimeout(hardTimer);
4257
4436
  onChange.delete(check);
4258
- resolve4();
4437
+ resolve5();
4259
4438
  }
4260
4439
  function check() {
4261
4440
  clearTimeout(quietTimer);
@@ -4441,14 +4620,14 @@ var init_daemonLink = __esm({
4441
4620
  });
4442
4621
 
4443
4622
  // src/pauseState.ts
4444
- import { existsSync as existsSync11, readFileSync as readFileSync11, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync10 } from "fs";
4445
- import { join as join18 } from "path";
4623
+ import { existsSync as existsSync11, readFileSync as readFileSync12, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync10 } from "fs";
4624
+ import { join as join19 } from "path";
4446
4625
  function pauseStatePath(home) {
4447
- return join18(home, "pause.json");
4626
+ return join19(home, "pause.json");
4448
4627
  }
4449
4628
  function loadPauseState(home, io = {}) {
4450
4629
  const exists = io.exists ?? existsSync11;
4451
- const readFile2 = io.readFile ?? ((p) => readFileSync11(p, "utf8"));
4630
+ const readFile2 = io.readFile ?? ((p) => readFileSync12(p, "utf8"));
4452
4631
  const path = pauseStatePath(home);
4453
4632
  if (!exists(path)) return ACTIVE;
4454
4633
  try {
@@ -4509,10 +4688,10 @@ var init_pauseState = __esm({
4509
4688
  });
4510
4689
 
4511
4690
  // src/heartbeat.ts
4512
- import { readFileSync as readFileSync12, renameSync as renameSync6, writeFileSync as writeFileSync11 } from "fs";
4691
+ import { readFileSync as readFileSync13, renameSync as renameSync6, writeFileSync as writeFileSync11 } from "fs";
4513
4692
  function readHeartbeat(path) {
4514
4693
  try {
4515
- const ms = Number(readFileSync12(path, "utf8").trim());
4694
+ const ms = Number(readFileSync13(path, "utf8").trim());
4516
4695
  return Number.isFinite(ms) ? ms : null;
4517
4696
  } catch {
4518
4697
  return null;
@@ -4532,7 +4711,7 @@ __export(db_exports, {
4532
4711
  import { createRequire as createRequire2 } from "module";
4533
4712
  import { randomUUID as randomUUID3 } from "crypto";
4534
4713
  import { existsSync as existsSync12 } from "fs";
4535
- import { resolve as resolve2 } from "path";
4714
+ import { resolve as resolve3 } from "path";
4536
4715
  function rowToQuestion(row) {
4537
4716
  return {
4538
4717
  id: row.id,
@@ -4574,6 +4753,7 @@ function rowToRun(row) {
4574
4753
  tokens_in: row.tokens_in ?? 0,
4575
4754
  tokens_out: row.tokens_out ?? 0,
4576
4755
  duration_ms: row.duration_ms ?? 0,
4756
+ cost_unmeasured: row.cost_unmeasured ?? 0,
4577
4757
  pr_url: row.pr_url ?? null,
4578
4758
  // TASWM-506: файлы, занятые прогоном с открытым PR — JSON-массив, как depends_on.
4579
4759
  changed_paths: row.changed_paths ? JSON.parse(row.changed_paths) : [],
@@ -4654,6 +4834,7 @@ CREATE TABLE IF NOT EXISTS runs (
4654
4834
  tokens_in INTEGER NOT NULL DEFAULT 0,
4655
4835
  tokens_out INTEGER NOT NULL DEFAULT 0,
4656
4836
  duration_ms INTEGER NOT NULL DEFAULT 0,
4837
+ cost_unmeasured INTEGER NOT NULL DEFAULT 0,
4657
4838
  pr_url TEXT,
4658
4839
  superseded_by INTEGER,
4659
4840
  decision TEXT,
@@ -4769,6 +4950,10 @@ CREATE TABLE IF NOT EXISTS imports (
4769
4950
  "ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
4770
4951
  "ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
4771
4952
  "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0",
4953
+ // TASWM-548: сколько запусков прогона остались без снятой цены — отличает «бесплатно»
4954
+ // от «не измерено». Прогоны до фичи получают 0: их нули мы перепроверить уже не можем,
4955
+ // а задним числом объявлять их неизмеренными значило бы утверждать непроверенное.
4956
+ "ALTER TABLE runs ADD COLUMN cost_unmeasured INTEGER NOT NULL DEFAULT 0",
4772
4957
  // TASWM-395: усилие рассуждения прогона (`claude --effort`). Nullable без DEFAULT —
4773
4958
  // NULL это и все прогоны до фичи, и прогоны на дефолте харнесса.
4774
4959
  "ALTER TABLE runs ADD COLUMN effort TEXT",
@@ -5155,7 +5340,7 @@ CREATE TABLE IF NOT EXISTS imports (
5155
5340
  * транзакции: прервётся — маркер не встанет, повтор безопасен.
5156
5341
  */
5157
5342
  importDb(sourcePath, repo) {
5158
- const abs = resolve2(sourcePath);
5343
+ const abs = resolve3(sourcePath);
5159
5344
  const repoN = normalizeRepo(repo);
5160
5345
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
5161
5346
  if (already) return { imported: false, runs: already.run_count };
@@ -5486,8 +5671,8 @@ CREATE TABLE IF NOT EXISTS imports (
5486
5671
  });
5487
5672
 
5488
5673
  // src/repoScan.ts
5489
- import { readdirSync as readdirSync9, readFileSync as readFileSync13, statSync as statSync9 } from "fs";
5490
- import { join as join19 } from "path";
5674
+ import { readdirSync as readdirSync10, readFileSync as readFileSync14, statSync as statSync10 } from "fs";
5675
+ import { join as join20 } from "path";
5491
5676
  var init_repoScan = __esm({
5492
5677
  "src/repoScan.ts"() {
5493
5678
  "use strict";
@@ -5496,9 +5681,9 @@ var init_repoScan = __esm({
5496
5681
  });
5497
5682
 
5498
5683
  // src/repoDiscovery.ts
5499
- import { homedir as homedir5 } from "os";
5500
- import { readFileSync as readFileSync14, renameSync as renameSync7, writeFileSync as writeFileSync12 } from "fs";
5501
- import { isAbsolute as isAbsolute3, join as join20, resolve as resolve3 } from "path";
5684
+ import { homedir as homedir6 } from "os";
5685
+ import { readFileSync as readFileSync15, renameSync as renameSync7, writeFileSync as writeFileSync12 } from "fs";
5686
+ import { isAbsolute as isAbsolute3, join as join21, resolve as resolve4 } from "path";
5502
5687
  function repoMapLines(rows) {
5503
5688
  if (!rows?.length) return [];
5504
5689
  const missing = rows.filter((r) => !r.path).length;
@@ -5513,7 +5698,7 @@ function repoMapLines(rows) {
5513
5698
  }
5514
5699
  function loadRepoMapCache(path) {
5515
5700
  try {
5516
- const raw = JSON.parse(readFileSync14(path, "utf8"));
5701
+ const raw = JSON.parse(readFileSync15(path, "utf8"));
5517
5702
  if (!Array.isArray(raw?.entries)) return { scannedAt: null, entries: [] };
5518
5703
  return {
5519
5704
  scannedAt: typeof raw.scannedAt === "string" && raw.scannedAt ? raw.scannedAt : null,
@@ -5882,11 +6067,11 @@ __export(statusCli_exports, {
5882
6067
  probeLink: () => probeLink,
5883
6068
  runStatusCli: () => runStatusCli
5884
6069
  });
5885
- import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
6070
+ import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
5886
6071
  function outboxPending(path) {
5887
6072
  try {
5888
6073
  if (!existsSync13(path)) return 0;
5889
- return readFileSync15(path, "utf8").split("\n").filter((l) => l.trim()).length;
6074
+ return readFileSync16(path, "utf8").split("\n").filter((l) => l.trim()).length;
5890
6075
  } catch {
5891
6076
  return null;
5892
6077
  }
@@ -6093,11 +6278,11 @@ var init_prodMergeGate = __esm({
6093
6278
  });
6094
6279
 
6095
6280
  // src/platform.ts
6096
- import { existsSync as existsSync14, readFileSync as readFileSync16 } from "fs";
6097
- import { delimiter, join as join21 } from "path";
6281
+ import { existsSync as existsSync14, readFileSync as readFileSync17 } from "fs";
6282
+ import { delimiter, join as join22 } from "path";
6098
6283
  function safeReadText(path) {
6099
6284
  try {
6100
- return readFileSync16(path, "utf8");
6285
+ return readFileSync17(path, "utf8");
6101
6286
  } catch {
6102
6287
  return null;
6103
6288
  }
@@ -6108,7 +6293,7 @@ function resolveCommandPath(name, env = process.env, platform = process.platform
6108
6293
  const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
6109
6294
  for (const dir of dirs) {
6110
6295
  for (const ext of exts) {
6111
- const candidate = join21(dir, name + ext);
6296
+ const candidate = join22(dir, name + ext);
6112
6297
  if (existsSync14(candidate)) return candidate;
6113
6298
  }
6114
6299
  }
@@ -6213,12 +6398,12 @@ var init_platform = __esm({
6213
6398
 
6214
6399
  // src/envGate.ts
6215
6400
  import { existsSync as existsSync15 } from "fs";
6216
- import { join as join22 } from "path";
6401
+ import { join as join23 } from "path";
6217
6402
  import { createConnection } from "net";
6218
6403
  import { execa as execa3 } from "execa";
6219
6404
  function detectRequirements(repo, io = {}) {
6220
6405
  const exists = io.exists ?? existsSync15;
6221
- const marker = COMPOSE_MARKERS.find((f) => exists(join22(repo, f)));
6406
+ const marker = COMPOSE_MARKERS.find((f) => exists(join23(repo, f)));
6222
6407
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
6223
6408
  }
6224
6409
  async function dockerAlive() {
@@ -6250,7 +6435,7 @@ var init_envGate = __esm({
6250
6435
  });
6251
6436
 
6252
6437
  // src/doctor.ts
6253
- import { isAbsolute as isAbsolute4, join as join23 } from "path";
6438
+ import { isAbsolute as isAbsolute4, join as join24 } from "path";
6254
6439
  function ok(id, title, detail) {
6255
6440
  return { id, title, status: "ok", detail, remedies: [], critical: false };
6256
6441
  }
@@ -6457,7 +6642,7 @@ function checkRepos(io, snap) {
6457
6642
  critical: true
6458
6643
  };
6459
6644
  }
6460
- if (env && !io.exists(join23(env, ".git"))) {
6645
+ if (env && !io.exists(join24(env, ".git"))) {
6461
6646
  return {
6462
6647
  id: "repos",
6463
6648
  title,
@@ -6467,7 +6652,7 @@ function checkRepos(io, snap) {
6467
6652
  critical: true
6468
6653
  };
6469
6654
  }
6470
- const broken = snap.paths.filter((p) => !io.exists(join23(p, ".git")));
6655
+ const broken = snap.paths.filter((p) => !io.exists(join24(p, ".git")));
6471
6656
  if (broken.length) {
6472
6657
  return {
6473
6658
  id: "repos",
@@ -6657,12 +6842,12 @@ var init_doctor = __esm({
6657
6842
  });
6658
6843
 
6659
6844
  // src/repoMemory.ts
6660
- import { readFileSync as readFileSync17, writeFileSync as writeFileSync13 } from "fs";
6661
- import { join as join24 } from "path";
6845
+ import { readFileSync as readFileSync18, writeFileSync as writeFileSync13 } from "fs";
6846
+ import { join as join25 } from "path";
6662
6847
  function loadRememberedRepos(home) {
6663
6848
  let raw;
6664
6849
  try {
6665
- raw = readFileSync17(join24(home, FILE), "utf8");
6850
+ raw = readFileSync18(join25(home, FILE), "utf8");
6666
6851
  } catch {
6667
6852
  return [];
6668
6853
  }
@@ -6981,8 +7166,8 @@ var init_updateCli = __esm({
6981
7166
 
6982
7167
  // src/cli.ts
6983
7168
  init_cliIo();
6984
- import { basename as basename6, dirname as dirname5, join as join25 } from "path";
6985
- import { existsSync as existsSync17, readFileSync as readFileSync18, writeFileSync as writeFileSync14, mkdirSync as mkdirSync10, readdirSync as readdirSync10, statSync as statSync10, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
7169
+ import { basename as basename6, dirname as dirname5, join as join26 } from "path";
7170
+ import { existsSync as existsSync17, readFileSync as readFileSync19, writeFileSync as writeFileSync14, mkdirSync as mkdirSync10, readdirSync as readdirSync11, statSync as statSync11, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
6986
7171
  import { setTimeout as sleep2 } from "timers/promises";
6987
7172
 
6988
7173
  // src/swarmClient.ts
@@ -6990,7 +7175,7 @@ init_config();
6990
7175
  init_resources();
6991
7176
  init_names2();
6992
7177
  init_storm();
6993
- import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
7178
+ import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
6994
7179
  import { dirname as dirname2 } from "path";
6995
7180
  import { randomUUID } from "crypto";
6996
7181
 
@@ -7022,19 +7207,19 @@ init_zoneNotes();
7022
7207
  init_toolFailure();
7023
7208
 
7024
7209
  // src/logTail.ts
7025
- import { closeSync, existsSync as existsSync5, openSync, readSync, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
7026
- import { join as join8 } from "path";
7210
+ import { closeSync, existsSync as existsSync5, openSync, readSync, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
7211
+ import { join as join9 } from "path";
7027
7212
 
7028
7213
  // src/runLogDir.ts
7029
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync as readdirSync4, renameSync as renameSync2, statSync as statSync3 } from "fs";
7030
- import { join as join7 } from "path";
7214
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readdirSync as readdirSync5, renameSync as renameSync2, statSync as statSync4 } from "fs";
7215
+ import { join as join8 } from "path";
7031
7216
  function runLogDirName(runLogKey) {
7032
7217
  const safe = runLogKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
7033
7218
  if (!safe) throw new Error(`\u043A\u043B\u044E\u0447 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435 \u0433\u043E\u0434\u0438\u0442\u0441\u044F \u0432 \u0438\u043C\u044F \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430 run-log'\u043E\u0432: ${JSON.stringify(runLogKey)}`);
7034
7219
  return /^\d+$/.test(safe) ? `k-${safe}` : safe;
7035
7220
  }
7036
7221
  function runLogDirPath(runLogsDir, runLogKey) {
7037
- return join7(runLogsDir, runLogDirName(runLogKey));
7222
+ return join8(runLogsDir, runLogDirName(runLogKey));
7038
7223
  }
7039
7224
  function findRunLogDir(runLogsDir, keys) {
7040
7225
  for (const key of keys) {
@@ -7246,13 +7431,13 @@ var SwarmClient = class _SwarmClient {
7246
7431
  recover() {
7247
7432
  if (existsSync6(this.mapPath)) {
7248
7433
  try {
7249
- const obj = JSON.parse(readFileSync5(this.mapPath, "utf8"));
7434
+ const obj = JSON.parse(readFileSync6(this.mapPath, "utf8"));
7250
7435
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
7251
7436
  } catch {
7252
7437
  }
7253
7438
  }
7254
7439
  if (existsSync6(this.outboxPath)) {
7255
- const txt = readFileSync5(this.outboxPath, "utf8");
7440
+ const txt = readFileSync6(this.outboxPath, "utf8");
7256
7441
  for (const line of txt.split("\n")) {
7257
7442
  const s = line.trim();
7258
7443
  if (!s) continue;
@@ -7275,7 +7460,7 @@ var SwarmClient = class _SwarmClient {
7275
7460
  if (!existsSync6(this.stormPath)) return;
7276
7461
  let txt;
7277
7462
  try {
7278
- txt = readFileSync5(this.stormPath, "utf8");
7463
+ txt = readFileSync6(this.stormPath, "utf8");
7279
7464
  } catch {
7280
7465
  return;
7281
7466
  }
@@ -7359,6 +7544,7 @@ var SwarmClient = class _SwarmClient {
7359
7544
  if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
7360
7545
  if ("tokens_out" in patch && patch.tokens_out !== void 0) out.tokensOut = patch.tokens_out;
7361
7546
  if ("duration_ms" in patch && patch.duration_ms !== void 0) out.durationMs = patch.duration_ms;
7547
+ if ("cost_unmeasured" in patch && patch.cost_unmeasured !== void 0) out.costUnmeasured = patch.cost_unmeasured;
7362
7548
  if ("respawns" in patch && patch.respawns !== void 0) out.respawns = patch.respawns;
7363
7549
  if ("pr_url" in patch) out.prUrl = patch.pr_url ?? null;
7364
7550
  if ("changed_paths" in patch && patch.changed_paths) out.changedPaths = patch.changed_paths;
@@ -7441,6 +7627,8 @@ var SwarmClient = class _SwarmClient {
7441
7627
  // копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
7442
7628
  tokens_out: 0,
7443
7629
  duration_ms: 0,
7630
+ cost_unmeasured: 0,
7631
+ // запусков без снятой цены (TASWM-548) — копится тем же учётом
7444
7632
  pr_url: null,
7445
7633
  changed_paths: [],
7446
7634
  // файлы прогона снимаются при открытии PR (TASWM-506)
@@ -9061,6 +9249,8 @@ var SwarmClient = class _SwarmClient {
9061
9249
  // токены и время работы (TASWM-353) — прод-поля, заполняются ниже
9062
9250
  tokens_out: 0,
9063
9251
  duration_ms: 0,
9252
+ cost_unmeasured: 0,
9253
+ // запусков без снятой цены (TASWM-548) — прод-поле, заполняется ниже
9064
9254
  pr_url: null,
9065
9255
  changed_paths: [],
9066
9256
  // занятые файлы (TASWM-506) — прод-поле, заполняется ниже
@@ -9142,6 +9332,7 @@ var SwarmClient = class _SwarmClient {
9142
9332
  local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
9143
9333
  local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
9144
9334
  local.duration_ms = typeof pr.durationMs === "number" ? pr.durationMs : 0;
9335
+ local.cost_unmeasured = typeof pr.costUnmeasured === "number" ? pr.costUnmeasured : 0;
9145
9336
  local.pr_url = pr.prUrl ?? null;
9146
9337
  local.changed_paths = Array.isArray(pr.changedPaths) ? pr.changedPaths : local.changed_paths ?? [];
9147
9338
  local.stop_reason = pr.stopReason ?? null;
@@ -9539,7 +9730,7 @@ init_config();
9539
9730
  import { $ as $6 } from "execa";
9540
9731
 
9541
9732
  // src/daemonLaunch.ts
9542
- import { openSync as openSync2, closeSync as closeSync2, readFileSync as readFileSync6 } from "fs";
9733
+ import { openSync as openSync2, closeSync as closeSync2, readFileSync as readFileSync7 } from "fs";
9543
9734
  import { spawn } from "child_process";
9544
9735
  var FOREGROUND_FLAGS = /* @__PURE__ */ new Set(["--foreground", "-f", "--attach"]);
9545
9736
  function parseStartMode(argv) {
@@ -9599,18 +9790,18 @@ function launchDetached(opts) {
9599
9790
  }
9600
9791
  function tailFile(path, lines) {
9601
9792
  try {
9602
- return tailText(readFileSync6(path, "utf8"), lines);
9793
+ return tailText(readFileSync7(path, "utf8"), lines);
9603
9794
  } catch {
9604
9795
  return "";
9605
9796
  }
9606
9797
  }
9607
9798
 
9608
9799
  // src/logRotate.ts
9609
- import { copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync5, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync7 } from "fs";
9800
+ import { copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync6, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync7 } from "fs";
9610
9801
  var realIo = {
9611
9802
  size: (path) => {
9612
9803
  try {
9613
- return statSync5(path).size;
9804
+ return statSync6(path).size;
9614
9805
  } catch {
9615
9806
  return null;
9616
9807
  }
@@ -9647,15 +9838,15 @@ init_repoRegistry();
9647
9838
 
9648
9839
  // src/hostIdentity.ts
9649
9840
  import { hostname } from "os";
9650
- import { existsSync as existsSync8, readFileSync as readFileSync7, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
9651
- import { join as join9 } from "path";
9841
+ import { existsSync as existsSync8, readFileSync as readFileSync8, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
9842
+ import { join as join10 } from "path";
9652
9843
  import { randomUUID as randomUUID2 } from "crypto";
9653
9844
  function hostIdentityPath(home) {
9654
- return join9(home, "host.json");
9845
+ return join10(home, "host.json");
9655
9846
  }
9656
9847
  function loadHostIdentity(deps) {
9657
9848
  const exists = deps.exists ?? existsSync8;
9658
- const readFile2 = deps.readFile ?? ((p) => readFileSync7(p, "utf8"));
9849
+ const readFile2 = deps.readFile ?? ((p) => readFileSync8(p, "utf8"));
9659
9850
  const writeFile3 = deps.writeFile ?? ((p, data) => {
9660
9851
  writeFileSync6(`${p}.tmp`, data);
9661
9852
  renameSync4(`${p}.tmp`, p);
@@ -9772,8 +9963,8 @@ import { basename as basename4 } from "path";
9772
9963
  import { serve as serve2 } from "@hono/node-server";
9773
9964
 
9774
9965
  // src/diff.ts
9775
- import { statSync as statSync6 } from "fs";
9776
- import { join as join10 } from "path";
9966
+ import { statSync as statSync7 } from "fs";
9967
+ import { join as join11 } from "path";
9777
9968
  import { $ as $3 } from "execa";
9778
9969
  var MAX_FILES = 200;
9779
9970
  var MAX_LINES_PER_FILE = 1500;
@@ -9916,7 +10107,7 @@ async function resolveBase(wt, baseBranch) {
9916
10107
  }
9917
10108
  function mtime(path) {
9918
10109
  try {
9919
- return statSync6(path).mtimeMs;
10110
+ return statSync7(path).mtimeMs;
9920
10111
  } catch {
9921
10112
  return 0;
9922
10113
  }
@@ -9941,7 +10132,7 @@ async function collectRunDiff(worktree, opts = {}) {
9941
10132
  files.push(pf);
9942
10133
  }
9943
10134
  }
9944
- files.sort((a, b) => mtime(join10(worktree, b.path)) - mtime(join10(worktree, a.path)));
10135
+ files.sort((a, b) => mtime(join11(worktree, b.path)) - mtime(join11(worktree, a.path)));
9945
10136
  const truncatedFiles = files.length > MAX_FILES;
9946
10137
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
9947
10138
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -11667,14 +11858,14 @@ function makeHandler(deps) {
11667
11858
  const ref = url.searchParams.get("pr") ?? "";
11668
11859
  if (!ref.trim()) return Response.json({ error: "\u043D\u0443\u0436\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 pr=<url|\u043D\u043E\u043C\u0435\u0440>" }, { status: 400 });
11669
11860
  const ctxRepo = url.searchParams.get("repo");
11670
- const resolve4 = deps.mergeGate ?? makeMergeGateResolver({
11861
+ const resolve5 = deps.mergeGate ?? makeMergeGateResolver({
11671
11862
  listRuns: () => deps.db.listRuns(),
11672
11863
  fallbackAutoReview: SWARM_AUTO_REVIEW,
11673
11864
  autoVerify: SWARM_AUTO_VERIFY,
11674
11865
  // TASWM-344
11675
11866
  ...defaultRepo ? { repoFallback: defaultRepo } : {}
11676
11867
  });
11677
- return Response.json(await resolve4(ref, ctxRepo));
11868
+ return Response.json(await resolve5(ref, ctxRepo));
11678
11869
  }
11679
11870
  if (p === "/resources") return Response.json(hostResourceSnapshot());
11680
11871
  if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
@@ -11832,15 +12023,15 @@ import { cancelRun, respawnRun, runDaemon, wakeRun, OVERMIND_ENTRY, listDronePro
11832
12023
 
11833
12024
  // src/launcher.ts
11834
12025
  import { spawn as spawn2 } from "child_process";
11835
- import { closeSync as closeSync3, existsSync as existsSync9, mkdirSync as mkdirSync9, openSync as openSync3, readdirSync as readdirSync7 } from "fs";
12026
+ import { closeSync as closeSync3, existsSync as existsSync9, mkdirSync as mkdirSync9, openSync as openSync3, readdirSync as readdirSync8 } from "fs";
11836
12027
  import { createRequire } from "module";
11837
- import { homedir as homedir4 } from "os";
11838
- import { dirname as dirname4, join as join13 } from "path";
12028
+ import { homedir as homedir5 } from "os";
12029
+ import { dirname as dirname4, join as join14 } from "path";
11839
12030
  import { fileURLToPath as fileURLToPath2 } from "url";
11840
12031
 
11841
12032
  // src/droneHome.ts
11842
- import { lstatSync, mkdirSync as mkdirSync8, readdirSync as readdirSync6, rmSync, statSync as statSync7, symlinkSync as symlinkSync2 } from "fs";
11843
- import { join as join12, relative, sep } from "path";
12033
+ import { lstatSync, mkdirSync as mkdirSync8, readdirSync as readdirSync7, rmSync, statSync as statSync8, symlinkSync as symlinkSync2 } from "fs";
12034
+ import { join as join13, relative, sep } from "path";
11844
12035
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
11845
12036
  function droneHomeDeny(env = process.env) {
11846
12037
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -11859,28 +12050,28 @@ function buildDroneHome(opts) {
11859
12050
  const deny = opts.deny ?? droneHomeDeny();
11860
12051
  const platform = opts.platform ?? process.platform;
11861
12052
  mkdirSync8(droneHome, { recursive: true, mode: 448 });
11862
- const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
12053
+ const wanted = new Set(mirroredEntries(readdirSync7(realHome), deny, platform));
11863
12054
  const rel = relative(realHome, droneHome);
11864
12055
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
11865
- for (const existing of readdirSync6(droneHome)) {
12056
+ for (const existing of readdirSync7(droneHome)) {
11866
12057
  if (wanted.has(existing)) continue;
11867
- const p = join12(droneHome, existing);
12058
+ const p = join13(droneHome, existing);
11868
12059
  try {
11869
12060
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
11870
12061
  } catch {
11871
12062
  }
11872
12063
  }
11873
12064
  for (const name of wanted) {
11874
- const link = join12(droneHome, name);
12065
+ const link = join13(droneHome, name);
11875
12066
  try {
11876
12067
  lstatSync(link);
11877
12068
  continue;
11878
12069
  } catch {
11879
12070
  }
11880
- const target = join12(realHome, name);
12071
+ const target = join13(realHome, name);
11881
12072
  let isDir;
11882
12073
  try {
11883
- isDir = statSync7(target).isDirectory();
12074
+ isDir = statSync8(target).isDirectory();
11884
12075
  } catch {
11885
12076
  continue;
11886
12077
  }
@@ -11898,7 +12089,7 @@ var REAL_PATH_ENV_PINS = [
11898
12089
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
11899
12090
  ];
11900
12091
  function realPathEnvPins(realHome) {
11901
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join12(realHome, ...segments)]));
12092
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join13(realHome, ...segments)]));
11902
12093
  }
11903
12094
  function dronePrivatePaths(realHome, swarmHome) {
11904
12095
  return [
@@ -12010,7 +12201,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
12010
12201
  const env = { ...base3 };
12011
12202
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
12012
12203
  if (isolate && !base3.TURBO_CACHE_DIR) {
12013
- env.TURBO_CACHE_DIR = join13(worktree, ".turbo", "cache");
12204
+ env.TURBO_CACHE_DIR = join14(worktree, ".turbo", "cache");
12014
12205
  }
12015
12206
  Object.assign(env, droneTestEnv(droneCap, base3));
12016
12207
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -12038,10 +12229,10 @@ var DAEMON_STATE_ENV = [
12038
12229
  "TASKLESS_TOKEN"
12039
12230
  ];
12040
12231
  function bundledRoleDir(kind) {
12041
- return join13(dirname4(fileURLToPath2(import.meta.url)), "..", "roles", kind);
12232
+ return join14(dirname4(fileURLToPath2(import.meta.url)), "..", "roles", kind);
12042
12233
  }
12043
12234
  function roleDir(worktree, kind) {
12044
- return join13(worktree, ".claude", "roles", kind);
12235
+ return join14(worktree, ".claude", "roles", kind);
12045
12236
  }
12046
12237
  function roleDirs(worktree, kind) {
12047
12238
  return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync9(dir));
@@ -12052,7 +12243,7 @@ function nextRunLogPath(runLogsDir, runLogKeys) {
12052
12243
  mkdirSync9(dir, { recursive: true });
12053
12244
  let files = [];
12054
12245
  try {
12055
- files = readdirSync7(dir);
12246
+ files = readdirSync8(dir);
12056
12247
  } catch {
12057
12248
  }
12058
12249
  let next = 0;
@@ -12060,7 +12251,7 @@ function nextRunLogPath(runLogsDir, runLogKeys) {
12060
12251
  const m = /^(\d+)\.json/.exec(f);
12061
12252
  if (m) next = Math.max(next, Number(m[1]) + 1);
12062
12253
  }
12063
- return join13(dir, `${next}.json`);
12254
+ return join14(dir, `${next}.json`);
12064
12255
  }
12065
12256
  function openNextRunLog(runLogsDir, runLogKeys) {
12066
12257
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -12141,11 +12332,11 @@ function makeLauncher(cfg) {
12141
12332
  } catch (e) {
12142
12333
  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)}`);
12143
12334
  }
12144
- const realHome = cfg.droneIsolation?.realHome ?? homedir4();
12335
+ const realHome = cfg.droneIsolation?.realHome ?? homedir5();
12145
12336
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
12146
- if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync9(join13(cfg.droneIsolation.swarmHome, "token"))) {
12337
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync9(join14(cfg.droneIsolation.swarmHome, "token"))) {
12147
12338
  console.error(
12148
- `[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 ${join13(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.`
12339
+ `[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 ${join14(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.`
12149
12340
  );
12150
12341
  }
12151
12342
  const executor = cfg.executor ?? resolveExecutor();
@@ -12561,10 +12752,10 @@ async function restoreDaemon(d, loginOk) {
12561
12752
  d.err("\u0434\u0435\u043C\u043E\u043D \u043E\u0431\u0440\u0430\u0442\u043D\u043E \u041D\u0415 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F (\u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u0432\u044B\u0448\u0435) \u2014 \u0440\u043E\u0439 \u0441\u0442\u043E\u0438\u0442. \u041F\u043E\u0434\u043D\u0438\u043C\u0438 \u0432\u0440\u0443\u0447\u043D\u0443\u044E: swarm start");
12562
12753
  }
12563
12754
  function isPortFree(port, host = "127.0.0.1") {
12564
- return new Promise((resolve4) => {
12755
+ return new Promise((resolve5) => {
12565
12756
  const probe = createServer();
12566
- probe.once("error", () => resolve4(false));
12567
- probe.listen(port, host, () => probe.close(() => resolve4(true)));
12757
+ probe.once("error", () => resolve5(false));
12758
+ probe.listen(port, host, () => probe.close(() => resolve5(true)));
12568
12759
  });
12569
12760
  }
12570
12761
  async function chooseWorkspace(d) {
@@ -13051,12 +13242,12 @@ async function runDaemonLogs(argv) {
13051
13242
  const head = tailFile(DAEMON_LOG_PATH, lines);
13052
13243
  if (head) console.log(head);
13053
13244
  if (!follow) return 0;
13054
- let pos = statSync10(DAEMON_LOG_PATH).size;
13245
+ let pos = statSync11(DAEMON_LOG_PATH).size;
13055
13246
  for (; ; ) {
13056
13247
  await sleep2(500);
13057
13248
  let size;
13058
13249
  try {
13059
- size = statSync10(DAEMON_LOG_PATH).size;
13250
+ size = statSync11(DAEMON_LOG_PATH).size;
13060
13251
  } catch {
13061
13252
  continue;
13062
13253
  }
@@ -13328,7 +13519,7 @@ function effectiveWorktreesDir() {
13328
13519
  exists: (d) => existsSync17(d),
13329
13520
  isNonEmptyDir: (d) => {
13330
13521
  try {
13331
- return readdirSync10(d).length > 0;
13522
+ return readdirSync11(d).length > 0;
13332
13523
  } catch {
13333
13524
  return false;
13334
13525
  }
@@ -13384,17 +13575,17 @@ async function scoutRepo(repo) {
13384
13575
  await $6({ reject: false })`git -C ${worktree} reset --hard ${baseRef}`;
13385
13576
  }
13386
13577
  await setWorktreeIdentity(worktree, name);
13387
- const abs = (rel) => join25(worktree, rel);
13578
+ const abs = (rel) => join26(worktree, rel);
13388
13579
  const code = await runRepoScout({
13389
13580
  repoFullName,
13390
13581
  repoPath: worktree,
13391
13582
  name,
13392
- readFile: (rel) => existsSync17(abs(rel)) ? readFileSync18(abs(rel), "utf8") : null,
13583
+ readFile: (rel) => existsSync17(abs(rel)) ? readFileSync19(abs(rel), "utf8") : null,
13393
13584
  writeFile: (rel, text) => {
13394
13585
  mkdirSync10(dirname5(abs(rel)), { recursive: true });
13395
13586
  writeFileSync14(abs(rel), text);
13396
13587
  },
13397
- runScout: (prompt) => new Promise((resolve4) => {
13588
+ runScout: (prompt) => new Promise((resolve5) => {
13398
13589
  const child = spawn3(bin, [...ISOLATION_FLAGS, "--dangerously-skip-permissions", "-p", prompt], {
13399
13590
  cwd: worktree,
13400
13591
  stdio: ["ignore", "inherit", "inherit"]
@@ -13402,9 +13593,9 @@ async function scoutRepo(repo) {
13402
13593
  });
13403
13594
  child.on("error", (e) => {
13404
13595
  console.error(`\u043D\u0435 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B\u0441\u044F \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C (${bin}): ${e.message}`);
13405
- resolve4(1);
13596
+ resolve5(1);
13406
13597
  });
13407
- child.on("exit", (c) => resolve4(c ?? 1));
13598
+ child.on("exit", (c) => resolve5(c ?? 1));
13408
13599
  }),
13409
13600
  publish: (files) => publishScoutPr({ worktree, repo, files, name, baseBranch, openPr: openPr.url, fresh: openPr.known && !openPr.url }),
13410
13601
  log: (line) => console.log(line)
@@ -13942,6 +14133,11 @@ switch (cmd) {
13942
14133
  {
13943
14134
  db,
13944
14135
  kill: (pid) => process.kill(pid, "SIGTERM"),
14136
+ // TASWM-553: тот же порядок, что у демона (stop_run / POST /cancel) — снос worktree
14137
+ // ПОСЛЕ смерти процесса, а не следом за просьбой умереть. Здесь это нужнее всего:
14138
+ // сюда попадают ровно тогда, когда демон не ответил, и подчистить за собой некому.
14139
+ isAlive: pidAlive,
14140
+ hardKill: (pid) => process.kill(pid, "SIGKILL"),
13945
14141
  removeWorktree: (repo, p) => {
13946
14142
  const r = repo || SWARM_REPO;
13947
14143
  return r ? removeWorktree(r, p) : Promise.resolve();