@taskless-app/swarm 0.6.14 → 0.6.15

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/overmind.js CHANGED
@@ -317,21 +317,28 @@ function computeReviewPhase(input) {
317
317
  return { kind: "reviewer_dead", label: REVIEW_PHASE_TEXT.reviewer_dead, ready: true };
318
318
  }
319
319
  const p = input.ciProgress;
320
+ if (p && p.total > 0 && p.pass >= p.total) {
321
+ return { kind: "awaiting_reviewer", label: REVIEW_PHASE_TEXT.awaiting_reviewer, ready: false };
322
+ }
320
323
  const label = p && p.total > 0 ? `${REVIEW_PHASE_TEXT.checks} \u2014 CI ${p.pass}/${p.total}` : REVIEW_PHASE_TEXT.checks;
321
324
  return { kind: "checks", label, ready: false };
322
325
  }
323
- var REVIEW_PHASE_TEXT;
326
+ var REVIEW_PHASE_TEXT, PHASE_BY_LENGTH;
324
327
  var init_reviewPhase = __esm({
325
328
  "../swarm-core/src/reviewPhase.ts"() {
326
329
  "use strict";
327
330
  REVIEW_PHASE_TEXT = {
328
331
  checks: "\u0418\u0434\u0443\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438",
332
+ awaiting_reviewer: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430",
329
333
  ci_fixing: "CI \u043A\u0440\u0430\u0441\u043D\u044B\u0439 \u2014 \u0434\u0440\u043E\u043D \u0447\u0438\u043D\u0438\u0442",
330
334
  in_review: "\u041D\u0430 \u0440\u0435\u0432\u044C\u044E",
331
335
  changes: "\u041D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0430\u0432\u0438\u0442",
332
336
  ready: "\u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0433\u043E\u0442\u043E\u0432 \u043A \u043C\u0435\u0440\u0436\u0443",
333
337
  reviewer_dead: "\u0420\u0435\u0432\u044C\u044E\u0435\u0440 \u0443\u043C\u0435\u0440 \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u043D\u0435\u0442, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A"
334
338
  };
339
+ PHASE_BY_LENGTH = Object.keys(REVIEW_PHASE_TEXT).sort(
340
+ (a, b) => REVIEW_PHASE_TEXT[b].length - REVIEW_PHASE_TEXT[a].length
341
+ );
335
342
  }
336
343
  });
337
344
 
@@ -511,7 +518,7 @@ var init_status = __esm({
511
518
  // ../swarm-core/src/deps.ts
512
519
  function judgeDep(runs) {
513
520
  const real = runs.filter((r) => !r.service);
514
- if (real.length === 0) return { ...PENDING };
521
+ if (real.length === 0) return { ...UNKNOWN };
515
522
  if (real.some((r) => !isTerminalStatus(r.status))) return { ...PENDING };
516
523
  if (real.some((r) => r.status === "done")) {
517
524
  return { outcome: "done", met: true, status: "done", name: null, stopReason: null };
@@ -531,12 +538,13 @@ function depClearedText(slug, v) {
531
538
  const why = v.stopReason ? ` \u2014 ${v.stopReason}` : "";
532
539
  return `\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C \u0441\u043D\u044F\u0442\u0430: \u0436\u0434\u0430\u043B ${who}, \u0442\u043E\u0442 ${how}${why}. \u0423\u0441\u043F\u0435\u0448\u043D\u043E\u0433\u043E \u0444\u0438\u043D\u0430\u043B\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u2014 \u0435\u0434\u0443.`;
533
540
  }
534
- var PENDING, DEP_END_LABEL;
541
+ var PENDING, UNKNOWN, DEP_END_LABEL;
535
542
  var init_deps = __esm({
536
543
  "../swarm-core/src/deps.ts"() {
537
544
  "use strict";
538
545
  init_status();
539
546
  PENDING = { outcome: "pending", met: false, status: null, name: null, stopReason: null };
547
+ UNKNOWN = { outcome: "unknown", met: false, status: null, name: null, stopReason: null };
540
548
  DEP_END_LABEL = {
541
549
  stopped: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C",
542
550
  failed: "\u0443\u043F\u0430\u043B",
@@ -555,6 +563,9 @@ function stampFromEpochSec(sec) {
555
563
  function sqliteMs(ts) {
556
564
  return Date.parse(ts.replace(" ", "T") + "Z");
557
565
  }
566
+ function stampMs(at) {
567
+ return at.includes("T") ? Date.parse(at) : sqliteMs(at);
568
+ }
558
569
  function toStamp(v) {
559
570
  if (typeof v !== "string" || !v) return null;
560
571
  const d = new Date(v);
@@ -1263,31 +1274,6 @@ function buildZoneModel(entries, opts = {}) {
1263
1274
  const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
1264
1275
  return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
1265
1276
  }
1266
- function zoneForFiles(model, files) {
1267
- if (!hasZoneModel(model) || !files.length) return null;
1268
- const spineSet = new Set(model.spine);
1269
- const touchedSpine = files.filter((f) => spineSet.has(f));
1270
- const votes = /* @__PURE__ */ new Map();
1271
- for (const f of new Set(files)) {
1272
- const z2 = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
1273
- if (z2) bump(votes, z2);
1274
- }
1275
- const top = topKey(votes);
1276
- if (!top) {
1277
- return {
1278
- zone: SPINE_ZONE,
1279
- confidence: 1,
1280
- why: `\u0432\u0441\u0435 \u0444\u0430\u0439\u043B\u044B \u2014 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${touchedSpine.slice(0, 3).join(", ")})`,
1281
- spine: true
1282
- };
1283
- }
1284
- return {
1285
- zone: top.key,
1286
- confidence: top.share,
1287
- why: `${top.count} \u0438\u0437 ${[...votes.values()].reduce((a, b) => a + b, 0)} \u0444\u0430\u0439\u043B\u043E\u0432 \u2014 \u0437\u043E\u043D\u0430 ${top.key}`,
1288
- spine: touchedSpine.length > 0
1289
- };
1290
- }
1291
1277
  function suggestZonesFromText(model, text, limit = 3) {
1292
1278
  if (!hasZoneModel(model)) return [];
1293
1279
  const terms = tokenize(text);
@@ -2105,6 +2091,7 @@ __export(config_exports, {
2105
2091
  REPO_MAP_PATH: () => REPO_MAP_PATH,
2106
2092
  REVIEW_CUTOFF_PATH: () => REVIEW_CUTOFF_PATH,
2107
2093
  RUN_LOGS_DIR: () => RUN_LOGS_DIR,
2094
+ SWARM_ANOMALY_DEP_MISSING_MS: () => SWARM_ANOMALY_DEP_MISSING_MS,
2108
2095
  SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
2109
2096
  SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS: () => SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
2110
2097
  SWARM_ANOMALY_MAX_REPEATS: () => SWARM_ANOMALY_MAX_REPEATS,
@@ -2147,6 +2134,7 @@ __export(config_exports, {
2147
2134
  SWARM_REPO: () => SWARM_REPO,
2148
2135
  SWARM_REPO_MAP_MS: () => SWARM_REPO_MAP_MS,
2149
2136
  SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
2137
+ SWARM_REPO_SCAN_TTL_MS: () => SWARM_REPO_SCAN_TTL_MS,
2150
2138
  SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
2151
2139
  SWARM_SCAN_DEPTH: () => SWARM_SCAN_DEPTH,
2152
2140
  SWARM_SCAN_ROOTS_RAW: () => SWARM_SCAN_ROOTS_RAW,
@@ -2168,6 +2156,7 @@ __export(config_exports, {
2168
2156
  WORKTREES_LEGACY_DIR: () => WORKTREES_LEGACY_DIR,
2169
2157
  WORKTREES_NOINDEX_DIR: () => WORKTREES_NOINDEX_DIR,
2170
2158
  droneWorktreePath: () => droneWorktreePath,
2159
+ droneWorktreeRoot: () => droneWorktreeRoot,
2171
2160
  mcpEndpoint: () => mcpEndpoint,
2172
2161
  migrateWorktreesToNoindex: () => migrateWorktreesToNoindex,
2173
2162
  normalizeRepo: () => normalizeRepo,
@@ -2186,7 +2175,10 @@ function normalizeRepo(repo) {
2186
2175
  }
2187
2176
  }
2188
2177
  function droneWorktreePath(worktreesDir, repo, slug) {
2189
- return join5(worktreesDir, basename(normalizeRepo(repo)), slug);
2178
+ return join5(droneWorktreeRoot(worktreesDir, repo), slug);
2179
+ }
2180
+ function droneWorktreeRoot(worktreesDir, repo) {
2181
+ return join5(worktreesDir, basename(normalizeRepo(repo)));
2190
2182
  }
2191
2183
  function resolveEffectiveWorktreesDir(deps) {
2192
2184
  if (deps.envOverride) return deps.envOverride;
@@ -2248,7 +2240,7 @@ function reviewCutoffAt() {
2248
2240
  }
2249
2241
  return at;
2250
2242
  }
2251
- var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_WATCHDOG_STALL_MS;
2243
+ var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_CPU_WINDOW_MS, SWARM_CPU_SANITY_BUSY, SWARM_CPU_SANITY_LOAD_FACTOR, SWARM_CPU_SANITY_LAG_MS, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, SWARM_REPO_SCAN_TTL_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, SWARM_AUTO_VERIFY, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_ANOMALY_DEP_MISSING_MS, SWARM_WATCHDOG_STALL_MS;
2252
2244
  var init_config = __esm({
2253
2245
  "src/config.ts"() {
2254
2246
  "use strict";
@@ -2310,6 +2302,7 @@ var init_config = __esm({
2310
2302
  SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
2311
2303
  REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join5(SWARM_HOME, "repo-map.json");
2312
2304
  SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
2305
+ SWARM_REPO_SCAN_TTL_MS = envNum("SWARM_REPO_SCAN_TTL_MS", 24 * 60 * 6e4);
2313
2306
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
2314
2307
  SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
2315
2308
  TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
@@ -2336,6 +2329,7 @@ var init_config = __esm({
2336
2329
  SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
2337
2330
  SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
2338
2331
  SWARM_ANOMALY_RESOURCE_GATE_STALL_MS = envNum("SWARM_ANOMALY_RESOURCE_GATE_STALL_MS", 5 * 6e4);
2332
+ SWARM_ANOMALY_DEP_MISSING_MS = envNum("SWARM_ANOMALY_DEP_MISSING_MS", 2 * 6e4);
2339
2333
  SWARM_WATCHDOG_STALL_MS = envNum("SWARM_WATCHDOG_STALL_MS", 15 * 6e4);
2340
2334
  }
2341
2335
  });
@@ -2734,11 +2728,11 @@ var init_storm = __esm({
2734
2728
  });
2735
2729
 
2736
2730
  // src/heartbeat.ts
2737
- import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
2731
+ import { readFileSync as readFileSync5, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
2738
2732
  function writeHeartbeat(path, now) {
2739
2733
  const tmp = `${path}.tmp`;
2740
2734
  writeFileSync5(tmp, String(now));
2741
- renameSync3(tmp, path);
2735
+ renameSync4(tmp, path);
2742
2736
  }
2743
2737
  function readHeartbeat(path) {
2744
2738
  try {
@@ -2761,7 +2755,7 @@ var init_heartbeat = __esm({
2761
2755
 
2762
2756
  // src/agentToken.ts
2763
2757
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3 } from "fs";
2764
- import { join as join8 } from "path";
2758
+ import { join as join9 } from "path";
2765
2759
  function loadAgentToken() {
2766
2760
  try {
2767
2761
  const s = JSON.parse(readFileSync7(STORE_PATH, "utf8"));
@@ -2817,7 +2811,7 @@ var init_agentToken = __esm({
2817
2811
  "src/agentToken.ts"() {
2818
2812
  "use strict";
2819
2813
  init_config();
2820
- STORE_PATH = join8(SWARM_HOME, "agent.json");
2814
+ STORE_PATH = join9(SWARM_HOME, "agent.json");
2821
2815
  base = (api) => api.replace(/\/$/, "");
2822
2816
  TOKEN_PROBE_TIMEOUT_MS = 8e3;
2823
2817
  }
@@ -3177,9 +3171,9 @@ var init_procCleanup = __esm({
3177
3171
  });
3178
3172
 
3179
3173
  // src/git.ts
3180
- import { existsSync as existsSync9, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
3174
+ import { existsSync as existsSync10, readdirSync as readdirSync11, statSync as statSync8 } from "fs";
3181
3175
  import { appendFile, chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
3182
- import { isAbsolute as isAbsolute2, join as join16, sep as sep2 } from "path";
3176
+ import { isAbsolute as isAbsolute2, join as join17, sep as sep2 } from "path";
3183
3177
  import { $ as $2 } from "execa";
3184
3178
  function isUnrecoverableRefError(e) {
3185
3179
  if (e instanceof MissingRefError) return true;
@@ -3191,7 +3185,7 @@ async function reclaimStaleWorktree(repo, worktree) {
3191
3185
  }
3192
3186
  function isLinkedWorktree(dir) {
3193
3187
  try {
3194
- return statSync7(join16(dir, ".git")).isFile();
3188
+ return statSync8(join17(dir, ".git")).isFile();
3195
3189
  } catch {
3196
3190
  return false;
3197
3191
  }
@@ -3203,7 +3197,7 @@ async function detachWorktree(repo, worktree) {
3203
3197
  }
3204
3198
  }
3205
3199
  async function ensureWorktreeDirGone(worktree) {
3206
- if (!existsSync9(worktree)) return false;
3200
+ if (!existsSync10(worktree)) return false;
3207
3201
  if (!isLinkedWorktree(worktree)) {
3208
3202
  console.error(
3209
3203
  `[git] ${worktree} \u2014 \u043D\u0435 \u043F\u0440\u0438\u0441\u0442\u0451\u0433\u043D\u0443\u0442\u044B\u0439 worktree (.git \u043D\u0435 \u0444\u0430\u0439\u043B): \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u041D\u0415 \u0442\u0440\u043E\u0433\u0430\u044E. \u0415\u0441\u043B\u0438 \u044D\u0442\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E worktree \u0434\u0440\u043E\u043D\u0430 \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043E \u0440\u0443\u043A\u0430\u043C\u0438; \u0435\u0441\u043B\u0438 \u044D\u0442\u043E \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u0447\u0435\u043A\u0430\u0443\u0442, \u0437\u043D\u0430\u0447\u0438\u0442 \u0432\u0435\u0442\u043A\u0430 \u0441\u043B\u0430\u0433\u0430 \u0447\u0435\u043A\u043D\u0443\u0442\u0430 \u0432 \u043D\u0451\u043C (git checkout <slug>) \u2014 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0435\u0433\u043E \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u0432\u0435\u0442\u043A\u0443.`
@@ -3218,7 +3212,7 @@ async function ensureWorktreeDirGone(worktree) {
3218
3212
  );
3219
3213
  return true;
3220
3214
  }
3221
- if (existsSync9(worktree)) {
3215
+ if (existsSync10(worktree)) {
3222
3216
  console.error(`[git] \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree ${worktree} \u043F\u0435\u0440\u0435\u0436\u0438\u043B rm \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u043A\u0430\u043C\u0438, \u0438\u043D\u0430\u0447\u0435 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u0441\u043B\u0430\u0433\u0430 \u0432\u0441\u0442\u0430\u043D\u0435\u0442.`);
3223
3217
  }
3224
3218
  return true;
@@ -3266,7 +3260,7 @@ async function checkoutWorktree(repo, path, branch, opts) {
3266
3260
  }
3267
3261
  const busyAt = await worktreeForBranch(repo, branch);
3268
3262
  if (busyAt) {
3269
- const exists = existsSync9(busyAt);
3263
+ const exists = existsSync10(busyAt);
3270
3264
  const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
3271
3265
  if (live) throw new WorktreeBusyError(branch, busyAt);
3272
3266
  if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
@@ -3301,8 +3295,8 @@ async function ensureGitExcluded(worktree, pattern) {
3301
3295
  try {
3302
3296
  const commonDir = await gitPath(worktree, "--git-common-dir");
3303
3297
  if (!commonDir) return false;
3304
- const infoDir = join16(commonDir, "info");
3305
- const excludeFile = join16(infoDir, "exclude");
3298
+ const infoDir = join17(commonDir, "info");
3299
+ const excludeFile = join17(infoDir, "exclude");
3306
3300
  let current = "";
3307
3301
  try {
3308
3302
  current = await readFile(excludeFile, "utf8");
@@ -3322,7 +3316,7 @@ async function gitPath(worktree, flag) {
3322
3316
  const out = await $2({ reject: false })`git -C ${worktree} rev-parse ${flag}`;
3323
3317
  const raw = out.stdout.trim();
3324
3318
  if (out.exitCode !== 0 || !raw) return null;
3325
- return isAbsolute2(raw) ? raw : join16(worktree, raw);
3319
+ return isAbsolute2(raw) ? raw : join17(worktree, raw);
3326
3320
  }
3327
3321
  async function ensureWorktreeHygiene(worktree) {
3328
3322
  for (const pattern of SWARM_EXCLUDE_PATTERNS) await ensureGitExcluded(worktree, pattern);
@@ -3332,12 +3326,12 @@ async function installGuardHooks(worktree) {
3332
3326
  try {
3333
3327
  const gitDir = await gitPath(worktree, "--git-dir");
3334
3328
  if (!gitDir) return false;
3335
- const hooksDir = join16(gitDir, "swarm-hooks");
3329
+ const hooksDir = join17(gitDir, "swarm-hooks");
3336
3330
  const prev = await previousHooksPath(worktree);
3337
3331
  await mkdir(hooksDir, { recursive: true });
3338
3332
  for (const hook of GUARD_HOOKS) {
3339
- await writeFile(join16(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
3340
- await chmod(join16(hooksDir, hook), 493);
3333
+ await writeFile(join17(hooksDir, hook), guardHookScript(hook, prev ? `${posix(prev)}/${hook}` : null));
3334
+ await chmod(join17(hooksDir, hook), 493);
3341
3335
  }
3342
3336
  await $2`git -C ${worktree} config extensions.worktreeConfig true`;
3343
3337
  await $2`git -C ${worktree} config --worktree core.hooksPath ${posix(hooksDir)}`;
@@ -3356,7 +3350,7 @@ async function previousHooksPath(worktree) {
3356
3350
  if (out.exitCode === 0 && value) return value;
3357
3351
  }
3358
3352
  const commonDir = await gitPath(worktree, "--git-common-dir");
3359
- return commonDir ? join16(commonDir, "hooks") : null;
3353
+ return commonDir ? join17(commonDir, "hooks") : null;
3360
3354
  }
3361
3355
  async function removeWorktree(repo, path) {
3362
3356
  const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
@@ -3375,7 +3369,7 @@ async function worktreeDirty(path) {
3375
3369
  async function repairMovedWorktrees(targetDir) {
3376
3370
  const subdirs = (dir) => {
3377
3371
  try {
3378
- return readdirSync10(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join16(dir, d.name));
3372
+ return readdirSync11(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join17(dir, d.name));
3379
3373
  } catch {
3380
3374
  return [];
3381
3375
  }
@@ -3502,7 +3496,7 @@ __export(droneChannel_exports, {
3502
3496
  verifyLocalCliToken: () => verifyLocalCliToken
3503
3497
  });
3504
3498
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
3505
- import { chmodSync, mkdirSync as mkdirSync7, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
3499
+ import { chmodSync, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
3506
3500
  import { dirname as dirname3 } from "path";
3507
3501
  import { serve } from "@hono/node-server";
3508
3502
  function loadChannelSecret(path) {
@@ -3512,7 +3506,7 @@ function loadChannelSecret(path) {
3512
3506
  } catch {
3513
3507
  }
3514
3508
  const secret = randomBytes(32).toString("hex");
3515
- mkdirSync7(dirname3(path), { recursive: true });
3509
+ mkdirSync8(dirname3(path), { recursive: true });
3516
3510
  writeFileSync10(path, secret, { mode: 384 });
3517
3511
  try {
3518
3512
  chmodSync(path, 384);
@@ -3857,7 +3851,7 @@ var init_droneChannel = __esm({
3857
3851
 
3858
3852
  // src/attachments.ts
3859
3853
  import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
3860
- import { join as join19 } from "path";
3854
+ import { join as join20 } from "path";
3861
3855
  function attachmentsFromDoc(doc, origin) {
3862
3856
  const out = [];
3863
3857
  const walk2 = (node) => {
@@ -3948,7 +3942,7 @@ async function materializeAttachments(opts) {
3948
3942
  };
3949
3943
  const result = { files: [], skipped: [] };
3950
3944
  if (opts.refs.length === 0) return result;
3951
- const dir = join19(opts.worktree, ATTACHMENTS_DIR);
3945
+ const dir = join20(opts.worktree, ATTACHMENTS_DIR);
3952
3946
  await io.removeDir(dir);
3953
3947
  let dirReady = false;
3954
3948
  let total = 0;
@@ -3994,7 +3988,7 @@ async function materializeAttachments(opts) {
3994
3988
  await io.mkdir(dir);
3995
3989
  dirReady = true;
3996
3990
  }
3997
- await io.writeFile(join19(dir, name), file.bytes);
3991
+ await io.writeFile(join20(dir, name), file.bytes);
3998
3992
  } catch (e) {
3999
3993
  result.skipped.push(`${label} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText(e)})`);
4000
3994
  continue;
@@ -4147,7 +4141,7 @@ var init_attachments = __esm({
4147
4141
 
4148
4142
  // src/shotUpload.ts
4149
4143
  import { readFile as fsReadFile } from "fs/promises";
4150
- import { basename as basename2, isAbsolute as isAbsolute3, join as join20 } from "path";
4144
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join21 } from "path";
4151
4145
  function shotMimetype(path) {
4152
4146
  const dot = path.lastIndexOf(".");
4153
4147
  if (dot < 0) return null;
@@ -4173,7 +4167,7 @@ async function readShots(paths, opts) {
4173
4167
  }
4174
4168
  let bytes;
4175
4169
  try {
4176
- bytes = await read(isAbsolute3(path) ? path : join20(opts.worktree, path));
4170
+ bytes = await read(isAbsolute3(path) ? path : join21(opts.worktree, path));
4177
4171
  } catch (e) {
4178
4172
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
4179
4173
  continue;
@@ -4252,7 +4246,7 @@ var init_shotUpload = __esm({
4252
4246
  // src/oauth.ts
4253
4247
  import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
4254
4248
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
4255
- import { join as join21 } from "path";
4249
+ import { join as join22 } from "path";
4256
4250
  function makePkce() {
4257
4251
  const verifier = randomBytes2(48).toString("base64url");
4258
4252
  const challenge = createHash2("sha256").update(verifier).digest("base64url");
@@ -4387,8 +4381,8 @@ var init_oauth = __esm({
4387
4381
  AUTHORIZE = "/api/oauth/authorize";
4388
4382
  TOKEN = "/api/oauth/token";
4389
4383
  SCOPE = "openid profile email taskless:full";
4390
- STORE_PATH2 = join21(SWARM_HOME, "oauth.json");
4391
- CLIENT_PATH = join21(SWARM_HOME, "oauth-client.json");
4384
+ STORE_PATH2 = join22(SWARM_HOME, "oauth.json");
4385
+ CLIENT_PATH = join22(SWARM_HOME, "oauth-client.json");
4392
4386
  base2 = (api) => api.replace(/\/$/, "");
4393
4387
  }
4394
4388
  });
@@ -4435,7 +4429,7 @@ var init_authState = __esm({
4435
4429
 
4436
4430
  // src/workspace.ts
4437
4431
  import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
4438
- import { join as join22 } from "path";
4432
+ import { join as join23 } from "path";
4439
4433
  function loadWorkspace() {
4440
4434
  try {
4441
4435
  const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
@@ -4452,7 +4446,7 @@ var init_workspace = __esm({
4452
4446
  "src/workspace.ts"() {
4453
4447
  "use strict";
4454
4448
  init_config();
4455
- WORKSPACE_PATH = join22(SWARM_HOME, "workspace.json");
4449
+ WORKSPACE_PATH = join23(SWARM_HOME, "workspace.json");
4456
4450
  }
4457
4451
  });
4458
4452
 
@@ -4947,7 +4941,8 @@ __export(db_exports, {
4947
4941
  SwarmDb: () => SwarmDb
4948
4942
  });
4949
4943
  import { createRequire as createRequire2 } from "module";
4950
- import { existsSync as existsSync11 } from "fs";
4944
+ import { randomUUID as randomUUID3 } from "crypto";
4945
+ import { existsSync as existsSync12 } from "fs";
4951
4946
  import { resolve as resolve3 } from "path";
4952
4947
  function rowToQuestion(row) {
4953
4948
  return {
@@ -5109,6 +5104,12 @@ CREATE INDEX IF NOT EXISTS idx_runs_area ON runs(area);
5109
5104
  CREATE INDEX IF NOT EXISTS idx_questions_run ON questions(run_id);
5110
5105
  CREATE INDEX IF NOT EXISTS idx_questions_status ON questions(status);
5111
5106
  CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id);
5107
+ -- \u043A\u043B\u044E\u0447-\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u043F\u0440\u043E \u0421\u0410\u041C \u0424\u0410\u0419\u041B \u0431\u0430\u0437\u044B (\u043D\u0435 \u043F\u0440\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u044B). \u041F\u043E\u043A\u0430 \u0432 \u043D\u0451\u043C \u043E\u0434\u043D\u0430 \u0437\u0430\u043F\u0438\u0441\u044C \u2014 instance_id
5108
+ -- (TASWM-419): \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u042D\u0422\u041E\u0419 \u0436\u0438\u0437\u043D\u0438 \u0431\u0430\u0437\u044B, \u043A\u043E\u0442\u043E\u0440\u044B\u043C \u043D\u0435\u0439\u043C\u0441\u043F\u0435\u0439\u0441\u044F\u0442\u0441\u044F \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0438 run-log'\u043E\u0432.
5109
+ CREATE TABLE IF NOT EXISTS meta (
5110
+ key TEXT PRIMARY KEY,
5111
+ value TEXT NOT NULL
5112
+ );
5112
5113
  -- \u043C\u0430\u0440\u043A\u0435\u0440 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u043D\u044B\u0445 \u0438\u043C\u043F\u043E\u0440\u0442\u043E\u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0438\u0437 \u0434\u0440\u0443\u0433\u0438\u0445 \u0411\u0414 (\u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C importDb)
5113
5114
  CREATE TABLE IF NOT EXISTS imports (
5114
5115
  source TEXT PRIMARY KEY,
@@ -5121,6 +5122,8 @@ CREATE TABLE IF NOT EXISTS imports (
5121
5122
  defaultRepo;
5122
5123
  databaseCtor;
5123
5124
  // для importDb (второй Database, source)
5125
+ /** Идентификатор этой жизни файла базы (TASWM-419) — им неймспейсятся каталоги run-log'ов. */
5126
+ instanceId;
5124
5127
  constructor(path = "swarm.db", defaultRepo = "") {
5125
5128
  this.defaultRepo = normalizeRepo(defaultRepo);
5126
5129
  this.databaseCtor = loadBetterSqlite3();
@@ -5188,6 +5191,22 @@ CREATE TABLE IF NOT EXISTS imports (
5188
5191
  if (this.defaultRepo) {
5189
5192
  this.db.prepare("UPDATE runs SET repo = ? WHERE repo IS NULL").run(this.defaultRepo);
5190
5193
  }
5194
+ this.instanceId = this.ensureInstanceId();
5195
+ }
5196
+ /**
5197
+ * Идентификатор ЭТОЙ жизни файла базы (TASWM-419). Заводится при первом открытии и живёт
5198
+ * в самой базе, поэтому переживает рестарт демона, но НЕ переживает пересоздание файла:
5199
+ * снесли базу — новый id, и каталоги run-log'ов нового прогона #1 не подселяются к файлам
5200
+ * прогона #1 из прошлой жизни. Именно это и было корнем «лог не того прогона»: номер там
5201
+ * был вся информация об адресе.
5202
+ */
5203
+ ensureInstanceId() {
5204
+ const read = () => this.db.prepare("SELECT value FROM meta WHERE key = 'instance_id'").get()?.value;
5205
+ const existing = read();
5206
+ if (existing) return existing;
5207
+ const fresh = randomUUID3().replace(/-/g, "").slice(0, 12);
5208
+ this.db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES ('instance_id', ?)").run(fresh);
5209
+ return read() ?? fresh;
5191
5210
  }
5192
5211
  close() {
5193
5212
  this.db.close();
@@ -5282,6 +5301,15 @@ CREATE TABLE IF NOT EXISTS imports (
5282
5301
  const m = /^local:(\d+)$/.exec(key);
5283
5302
  return m ? this.getRun(Number(m[1])) : null;
5284
5303
  }
5304
+ /**
5305
+ * TASWM-419: адрес каталога run-log'ов — номер, НЕЙМСПЕЙСНУТЫЙ идентификатором файла базы
5306
+ * (`local:<instance>:<id>`). Тот же номер в пересозданной базе даёт другой instance, то
5307
+ * есть другой каталог: подселиться к файлам чужого прогона с тем же номером больше нечем.
5308
+ * Псевдонимов у sqlite-прогона нет — ключ не меняется, пока жив файл базы.
5309
+ */
5310
+ runLogKeys(id) {
5311
+ return this.getRun(id) ? [`local:${this.instanceId}:${id}`] : [];
5312
+ }
5285
5313
  listRuns(status) {
5286
5314
  const rows = status ? this.db.prepare("SELECT * FROM runs WHERE status = ? ORDER BY id DESC").all(status) : this.db.prepare("SELECT * FROM runs ORDER BY id DESC").all();
5287
5315
  return rows.map(rowToRun);
@@ -5522,7 +5550,7 @@ CREATE TABLE IF NOT EXISTS imports (
5522
5550
  const repoN = normalizeRepo(repo);
5523
5551
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
5524
5552
  if (already) return { imported: false, runs: already.run_count };
5525
- if (!existsSync11(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
5553
+ if (!existsSync12(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
5526
5554
  const src = new this.databaseCtor(abs);
5527
5555
  try {
5528
5556
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -5891,8 +5919,8 @@ var init_droneStore = __esm({
5891
5919
 
5892
5920
  // src/overmind.ts
5893
5921
  import { homedir as homedir5 } from "os";
5894
- import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync12, renameSync as renameSync10 } from "fs";
5895
- import { basename as basename4, dirname as dirname4, join as join29 } from "path";
5922
+ import { existsSync as existsSync17, readFileSync as readFileSync20, readdirSync as readdirSync13, renameSync as renameSync11 } from "fs";
5923
+ import { basename as basename4, dirname as dirname4, join as join30 } from "path";
5896
5924
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
5897
5925
  import { spawn as spawn3 } from "child_process";
5898
5926
 
@@ -5972,13 +6000,66 @@ init_config();
5972
6000
  init_resources();
5973
6001
  init_names2();
5974
6002
  init_storm();
5975
- import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
6003
+ import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
5976
6004
  import { dirname } from "path";
5977
6005
  import { randomUUID } from "crypto";
5978
6006
 
5979
6007
  // src/logTail.ts
5980
- import { closeSync, existsSync as existsSync4, openSync, readSync, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
6008
+ import { closeSync, existsSync as existsSync5, openSync, readSync, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
6009
+ import { join as join7 } from "path";
6010
+
6011
+ // src/runLogDir.ts
6012
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync4, renameSync as renameSync3, statSync as statSync3 } from "fs";
5981
6013
  import { join as join6 } from "path";
6014
+ var LEGACY_RUN_LOGS_SUBDIR = "legacy";
6015
+ function runLogDirName(runLogKey) {
6016
+ const safe2 = runLogKey.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "");
6017
+ if (!safe2) 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)}`);
6018
+ return /^\d+$/.test(safe2) ? `k-${safe2}` : safe2;
6019
+ }
6020
+ function runLogDirPath(runLogsDir, runLogKey) {
6021
+ return join6(runLogsDir, runLogDirName(runLogKey));
6022
+ }
6023
+ function findRunLogDir(runLogsDir, keys) {
6024
+ for (const key of keys) {
6025
+ const dir = runLogDirPath(runLogsDir, key);
6026
+ if (existsSync4(dir)) return dir;
6027
+ }
6028
+ return null;
6029
+ }
6030
+ function runLogDirForWrite(runLogsDir, keys) {
6031
+ const canonical = keys[0];
6032
+ if (!canonical) return null;
6033
+ return findRunLogDir(runLogsDir, keys) ?? runLogDirPath(runLogsDir, canonical);
6034
+ }
6035
+ function sweepLegacyRunLogs(runLogsDir) {
6036
+ const legacyDir = join6(runLogsDir, LEGACY_RUN_LOGS_SUBDIR);
6037
+ let entries;
6038
+ try {
6039
+ entries = readdirSync4(runLogsDir);
6040
+ } catch {
6041
+ return { moved: 0, failed: 0, legacyDir };
6042
+ }
6043
+ let moved = 0;
6044
+ let failed = 0;
6045
+ for (const name of entries) {
6046
+ if (!/^\d+$/.test(name)) continue;
6047
+ const from = join6(runLogsDir, name);
6048
+ try {
6049
+ if (!statSync3(from).isDirectory()) continue;
6050
+ mkdirSync4(legacyDir, { recursive: true });
6051
+ let to = join6(legacyDir, name);
6052
+ for (let n = 2; existsSync4(to) && n < 100; n++) to = join6(legacyDir, `${name}-${n}`);
6053
+ renameSync3(from, to);
6054
+ moved++;
6055
+ } catch {
6056
+ failed++;
6057
+ }
6058
+ }
6059
+ return { moved, failed, legacyDir };
6060
+ }
6061
+
6062
+ // src/logTail.ts
5982
6063
  var TAIL_LINES_FALLBACK = 200;
5983
6064
  var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
5984
6065
  function fileHeader(name, size, shown) {
@@ -5988,7 +6069,7 @@ function fileHeader(name, size, shown) {
5988
6069
  function readTail(path, opts) {
5989
6070
  let size;
5990
6071
  try {
5991
- size = statSync3(path).size;
6072
+ size = statSync4(path).size;
5992
6073
  } catch {
5993
6074
  return null;
5994
6075
  }
@@ -6018,11 +6099,12 @@ function readTail(path, opts) {
6018
6099
  text = rows.join("\n");
6019
6100
  return { text, size, shown: Buffer.byteLength(text, "utf8") };
6020
6101
  }
6021
- function latestRunLaunch(runLogsDir, localRunId) {
6022
- const dir = join6(runLogsDir, String(localRunId));
6102
+ function latestRunLaunch(runLogsDir, runLogKeys) {
6103
+ const dir = findRunLogDir(runLogsDir, runLogKeys);
6104
+ if (!dir) return null;
6023
6105
  let files;
6024
6106
  try {
6025
- files = readdirSync4(dir);
6107
+ files = readdirSync5(dir);
6026
6108
  } catch {
6027
6109
  return null;
6028
6110
  }
@@ -6033,13 +6115,17 @@ function latestRunLaunch(runLogsDir, localRunId) {
6033
6115
  }
6034
6116
  if (indexes.size === 0) return null;
6035
6117
  const index = Math.max(...indexes);
6036
- const out = [join6(dir, `${index}.json`), join6(dir, `${index}.json.done`)].find((p) => existsSync4(p)) ?? null;
6037
- const err = existsSync4(join6(dir, `${index}.err.log`)) ? join6(dir, `${index}.err.log`) : null;
6118
+ const out = [join7(dir, `${index}.json`), join7(dir, `${index}.json.done`)].find((p) => existsSync5(p)) ?? null;
6119
+ const err = existsSync5(join7(dir, `${index}.err.log`)) ? join7(dir, `${index}.err.log`) : null;
6038
6120
  return { index, total: indexes.size, out, err };
6039
6121
  }
6040
- function runLogTail(runLogsDir, localRunId, opts) {
6041
- const launch = latestRunLaunch(runLogsDir, localRunId);
6042
- if (!launch) return { error: `\u043B\u043E\u0433\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 (${join6(runLogsDir, String(localRunId))} \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442)` };
6122
+ function runLogTail(runLogsDir, runLogKeys, opts) {
6123
+ const launch = latestRunLaunch(runLogsDir, runLogKeys);
6124
+ if (!launch) {
6125
+ const canonical = runLogKeys[0];
6126
+ const where = canonical ? runLogDirPath(runLogsDir, canonical) : runLogsDir;
6127
+ return { error: `\u043B\u043E\u0433\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 (${where} \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442)` };
6128
+ }
6043
6129
  const parts = [];
6044
6130
  let budget = opts.maxBytes;
6045
6131
  if (launch.err) {
@@ -6094,8 +6180,8 @@ async function serveLogRequests(deps, commands) {
6094
6180
  } else if (!cmd.runId) {
6095
6181
  result = { error: "\u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0431\u0435\u0437 runId \u2014 \u043D\u0435\u0447\u0435\u0433\u043E \u0447\u0438\u0442\u0430\u0442\u044C" };
6096
6182
  } else {
6097
- const local = deps.localRunId(cmd.runId);
6098
- result = local == null ? { error: `\u043F\u0440\u043E\u0433\u043E\u043D ${cmd.runId} \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D (\u0435\u0433\u043E \u0432\u0435\u043B\u043E \u043D\u0435 \u043E\u043D\u0430?)` } : runLogTail(deps.runLogsDir, local, opts);
6183
+ const keys = deps.runLogKeys(cmd.runId);
6184
+ result = keys.length === 0 ? { error: `\u043F\u0440\u043E\u0433\u043E\u043D ${cmd.runId} \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D (\u0435\u0433\u043E \u0432\u0435\u043B\u043E \u043D\u0435 \u043E\u043D\u0430?)` } : runLogTail(deps.runLogsDir, keys, opts);
6099
6185
  }
6100
6186
  } catch (e) {
6101
6187
  result = { error: `\u043D\u0435 \u0441\u043C\u043E\u0433 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u043B\u043E\u0433: ${e instanceof Error ? e.message : String(e)}` };
@@ -6191,6 +6277,11 @@ var SwarmClient = class _SwarmClient {
6191
6277
  // Живёт в памяти: рестарт демона просто переспросит прод.
6192
6278
  depsProd = /* @__PURE__ */ new Map();
6193
6279
  depsAskedAt = /* @__PURE__ */ new Map();
6280
+ // TASWM-399: ссылка dependsOn → человеко-слаг блокера, как его назвал прод. В колонке
6281
+ // лежит cuid прогона или задачи, а в лог демона, в событие прогона и в аномалию должен
6282
+ // уходить «TASWM-392», а не «cmsqqpphd011y2xtbckkut6bj» — иначе оператору всё равно
6283
+ // нечего искать. Резолв делает прод (см. resolveDepRefs), здесь только запоминаем.
6284
+ depSlug = /* @__PURE__ */ new Map();
6194
6285
  static DEPS_TTL_MS = 6e4;
6195
6286
  // TASWM-327: прод старее демона и не знает аргумент `level` у swarm_notify. Выясняется
6196
6287
  // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
@@ -6250,25 +6341,25 @@ var SwarmClient = class _SwarmClient {
6250
6341
  this.repoFullNames = registry.map((e) => e.repoFullName);
6251
6342
  this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
6252
6343
  this.host = opts.host ?? null;
6253
- this.repoExists = opts.repoExists ?? existsSync5;
6344
+ this.repoExists = opts.repoExists ?? existsSync6;
6254
6345
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
6255
6346
  this.executorId = opts.executorId ?? EXECUTOR.id;
6256
6347
  this.outboxPath = opts.outboxPath;
6257
6348
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
6258
6349
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
6259
- mkdirSync4(dirname(this.outboxPath), { recursive: true });
6350
+ mkdirSync5(dirname(this.outboxPath), { recursive: true });
6260
6351
  this.recover();
6261
6352
  }
6262
6353
  // ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
6263
6354
  recover() {
6264
- if (existsSync5(this.mapPath)) {
6355
+ if (existsSync6(this.mapPath)) {
6265
6356
  try {
6266
6357
  const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
6267
6358
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
6268
6359
  } catch {
6269
6360
  }
6270
6361
  }
6271
- if (existsSync5(this.outboxPath)) {
6362
+ if (existsSync6(this.outboxPath)) {
6272
6363
  const txt = readFileSync4(this.outboxPath, "utf8");
6273
6364
  for (const line of txt.split("\n")) {
6274
6365
  const s = line.trim();
@@ -6289,7 +6380,7 @@ var SwarmClient = class _SwarmClient {
6289
6380
  // eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
6290
6381
  // переприсваиваем локально (id событий — только порядок, не корреляция с продом).
6291
6382
  replayGlobalEvents() {
6292
- if (!existsSync5(this.stormPath)) return;
6383
+ if (!existsSync6(this.stormPath)) return;
6293
6384
  let txt;
6294
6385
  try {
6295
6386
  txt = readFileSync4(this.stormPath, "utf8");
@@ -6554,6 +6645,24 @@ var SwarmClient = class _SwarmClient {
6554
6645
  const viaProd = [...this.runs.values()].find((r) => r.runKey === `p:${prodId}` || this.prodIdByKey.get(r.runKey) === prodId);
6555
6646
  return viaProd ? this.mat(viaProd) : null;
6556
6647
  }
6648
+ /**
6649
+ * TASWM-419: адреса каталога run-log'ов прогона. КАНОНИЧЕСКИЙ — прод-вида `p:<cuid>`, как
6650
+ * только cuid известен: он один и тот же во всех жизнях демона, а локальный uuid прогона
6651
+ * после рестарта уже не воскресает (гидрированный прогон приезжает под прод-ключом).
6652
+ * Дальше — псевдонимы: uuid, под которым каталог мог завестись в окно между спавном и
6653
+ * первым флашем в прод. По ним каталог ищется, чтобы запуски одного прогона не расползлись
6654
+ * по двум каталогам (иначе cost недосчитал бы часть, а хвост показал бы не тот запуск).
6655
+ * Номер прогона в адресе не участвует вовсе — он и есть источник «лога чужого прогона».
6656
+ */
6657
+ runLogKeys(id) {
6658
+ const key = this.runs.get(id)?.runKey;
6659
+ if (!key) return [];
6660
+ const prodId = key.startsWith("p:") ? key.slice(2) : this.prodIdByKey.get(key);
6661
+ if (!prodId) return [key];
6662
+ const canonical = `p:${prodId}`;
6663
+ const aliases = [key, ...[...this.prodIdByKey.entries()].filter(([, v]) => v === prodId).map(([k]) => k)];
6664
+ return [canonical, ...new Set(aliases.filter((k) => k !== canonical))];
6665
+ }
6557
6666
  listRuns(status) {
6558
6667
  const all = [...this.runs.values()].filter((r) => status ? r.status === status : true);
6559
6668
  all.sort((a, b) => b.id - a.id);
@@ -6670,16 +6779,25 @@ var SwarmClient = class _SwarmClient {
6670
6779
  /**
6671
6780
  * TASWM-361: вердикт по каждой зависимости — см. докблок SwarmStore.depVerdicts.
6672
6781
  *
6673
- * АСИММЕТРИЯ ЗЕРКАЛА И ПРОДА, и почему она здесь намеренная. Зеркало — ЧАСТИЧНАЯ выборка
6674
- * (только прогоны этой машины, только окно гидрации), и по частичной выборке два вывода
6675
- * стоят разного:
6676
- * • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он не
6677
- * портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
6678
- * • «живых прогонов слага не осталось» — вывод от ОТСУТСТВИЯ: невидимый зеркалу прогон
6679
- * на соседней машине как раз и может быть живым. Поэтому «исчерпана неуспешно»
6680
- * принимаем ТОЛЬКО из ответа прода (refreshDeps) — он видит всех.
6681
- * Прод недоступен → вердикта нет → прогон честно ждёт следующего окна: лишняя минута
6682
- * очереди дешевле дрона, поднятого поверх работающего блокера.
6782
+ * ИСТОЧНИК ПРАВДЫ — ПРОД (TASWM-399), зеркало здесь только быстрый путь. Зеркало —
6783
+ * ЧАСТИЧНАЯ выборка (прогоны этой машины, окно гидрации), и по частичной выборке два
6784
+ * вывода стоят разного:
6785
+ * • «есть смерженный прогон слага» — вывод от НАЛИЧИЯ: чего зеркало не видит, того он
6786
+ * не портит, поэтому успех подтверждаем и локально (так было до TASWM-361);
6787
+ * • всё остальное — вывод от ОТСУТСТВИЯ, и его принимаем ТОЛЬКО из ответа прода
6788
+ * (refreshDeps): невидимый зеркалу прогон на соседней машине как раз и может быть
6789
+ * живым, а закрытый с прода — давно смерженным.
6790
+ *
6791
+ * ССЫЛКА — НЕ ОБЯЗАТЕЛЬНО СЛАГ. В `dependsOn` лежит чаще всего cuid ПРОГОНА (так пишет
6792
+ * overmind), реже cuid задачи, и только дев-спавн `--after` пишет человеко-слаг. Быстрый
6793
+ * путь по зеркалу сравнивает `task_slug`, то есть срабатывает лишь на третьей форме —
6794
+ * это нормально: остальные две резолвит прод. А вот считать «раз в зеркале не нашёл —
6795
+ * значит не готово» было корнем TASWM-399.
6796
+ *
6797
+ * Прода ещё не спрашивали (первый тик) или он моргнул → `pending`: прогон честно ждёт
6798
+ * следующего окна. Лишняя минута очереди дешевле дрона, поднятого поверх работающего
6799
+ * блокера. `unknown` (блокера НЕ ЗНАЕТ И ПРОД) ставит только ответ прода — местным
6800
+ * незнанием такой вывод не делается.
6683
6801
  */
6684
6802
  depVerdicts(depends_on) {
6685
6803
  if (!depends_on || depends_on.length === 0) return [];
@@ -6689,57 +6807,89 @@ var SwarmClient = class _SwarmClient {
6689
6807
  doneInMirror.add(r.task_slug);
6690
6808
  }
6691
6809
  }
6692
- return depends_on.map((slug) => {
6693
- if (doneInMirror.has(slug)) {
6694
- return { slug, outcome: "done", met: true, status: "done", name: null, stopReason: null };
6810
+ return depends_on.map((ref) => {
6811
+ if (doneInMirror.has(ref)) {
6812
+ return { slug: ref, outcome: "done", met: true, status: "done", name: null, stopReason: null };
6813
+ }
6814
+ const fromProd = this.depsProd.get(ref);
6815
+ if (fromProd) {
6816
+ if (fromProd.outcome === "unknown" && this.knowsDepRef(ref)) {
6817
+ return { slug: this.depSlug.get(ref) ?? ref, outcome: "pending", met: false, status: null, name: null, stopReason: null };
6818
+ }
6819
+ return { slug: this.depSlug.get(ref) ?? ref, ...fromProd };
6695
6820
  }
6696
- const fromProd = this.depsProd.get(slug);
6697
- if (fromProd) return { slug, ...fromProd };
6698
- return { slug, outcome: "pending", met: false, status: null, name: null, stopReason: null };
6821
+ return { slug: ref, outcome: "pending", met: false, status: null, name: null, stopReason: null };
6699
6822
  });
6700
6823
  }
6701
6824
  /**
6702
- * TASWM-300: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
6825
+ * Знает ли ЗЕРКАЛО хоть один прогон по ссылке `dependsOn` (TASWM-399). Ссылка бывает
6826
+ * слагом, cuid'ом задачи и cuid'ом прогона — последний сверяем с прод-id, как это
6827
+ * делают runByProdId/refreshRun. Служебные сабраны (ревьюер/резолвер/верификатор) не
6828
+ * в счёт: их существование не означает, что жив сам блокер.
6829
+ */
6830
+ knowsDepRef(ref) {
6831
+ for (const r of this.runs.values()) {
6832
+ if (r.resolves_run_id != null || r.reviews_run_id != null || r.verifies_run_id != null) continue;
6833
+ if (r.task_slug === ref) return true;
6834
+ if (r.runKey === `p:${ref}` || this.prodIdByKey.get(r.runKey) === ref) return true;
6835
+ }
6836
+ return false;
6837
+ }
6838
+ /**
6839
+ * TASWM-300 → TASWM-399: спросить ПРОД про зависимости — см. докблок SwarmStore.refreshDeps.
6840
+ *
6841
+ * Почему зеркала мало. В нём лежат только прогоны ЭТОЙ машины (гейт isMine, hydrateRun) и
6842
+ * только те, что попали в окно гидрации (`swarm_list_runs limit`, сортировка по updatedAt
6843
+ * desc — сутки работы роя). Зависимость, закрытая на соседней машине, обнулившаяся при
6844
+ * рестарте демона или просто позавчерашняя, в зеркало не попадала — и `dependsOn` держал
6845
+ * прогон в очереди вечно.
6703
6846
  *
6704
- * Почему зеркала было мало. В нём лежат только прогоны ЭТОЙ машины (гейт isMine,
6705
- * hydrateRun) и только те, что попали в окно гидрации (`swarm_list_runs limit`, сортировка
6706
- * по updatedAt desc — сутки работы роя). Зависимость, закрытая на соседней машине или
6707
- * просто позавчера, в зеркало не попадала — и `--after` держал прогон в очереди вечно
6708
- * (04.08: снимали руками).
6847
+ * ЧЕМ СПРАШИВАЕМ И ПОЧЕМУ НЕ `swarm_list_runs` (корень TASWM-399). До этой задачи здесь
6848
+ * шёл `swarm_list_runs { taskId: <ref> }` — то есть ссылка трактовалась как ЗАДАЧА. Но в
6849
+ * `dependsOn` чаще всего лежит cuid ПРОГОНА: `resolveTaskRef` его задачей не признаёт,
6850
+ * ответ приходит `isError`, вердикт не появляется НИКОГДА, и прогон стоит молча при
6851
+ * свободной машине (12–13.08 — пять остановок роя за сутки). `swarm_deps_status` понимает
6852
+ * все три формы ссылки и отвечает по единому правилу роя (`judgeDep` на прод-стороне).
6709
6853
  *
6710
6854
  * Кеш. «Смержено» МОНОТОННО: прогон, ставший done, назад не откатывается, — поэтому
6711
6855
  * положительный ответ помним навсегда и больше не спрашиваем. Остальные вердикты (включая
6712
- * «исчерпана неуспешно», TASWM-361 — блокер могут переиграть новым прогоном) переспрашиваем
6713
- * не чаще DEPS_TTL_MS: тик идёт раз в ~4с, а ждущих зависимость прогонов может быть
6714
- * много — без TTL это был бы поход в прод на каждый слаг каждый тик.
6856
+ * «исчерпана неуспешно» и «блокера не знает никто») переспрашиваем не чаще DEPS_TTL_MS:
6857
+ * оператор вправе переиграть блокер новым прогоном, а тик идёт раз в ~4 с — без TTL это
6858
+ * был бы поход в прод на каждую ссылку каждый тик.
6859
+ *
6860
+ * Одним вызовом на все просроченные ссылки, а не по вызову на ссылку: очередь ждущих
6861
+ * бывает в десятки прогонов, и N запросов на тик — это тот же самый поход в прод, только
6862
+ * дороже.
6715
6863
  */
6716
- async refreshDeps(slugs, nowMs = Date.now()) {
6717
- for (const slug of new Set(slugs)) {
6718
- if (!slug || this.depsProd.get(slug)?.outcome === "done") continue;
6719
- const asked = this.depsAskedAt.get(slug);
6720
- if (asked !== void 0 && nowMs - asked < _SwarmClient.DEPS_TTL_MS) continue;
6721
- this.depsAskedAt.set(slug, nowMs);
6722
- let res;
6723
- try {
6724
- res = await this.call("swarm_list_runs", { taskId: slug });
6725
- } catch {
6726
- this.depsAskedAt.delete(slug);
6727
- continue;
6728
- }
6729
- if (res?.isError) continue;
6730
- const runs = res.structuredContent?.runs ?? [];
6731
- const facts = runs.slice().sort((a, b) => String(a.createdAt ?? "").localeCompare(String(b.createdAt ?? ""))).map((r) => ({
6732
- status: String(r.status ?? ""),
6733
- // Служебный сабран под тем же слагом (резолвер конфликта / ревьюер): его финал —
6734
- // не финал задачи. Резолвер вычёркивался и до TASWM-361, ревьюер добавлен вместе
6735
- // с расширением критерия — иначе остановленный ревьюер закрывал бы зависимость.
6736
- service: Boolean(r.resolvesRunId) || Boolean(r.reviewsRunId) || Boolean(r.verifiesRunId),
6737
- // В проде имя дрона — колонка droneName (см. hydrateRun); `name` терпим как
6738
- // запасной вариант ради фикстур и старых ответов.
6739
- name: r.droneName ?? r.name ?? null,
6740
- stopReason: r.stopReason ?? null
6741
- }));
6742
- this.depsProd.set(slug, judgeDep(facts));
6864
+ async refreshDeps(refs, nowMs = Date.now()) {
6865
+ const ask = [...new Set(refs)].filter((ref) => {
6866
+ if (!ref || this.depsProd.get(ref)?.outcome === "done") return false;
6867
+ const asked = this.depsAskedAt.get(ref);
6868
+ return asked === void 0 || nowMs - asked >= _SwarmClient.DEPS_TTL_MS;
6869
+ });
6870
+ if (!ask.length) return;
6871
+ for (const ref of ask) this.depsAskedAt.set(ref, nowMs);
6872
+ let res;
6873
+ try {
6874
+ res = await this.call("swarm_deps_status", { refs: ask });
6875
+ } catch {
6876
+ for (const ref of ask) this.depsAskedAt.delete(ref);
6877
+ return;
6878
+ }
6879
+ if (res?.isError) return;
6880
+ const deps = res.structuredContent?.deps ?? [];
6881
+ for (const d of deps) {
6882
+ const ref = typeof d.ref === "string" ? d.ref : null;
6883
+ if (!ref) continue;
6884
+ const outcome = String(d.outcome ?? "pending");
6885
+ this.depsProd.set(ref, {
6886
+ outcome,
6887
+ met: Boolean(d.met),
6888
+ status: d.status ?? null,
6889
+ name: d.name ?? null,
6890
+ stopReason: d.stopReason ?? null
6891
+ });
6892
+ if (typeof d.slug === "string" && d.slug) this.depSlug.set(ref, d.slug);
6743
6893
  }
6744
6894
  }
6745
6895
  /**
@@ -7058,7 +7208,7 @@ var SwarmClient = class _SwarmClient {
7058
7208
  * транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
7059
7209
  */
7060
7210
  reconcileRuntime(opts) {
7061
- const wtExists = opts.worktreeExists ?? existsSync5;
7211
+ const wtExists = opts.worktreeExists ?? existsSync6;
7062
7212
  const pidByWorktree = /* @__PURE__ */ new Map();
7063
7213
  try {
7064
7214
  for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
@@ -7547,6 +7697,12 @@ var SwarmClient = class _SwarmClient {
7547
7697
  ...typeof h.paused === "boolean" ? { paused: h.paused } : {},
7548
7698
  ...typeof h.pausedAt === "string" ? { pausedAt: h.pausedAt } : {},
7549
7699
  ...typeof h.pauseReason === "string" ? { pauseReason: h.pauseReason } : {},
7700
+ // TASWM-424: «доступ отозван» — состояние ровно того же рода, что пауза, и
7701
+ // формулировка у него ОДНА на все поверхности (HOST_ACCESS_REVOKED_TEXT):
7702
+ // `hostLines` зовёт тот же `hostStateText`, что и парк машин в /swarm. Без
7703
+ // этого поля `swarm hosts` показывал отвергнутую машину просто молчащей —
7704
+ // то самое различие, ради которого флаг и заводили (TASWM-324).
7705
+ ...typeof h.accessRevoked === "boolean" ? { accessRevoked: h.accessRevoked } : {},
7550
7706
  // Показаний нет (старый прод, машина со старым демоном) — ключа не заводим вовсе,
7551
7707
  // ровно как с паузой выше: «не знаем» и так значит «не знаем».
7552
7708
  ...resources ? { resources } : {}
@@ -7632,9 +7788,18 @@ var SwarmClient = class _SwarmClient {
7632
7788
  * ВСЕГДА (в т.ч. `storm:false`), как паузу: это состояние, а не команда, и молчание тут
7633
7789
  * означало бы «прод помнит вчерашний шторм», когда рой давно работает.
7634
7790
  *
7791
+ * TASWM-402: тем же пульсом уезжают КОРНИ WORKTREE'ЕВ этой машины (`worktreeRoots` —
7792
+ * `owner/repo` → `<worktreesDir>/<repo-basename>`). Это НЕ рантайм и правило «pid и путь
7793
+ * дрона в прод не уходят» не трогает: корень — статичная конфигурация машины, того же
7794
+ * рода, что реестр репо и ёмкость, он не зависит ни от прогона, ни от живости процесса и
7795
+ * не протухает между пульсами. Прод склеивает `<корень>/<слаг>` сам — и отвечает человеку
7796
+ * на вопрос «куда идти руками», когда команда машине не доиграла (а в этом случае, по
7797
+ * определению, спросить рантайм у молчащей машины уже нельзя). Не передали корень (не
7798
+ * знаем своего каталога) → ключа нет, прод прошлое не трогает — как с capacity/resources.
7799
+ *
7635
7800
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
7636
7801
  */
7637
- async heartbeat(daemonVersion, pause, capacity, resources, storm, signIn) {
7802
+ async heartbeat(daemonVersion, pause, capacity, resources, storm, worktreesDir, signIn) {
7638
7803
  try {
7639
7804
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
7640
7805
  if (daemonVersion) args.daemonVersion = daemonVersion;
@@ -7652,6 +7817,11 @@ var SwarmClient = class _SwarmClient {
7652
7817
  args.storm = storm.active;
7653
7818
  if (storm.reason) args.stormReason = storm.reason;
7654
7819
  }
7820
+ if (worktreesDir) {
7821
+ const roots = {};
7822
+ for (const [full, path] of this.repoPathByFullName) roots[full] = droneWorktreeRoot(worktreesDir, path);
7823
+ if (Object.keys(roots).length) args.worktreeRoots = roots;
7824
+ }
7655
7825
  if (this.host) {
7656
7826
  args.hostId = this.host.hostId;
7657
7827
  args.hostName = this.host.hostName;
@@ -7800,10 +7970,20 @@ var SwarmClient = class _SwarmClient {
7800
7970
  * именно путь. Не нашли в реестре (репо не наш / ручной `--repo <path>` спавн, где
7801
7971
  * repo — ВСЕГДА уже путь, старый формат) → возвращаем как есть без изменений
7802
7972
  * (обратная совместимость: раньше pr.repo ВСЕГДА был локальным путём).
7973
+ *
7974
+ * РЕГИСТР НЕ РАЗЛИЧАЕМ (TASWM-422). Ключи карты приведены к нижнему регистру
7975
+ * (parseRepoFullName — «GitHub сам не различает case в owner/repo»), а прод присылает
7976
+ * `run.repo` тем регистром, каким репозиторий записан в проекте. 17.08 это разошлось:
7977
+ * машина объявляла `ilyasmurov/ansamblist`, задача пришла с `ilyasmurov/Ansamblist`,
7978
+ * `Map.get` промахнулся — и вместо пути в прогон уехало полное имя репозитория. Дальше
7979
+ * git-операции шли в несуществующий каталог, прогон умирал за 5 секунд, а задача
7980
+ * возвращалась в очередь и бралась снова: пять павших прогонов за пять минут.
7981
+ * Возвращаем ИСХОДНУЮ строку, а не приведённую: промах бывает и на локальном пути
7982
+ * (ручной спавн), а путь регистр различает — вернув lowercase, мы сломали бы его.
7803
7983
  */
7804
7984
  resolveRepo(raw) {
7805
7985
  if (!raw) return "";
7806
- return this.repoPathByFullName.get(raw) ?? raw;
7986
+ return this.repoPathByFullName.get(raw) ?? this.repoPathByFullName.get(raw.toLowerCase()) ?? raw;
7807
7987
  }
7808
7988
  /**
7809
7989
  * TASWM-281: МОЯ ли это работа. Три исхода:
@@ -8244,20 +8424,20 @@ init_config();
8244
8424
  init_heartbeat();
8245
8425
 
8246
8426
  // src/logRotate.ts
8247
- import { copyFileSync, renameSync as renameSync4, statSync as statSync4, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
8427
+ import { copyFileSync, renameSync as renameSync5, statSync as statSync5, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync7 } from "fs";
8248
8428
  var realIo = {
8249
8429
  size: (path) => {
8250
8430
  try {
8251
- return statSync4(path).size;
8431
+ return statSync5(path).size;
8252
8432
  } catch {
8253
8433
  return null;
8254
8434
  }
8255
8435
  },
8256
8436
  copy: copyFileSync,
8257
- rename: renameSync4,
8437
+ rename: renameSync5,
8258
8438
  truncate: (path) => truncateSync(path, 0),
8259
8439
  unlink: unlinkSync2,
8260
- exists: existsSync6
8440
+ exists: existsSync7
8261
8441
  };
8262
8442
  var realOwnerIo = {
8263
8443
  statFd: (fd) => {
@@ -8270,7 +8450,7 @@ var realOwnerIo = {
8270
8450
  },
8271
8451
  statPath: (path) => {
8272
8452
  try {
8273
- const s = statSync4(path);
8453
+ const s = statSync5(path);
8274
8454
  return { dev: s.dev, ino: s.ino };
8275
8455
  } catch {
8276
8456
  return null;
@@ -8342,12 +8522,12 @@ async function buildRepoRegistry(paths) {
8342
8522
 
8343
8523
  // src/repoMemory.ts
8344
8524
  import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
8345
- import { join as join7 } from "path";
8525
+ import { join as join8 } from "path";
8346
8526
  var FILE = "repos.json";
8347
8527
  function loadRememberedRepos(home) {
8348
8528
  let raw;
8349
8529
  try {
8350
- raw = readFileSync6(join7(home, FILE), "utf8");
8530
+ raw = readFileSync6(join8(home, FILE), "utf8");
8351
8531
  } catch {
8352
8532
  return [];
8353
8533
  }
@@ -8362,7 +8542,7 @@ function loadRememberedRepos(home) {
8362
8542
  function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
8363
8543
  const clean = paths.filter((p) => p.trim().length > 0);
8364
8544
  if (clean.length === 0) return;
8365
- writeFileSync6(join7(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
8545
+ writeFileSync6(join8(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
8366
8546
  `);
8367
8547
  }
8368
8548
  function pickRepoPaths(args) {
@@ -8377,12 +8557,12 @@ init_src();
8377
8557
  init_config();
8378
8558
  init_linkFailure();
8379
8559
  import { homedir as homedir3 } from "os";
8380
- import { readFileSync as readFileSync9, renameSync as renameSync5, writeFileSync as writeFileSync8 } from "fs";
8381
- import { isAbsolute, join as join10, resolve as resolve2 } from "path";
8560
+ import { readFileSync as readFileSync9, renameSync as renameSync6, writeFileSync as writeFileSync8 } from "fs";
8561
+ import { isAbsolute, join as join11, resolve as resolve2 } from "path";
8382
8562
 
8383
8563
  // src/repoScan.ts
8384
- import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync5 } from "fs";
8385
- import { join as join9 } from "path";
8564
+ import { readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync6 } from "fs";
8565
+ import { join as join10 } from "path";
8386
8566
  var SKIP = /* @__PURE__ */ new Set([
8387
8567
  "node_modules",
8388
8568
  "vendor",
@@ -8412,7 +8592,7 @@ function parseOriginFromGitConfig(text) {
8412
8592
  }
8413
8593
  function originOf(dir) {
8414
8594
  try {
8415
- return parseOriginFromGitConfig(readFileSync8(join9(dir, ".git", "config"), "utf8"));
8595
+ return parseOriginFromGitConfig(readFileSync8(join10(dir, ".git", "config"), "utf8"));
8416
8596
  } catch {
8417
8597
  return null;
8418
8598
  }
@@ -8441,15 +8621,15 @@ function walk(dir, depth, maxDepth, found) {
8441
8621
  }
8442
8622
  let entries;
8443
8623
  try {
8444
- entries = readdirSync5(dir);
8624
+ entries = readdirSync6(dir);
8445
8625
  } catch {
8446
8626
  return;
8447
8627
  }
8448
8628
  for (const name of entries) {
8449
8629
  if (name.startsWith(".") || SKIP.has(name)) continue;
8450
- const child = join9(dir, name);
8630
+ const child = join10(dir, name);
8451
8631
  try {
8452
- if (!statSync5(child).isDirectory()) continue;
8632
+ if (!statSync6(child).isDirectory()) continue;
8453
8633
  } catch {
8454
8634
  continue;
8455
8635
  }
@@ -8469,7 +8649,7 @@ function defaultScanRoots(home, knownPaths = []) {
8469
8649
  if (parent && parent !== p) add(parent);
8470
8650
  }
8471
8651
  for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
8472
- add(join9(home, name));
8652
+ add(join10(home, name));
8473
8653
  }
8474
8654
  return out;
8475
8655
  }
@@ -8483,7 +8663,7 @@ function resolveScanRoots(raw, home = homedir3(), knownPaths = []) {
8483
8663
  for (const part of v.split(",")) {
8484
8664
  const p = part.trim();
8485
8665
  if (!p) continue;
8486
- const abs = p === "~" ? home : p.startsWith("~/") ? join10(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
8666
+ const abs = p === "~" ? home : p.startsWith("~/") ? join11(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
8487
8667
  if (!out.includes(abs)) out.push(abs);
8488
8668
  }
8489
8669
  return out;
@@ -8548,6 +8728,28 @@ function describeReposFailure(kind, detail) {
8548
8728
  function emptyRepoMapState(roots) {
8549
8729
  return { clones: [], missing: [], roots: [...roots], at: null, unknown: true };
8550
8730
  }
8731
+ var SCAN_REASON_TEXT = {
8732
+ "list-unknown": "\u043F\u0440\u043E\u0434 \u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043B, \u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443 \u0440\u043E\u044F \u2014 \u0441\u043C\u043E\u0442\u0440\u044E \u0441\u0430\u043C, \u0447\u0435\u0433\u043E \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 \u043F\u0440\u0438\u0431\u0430\u0432\u0438\u043B\u043E\u0441\u044C",
8733
+ "wanted-missing": "\u0438\u0437 \u0441\u043F\u0438\u0441\u043A\u0430 \u0440\u043E\u044F \u0447\u0435\u0433\u043E-\u0442\u043E \u043D\u0435\u0442 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435",
8734
+ "cache-expired": "\u043A\u044D\u0448 \u043A\u0430\u0440\u0442\u044B \u043F\u0440\u043E\u0442\u0443\u0445"
8735
+ };
8736
+ var REPO_SCAN_TTL_MS = 24 * 60 * 6e4;
8737
+ var UNKNOWN_LIST_SCAN_TTL_MS = 60 * 6e4;
8738
+ function isScanStale(scannedAt, ttlMs, now = Date.now()) {
8739
+ if (!scannedAt) return true;
8740
+ const t = stampMs(scannedAt);
8741
+ if (!Number.isFinite(t)) return true;
8742
+ return now - t >= ttlMs;
8743
+ }
8744
+ function decideScan(opts) {
8745
+ if (!opts.known) {
8746
+ const ttl = Math.min(opts.ttlMs, UNKNOWN_LIST_SCAN_TTL_MS);
8747
+ return isScanStale(opts.scannedAt, ttl, opts.now) ? { scan: true, reason: "list-unknown" } : { scan: false, reason: null };
8748
+ }
8749
+ if (opts.missingWanted > 0) return { scan: true, reason: "wanted-missing" };
8750
+ if (isScanStale(opts.scannedAt, opts.ttlMs, opts.now)) return { scan: true, reason: "cache-expired" };
8751
+ return { scan: false, reason: null };
8752
+ }
8551
8753
  async function refreshRepoMap(opts) {
8552
8754
  const log = opts.log ?? (() => {
8553
8755
  });
@@ -8561,14 +8763,21 @@ async function refreshRepoMap(opts) {
8561
8763
  clones.set(repoFullName, { repoFullName, path, source, alternatives });
8562
8764
  };
8563
8765
  for (const e of opts.configured) take(e.repoFullName.toLowerCase(), e.path, "configured");
8564
- for (const e of readRepoMapCache(opts.cachePath)) {
8766
+ const cache = loadRepoMapCache(opts.cachePath);
8767
+ for (const e of cache.entries) {
8565
8768
  const name = e.repoFullName.toLowerCase();
8566
8769
  if (clones.has(name)) continue;
8567
8770
  if (repoAt(e.path) === name) take(name, e.path, "cache");
8568
8771
  }
8569
8772
  const wantedNames = wanted ? normalizeFullNames(wanted.map((r) => r.repoFullName)) : [];
8570
- const needScan = wanted ? wantedNames.some((n) => !clones.has(n)) : clones.size === 0;
8571
- if (needScan) {
8773
+ const decision = decideScan({
8774
+ known: !!wanted,
8775
+ missingWanted: wantedNames.filter((n) => !clones.has(n)).length,
8776
+ scannedAt: cache.scannedAt,
8777
+ ttlMs: opts.scanTtlMs ?? REPO_SCAN_TTL_MS
8778
+ });
8779
+ let scannedAt = cache.scannedAt;
8780
+ if (decision.scan) {
8572
8781
  const startedAt = Date.now();
8573
8782
  const scanned = scan(opts.roots, opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {});
8574
8783
  let added = 0;
@@ -8577,9 +8786,10 @@ async function refreshRepoMap(opts) {
8577
8786
  take(full, paths[0], "scan", paths.slice(1));
8578
8787
  added++;
8579
8788
  }
8580
- if (added > 0) {
8581
- log(`[overmind] \u043D\u0430\u0448\u0451\u043B \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 ${added} \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439(\u0435\u0432) \u0441\u043A\u0430\u043D\u043E\u043C \u0437\u0430 ${Date.now() - startedAt}\u043C\u0441`);
8582
- }
8789
+ scannedAt = stamp();
8790
+ log(
8791
+ `[overmind] \u043F\u0440\u043E\u0448\u0451\u043B \u043F\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C (${SCAN_REASON_TEXT[decision.reason]}): ${added > 0 ? `\u043D\u0430\u0448\u0451\u043B \u0435\u0449\u0451 ${added}` : "\u043D\u043E\u0432\u043E\u0433\u043E \u043D\u0435 \u043D\u0430\u0448\u0451\u043B"}, \u0432\u0441\u0435\u0433\u043E \u043A\u043B\u043E\u043D\u043E\u0432 ${clones.size}, \u0437\u0430 ${Date.now() - startedAt}\u043C\u0441. \u0418\u0441\u043A\u0430\u043B \u0432: ${opts.roots.length ? opts.roots.slice(0, 6).join(", ") : "\u043D\u0438\u0433\u0434\u0435 \u2014 \u043F\u043E\u0438\u0441\u043A \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D (SWARM_SCAN_ROOTS)"}`
8792
+ );
8583
8793
  }
8584
8794
  const registry = [...clones.values()].map((c) => ({
8585
8795
  repoFullName: c.repoFullName,
@@ -8587,10 +8797,12 @@ async function refreshRepoMap(opts) {
8587
8797
  }));
8588
8798
  writeRepoMapCache(
8589
8799
  opts.cachePath,
8590
- [...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path }))
8800
+ [...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path })),
8801
+ scannedAt
8591
8802
  );
8592
8803
  if (!answer.ok) {
8593
8804
  log(describeReposFailure(answer.kind, answer.detail));
8805
+ log(`[overmind] \u0438\u0442\u043E\u0433: \u043A\u043B\u043E\u043D\u043E\u0432 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E ${clones.size}, \u0441\u043F\u0438\u0441\u043E\u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0440\u043E\u044F \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D (\u0441\u043C. \u0441\u0442\u0440\u043E\u043A\u0443 \u0432\u044B\u0448\u0435)`);
8594
8806
  return registry;
8595
8807
  }
8596
8808
  opts.state.unknown = false;
@@ -8602,7 +8814,7 @@ async function refreshRepoMap(opts) {
8602
8814
  log("[overmind] \u0443 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043D\u0435\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 \u0441 \u0432\u043A\u043B\u044E\u0447\u0451\u043D\u043D\u044B\u043C \u0440\u043E\u0435\u043C \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u043F\u043E \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u043C\u0443 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435");
8603
8815
  return registry;
8604
8816
  }
8605
- log(`[overmind] \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0440\u043E\u044F (${wantedNames.length}), \u043A\u0430\u0440\u0442\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435:`);
8817
+ log(`[overmind] ${describeMapSummary(opts.state, opts.roots)}`);
8606
8818
  for (const c of opts.state.clones) log(`[overmind] ${describeClone(c)}`);
8607
8819
  const prefixOf = new Map(answer.repos.map((r) => [r.repoFullName, r.taskPrefix]));
8608
8820
  for (const name of opts.state.missing) {
@@ -8611,6 +8823,11 @@ async function refreshRepoMap(opts) {
8611
8823
  }
8612
8824
  return registry;
8613
8825
  }
8826
+ function describeMapSummary(state, roots) {
8827
+ const found = state.clones.length;
8828
+ const where = roots.length ? roots.slice(0, 6).join(", ") : "\u043F\u043E\u0438\u0441\u043A \u043F\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D (SWARM_SCAN_ROOTS)";
8829
+ return `\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0440\u043E\u044F (${found + state.missing.length}): \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 ${found}, \u0431\u0435\u0437 \u043A\u043B\u043E\u043D\u0430 ${state.missing.length}. \u0418\u0441\u043A\u0430\u043B \u0432: ${where}`;
8830
+ }
8614
8831
  function describeClone(c) {
8615
8832
  const how = c.source === "configured" ? "\u043F\u0443\u0442\u044C \u0437\u0430\u0434\u0430\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C" : c.source === "cache" ? "\u0438\u0437 \u043A\u044D\u0448\u0430 \u043C\u0430\u0448\u0438\u043D\u044B" : "\u043D\u0430\u0439\u0434\u0435\u043D \u0441\u043A\u0430\u043D\u043E\u043C";
8616
8833
  const alt = c.alternatives.length ? ` \u26A0 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u0435\u0449\u0451 ${c.alternatives.length}: ${c.alternatives.join(", ")} \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u0432 \u043F\u0435\u0440\u0432\u043E\u043C \u043F\u043E \u043F\u043E\u0440\u044F\u0434\u043A\u0443 \u043A\u043E\u0440\u043D\u0435\u0439` : "";
@@ -8631,40 +8848,43 @@ function repoMapRows(state) {
8631
8848
  ...state.missing.map((repoFullName) => ({ repoFullName, path: null, source: null, alternatives: [] }))
8632
8849
  ];
8633
8850
  }
8634
- function readRepoMapCache(path) {
8851
+ function loadRepoMapCache(path) {
8635
8852
  try {
8636
8853
  const raw = JSON.parse(readFileSync9(path, "utf8"));
8637
- if (!Array.isArray(raw?.entries)) return [];
8638
- return raw.entries.filter(
8639
- (e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
8640
- );
8854
+ if (!Array.isArray(raw?.entries)) return { scannedAt: null, entries: [] };
8855
+ return {
8856
+ scannedAt: typeof raw.scannedAt === "string" && raw.scannedAt ? raw.scannedAt : null,
8857
+ entries: raw.entries.filter(
8858
+ (e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
8859
+ )
8860
+ };
8641
8861
  } catch {
8642
- return [];
8862
+ return { scannedAt: null, entries: [] };
8643
8863
  }
8644
8864
  }
8645
- function writeRepoMapCache(path, entries) {
8865
+ function writeRepoMapCache(path, entries, scannedAt = stamp()) {
8646
8866
  try {
8647
8867
  const tmp = `${path}.tmp`;
8648
- writeFileSync8(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
8649
- renameSync5(tmp, path);
8868
+ writeFileSync8(tmp, JSON.stringify({ at: stamp(), scannedAt, entries }, null, 2));
8869
+ renameSync6(tmp, path);
8650
8870
  } catch {
8651
8871
  }
8652
8872
  }
8653
8873
 
8654
8874
  // src/hostIdentity.ts
8655
8875
  import { hostname } from "os";
8656
- import { existsSync as existsSync7, readFileSync as readFileSync10, renameSync as renameSync6, writeFileSync as writeFileSync9 } from "fs";
8657
- import { join as join11 } from "path";
8876
+ import { existsSync as existsSync8, readFileSync as readFileSync10, renameSync as renameSync7, writeFileSync as writeFileSync9 } from "fs";
8877
+ import { join as join12 } from "path";
8658
8878
  import { randomUUID as randomUUID2 } from "crypto";
8659
8879
  function hostIdentityPath(home) {
8660
- return join11(home, "host.json");
8880
+ return join12(home, "host.json");
8661
8881
  }
8662
8882
  function loadHostIdentity(deps) {
8663
- const exists = deps.exists ?? existsSync7;
8883
+ const exists = deps.exists ?? existsSync8;
8664
8884
  const readFile2 = deps.readFile ?? ((p) => readFileSync10(p, "utf8"));
8665
8885
  const writeFile3 = deps.writeFile ?? ((p, data) => {
8666
8886
  writeFileSync9(`${p}.tmp`, data);
8667
- renameSync6(`${p}.tmp`, p);
8887
+ renameSync7(`${p}.tmp`, p);
8668
8888
  });
8669
8889
  const host = deps.hostname ?? hostname;
8670
8890
  const newId = deps.newId ?? randomUUID2;
@@ -8846,15 +9066,15 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
8846
9066
 
8847
9067
  // src/launcher.ts
8848
9068
  import { spawn } from "child_process";
8849
- import { closeSync as closeSync2, existsSync as existsSync8, mkdirSync as mkdirSync6, openSync as openSync2, readdirSync as readdirSync7 } from "fs";
9069
+ import { closeSync as closeSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, openSync as openSync2, readdirSync as readdirSync8 } from "fs";
8850
9070
  import { createRequire } from "module";
8851
9071
  import { homedir as homedir4 } from "os";
8852
- import { dirname as dirname2, join as join13 } from "path";
9072
+ import { dirname as dirname2, join as join14 } from "path";
8853
9073
  import { fileURLToPath } from "url";
8854
9074
 
8855
9075
  // src/droneHome.ts
8856
- import { lstatSync, mkdirSync as mkdirSync5, readdirSync as readdirSync6, rmSync, statSync as statSync6, symlinkSync as symlinkSync2 } from "fs";
8857
- import { join as join12, relative, sep } from "path";
9076
+ import { lstatSync, mkdirSync as mkdirSync6, readdirSync as readdirSync7, rmSync, statSync as statSync7, symlinkSync as symlinkSync2 } from "fs";
9077
+ import { join as join13, relative, sep } from "path";
8858
9078
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
8859
9079
  function droneHomeDeny(env = process.env) {
8860
9080
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -8872,29 +9092,29 @@ function buildDroneHome(opts) {
8872
9092
  const { realHome, droneHome } = opts;
8873
9093
  const deny = opts.deny ?? droneHomeDeny();
8874
9094
  const platform = opts.platform ?? process.platform;
8875
- mkdirSync5(droneHome, { recursive: true, mode: 448 });
8876
- const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
9095
+ mkdirSync6(droneHome, { recursive: true, mode: 448 });
9096
+ const wanted = new Set(mirroredEntries(readdirSync7(realHome), deny, platform));
8877
9097
  const rel = relative(realHome, droneHome);
8878
9098
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
8879
- for (const existing of readdirSync6(droneHome)) {
9099
+ for (const existing of readdirSync7(droneHome)) {
8880
9100
  if (wanted.has(existing)) continue;
8881
- const p = join12(droneHome, existing);
9101
+ const p = join13(droneHome, existing);
8882
9102
  try {
8883
9103
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
8884
9104
  } catch {
8885
9105
  }
8886
9106
  }
8887
9107
  for (const name of wanted) {
8888
- const link = join12(droneHome, name);
9108
+ const link = join13(droneHome, name);
8889
9109
  try {
8890
9110
  lstatSync(link);
8891
9111
  continue;
8892
9112
  } catch {
8893
9113
  }
8894
- const target = join12(realHome, name);
9114
+ const target = join13(realHome, name);
8895
9115
  let isDir;
8896
9116
  try {
8897
- isDir = statSync6(target).isDirectory();
9117
+ isDir = statSync7(target).isDirectory();
8898
9118
  } catch {
8899
9119
  continue;
8900
9120
  }
@@ -8912,7 +9132,7 @@ var REAL_PATH_ENV_PINS = [
8912
9132
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
8913
9133
  ];
8914
9134
  function realPathEnvPins(realHome) {
8915
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join12(realHome, ...segments)]));
9135
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join13(realHome, ...segments)]));
8916
9136
  }
8917
9137
  function dronePrivatePaths(realHome, swarmHome) {
8918
9138
  return [
@@ -9024,7 +9244,7 @@ function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
9024
9244
  const env = { ...base3 };
9025
9245
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
9026
9246
  if (isolate && !base3.TURBO_CACHE_DIR) {
9027
- env.TURBO_CACHE_DIR = join13(worktree, ".turbo", "cache");
9247
+ env.TURBO_CACHE_DIR = join14(worktree, ".turbo", "cache");
9028
9248
  }
9029
9249
  Object.assign(env, droneTestEnv(droneCap, base3));
9030
9250
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -9052,20 +9272,21 @@ var DAEMON_STATE_ENV = [
9052
9272
  "TASKLESS_TOKEN"
9053
9273
  ];
9054
9274
  function bundledRoleDir(kind) {
9055
- return join13(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
9275
+ return join14(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
9056
9276
  }
9057
9277
  function roleDir(worktree, kind) {
9058
- return join13(worktree, ".claude", "roles", kind);
9278
+ return join14(worktree, ".claude", "roles", kind);
9059
9279
  }
9060
9280
  function roleDirs(worktree, kind) {
9061
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync8(dir));
9281
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync9(dir));
9062
9282
  }
9063
- function nextRunLogPath(runLogsDir, runId) {
9064
- const dir = join13(runLogsDir, String(runId));
9065
- mkdirSync6(dir, { recursive: true });
9283
+ function nextRunLogPath(runLogsDir, runLogKeys) {
9284
+ const dir = runLogDirForWrite(runLogsDir, runLogKeys);
9285
+ if (!dir) throw new Error("\u043D\u0435\u0447\u0435\u043C \u0430\u0434\u0440\u0435\u0441\u043E\u0432\u0430\u0442\u044C \u043A\u0430\u0442\u0430\u043B\u043E\u0433 run-log'\u043E\u0432: \u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435\u0442 \u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u043E\u0433\u043E \u043A\u043B\u044E\u0447\u0430 (SwarmStore.runLogKeys)");
9286
+ mkdirSync7(dir, { recursive: true });
9066
9287
  let files = [];
9067
9288
  try {
9068
- files = readdirSync7(dir);
9289
+ files = readdirSync8(dir);
9069
9290
  } catch {
9070
9291
  }
9071
9292
  let next = 0;
@@ -9073,11 +9294,11 @@ function nextRunLogPath(runLogsDir, runId) {
9073
9294
  const m = /^(\d+)\.json/.exec(f);
9074
9295
  if (m) next = Math.max(next, Number(m[1]) + 1);
9075
9296
  }
9076
- return join13(dir, `${next}.json`);
9297
+ return join14(dir, `${next}.json`);
9077
9298
  }
9078
- function openNextRunLog(runLogsDir, runId) {
9299
+ function openNextRunLog(runLogsDir, runLogKeys) {
9079
9300
  for (let attempt = 0; attempt < 50; attempt++) {
9080
- const path = nextRunLogPath(runLogsDir, runId);
9301
+ const path = nextRunLogPath(runLogsDir, runLogKeys);
9081
9302
  let out;
9082
9303
  try {
9083
9304
  out = openSync2(path, "wx");
@@ -9093,7 +9314,7 @@ function openNextRunLog(runLogsDir, runId) {
9093
9314
  }
9094
9315
  return { out, err };
9095
9316
  }
9096
- throw new Error(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0439 run-log \u0444\u0430\u0439\u043B \u0434\u043B\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${runId} (\u0433\u043E\u043D\u043A\u0430?)`);
9317
+ throw new Error(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0439 run-log \u0444\u0430\u0439\u043B \u043F\u0440\u043E\u0433\u043E\u043D\u0430 (${runLogKeys[0]}) \u2014 \u0433\u043E\u043D\u043A\u0430?`);
9097
9318
  }
9098
9319
  function makeLauncher(cfg) {
9099
9320
  return ({ worktree, run, prompt, continueSession }) => {
@@ -9156,9 +9377,9 @@ function makeLauncher(cfg) {
9156
9377
  }
9157
9378
  const realHome = cfg.droneIsolation?.realHome ?? homedir4();
9158
9379
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
9159
- if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join13(cfg.droneIsolation.swarmHome, "token"))) {
9380
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync9(join14(cfg.droneIsolation.swarmHome, "token"))) {
9160
9381
  console.error(
9161
- `[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.`
9382
+ `[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.`
9162
9383
  );
9163
9384
  }
9164
9385
  const executor = cfg.executor ?? resolveExecutor();
@@ -9185,7 +9406,13 @@ function makeLauncher(cfg) {
9185
9406
  let outFd;
9186
9407
  let errFd;
9187
9408
  if (cfg.runLogsDir) {
9188
- const fds = openNextRunLog(cfg.runLogsDir, run.id);
9409
+ const logKeys = cfg.runLogKeys?.(run) ?? [];
9410
+ if (logKeys.length === 0) {
9411
+ throw new Error(
9412
+ `\u043F\u0440\u043E\u0433\u043E\u043D\u0443 #${run.id} (${run.task_slug}) \u043D\u0435\u0447\u0435\u043C \u0430\u0434\u0440\u0435\u0441\u043E\u0432\u0430\u0442\u044C \u043A\u0430\u0442\u0430\u043B\u043E\u0433 run-log'\u043E\u0432: \u0443 \u0441\u0442\u043E\u0440\u0430 \u043D\u0435\u0442 \u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u043E\u0433\u043E \u043A\u043B\u044E\u0447\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430. \u0414\u0440\u043E\u043D \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0435\u043D \u2014 \u0438\u043D\u0430\u0447\u0435 \u0435\u0433\u043E \u043B\u043E\u0433 \u043B\u0451\u0433 \u0431\u044B \u0432 \u043A\u0430\u0442\u0430\u043B\u043E\u0433, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044F \u043A\u0430\u043A \u0447\u0443\u0436\u043E\u0439 (TASWM-419).`
9413
+ );
9414
+ }
9415
+ const fds = openNextRunLog(cfg.runLogsDir, logKeys);
9189
9416
  outFd = fds.out;
9190
9417
  errFd = fds.err ?? void 0;
9191
9418
  stdio = ["ignore", outFd, errFd ?? "ignore"];
@@ -9213,21 +9440,21 @@ function makeLauncher(cfg) {
9213
9440
  init_procCleanup();
9214
9441
 
9215
9442
  // src/cost.ts
9443
+ import { readFileSync as readFileSync11, readdirSync as readdirSync9, renameSync as renameSync8 } from "fs";
9444
+ import { join as join15 } from "path";
9216
9445
  init_executors();
9217
- import { readFileSync as readFileSync11, readdirSync as readdirSync8, renameSync as renameSync7 } from "fs";
9218
- import { join as join14 } from "path";
9219
9446
  var UNPROCESSED_LOG_RE = /^\d+\.json$/;
9220
9447
  function unprocessedLogFiles(dir) {
9221
9448
  try {
9222
- return readdirSync8(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
9449
+ return readdirSync9(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
9223
9450
  } catch {
9224
9451
  return [];
9225
9452
  }
9226
9453
  }
9227
9454
  function accountRunCost(deps, run) {
9228
- const dir = join14(deps.runLogsDir, String(run.id));
9229
- const files = unprocessedLogFiles(dir);
9230
- if (files.length === 0) return;
9455
+ const dir = findRunLogDir(deps.runLogsDir, deps.db.runLogKeys(run.id));
9456
+ const files = dir ? unprocessedLogFiles(dir) : [];
9457
+ if (!dir || files.length === 0) return;
9231
9458
  const model = resolveRunModel(run, deps.swarmModels?.() ?? null);
9232
9459
  const executor = deps.executor ?? resolveExecutor();
9233
9460
  let added = 0;
@@ -9236,7 +9463,7 @@ function accountRunCost(deps, run) {
9236
9463
  let durationMs = 0;
9237
9464
  const processed = [];
9238
9465
  for (const f of files) {
9239
- const full = join14(dir, f);
9466
+ const full = join15(dir, f);
9240
9467
  let raw = null;
9241
9468
  try {
9242
9469
  raw = readFileSync11(full, "utf8");
@@ -9265,7 +9492,7 @@ function accountRunCost(deps, run) {
9265
9492
  }
9266
9493
  for (const full of processed) {
9267
9494
  try {
9268
- renameSync7(full, `${full}.done`);
9495
+ renameSync8(full, `${full}.done`);
9269
9496
  } catch {
9270
9497
  }
9271
9498
  }
@@ -9273,20 +9500,34 @@ function accountRunCost(deps, run) {
9273
9500
  function makeCostAccountant(deps) {
9274
9501
  return (run) => accountRunCost(deps, run);
9275
9502
  }
9503
+ function sweepPendingRunCosts(deps) {
9504
+ for (const run of deps.db.listRuns()) {
9505
+ if (run.pid != null && deps.isAlive(run.pid)) continue;
9506
+ try {
9507
+ accountRunCost(deps, run);
9508
+ } catch (e) {
9509
+ console.error(`[cost] \u043F\u0440\u043E\u0433\u043E\u043D #${run.id}: \u0443\u0447\u0451\u0442 \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F \u2014 ${String(e)}`);
9510
+ }
9511
+ }
9512
+ }
9513
+ function makeCostSweeper(deps) {
9514
+ return () => sweepPendingRunCosts(deps);
9515
+ }
9276
9516
 
9277
9517
  // src/runOutcome.ts
9518
+ import { readdirSync as readdirSync10, readFileSync as readFileSync12 } from "fs";
9519
+ import { join as join16 } from "path";
9278
9520
  init_executors();
9279
- import { readdirSync as readdirSync9, readFileSync as readFileSync12 } from "fs";
9280
- import { join as join15 } from "path";
9281
9521
  function endedItself(outcome) {
9282
9522
  return outcome === "graceful" || outcome === "budget";
9283
9523
  }
9284
9524
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
9285
- function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
9286
- const dir = join15(runLogsDir, String(runId));
9525
+ function classifyExit(runLogsDir, runLogKeys, executor = resolveExecutor()) {
9526
+ const dir = findRunLogDir(runLogsDir, runLogKeys);
9527
+ if (!dir) return "unknown";
9287
9528
  let files;
9288
9529
  try {
9289
- files = readdirSync9(dir);
9530
+ files = readdirSync10(dir);
9290
9531
  } catch {
9291
9532
  return "unknown";
9292
9533
  }
@@ -9300,7 +9541,7 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
9300
9541
  if (!latest) return "unknown";
9301
9542
  let raw;
9302
9543
  try {
9303
- raw = readFileSync12(join15(dir, latest.name), "utf8");
9544
+ raw = readFileSync12(join16(dir, latest.name), "utf8");
9304
9545
  } catch {
9305
9546
  return "unknown";
9306
9547
  }
@@ -9308,9 +9549,9 @@ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
9308
9549
  if (!result) return "crash";
9309
9550
  return result.stoppedByBudget ? "budget" : "graceful";
9310
9551
  }
9311
- function makeExitClassifier(runLogsDir, executor) {
9552
+ function makeExitClassifier(runLogsDir, runLogKeys, executor) {
9312
9553
  const exec = executor ?? resolveExecutor();
9313
- return (run) => classifyExit(runLogsDir, run.id, exec);
9554
+ return (run) => classifyExit(runLogsDir, runLogKeys(run.id), exec);
9314
9555
  }
9315
9556
 
9316
9557
  // src/orchestrator.ts
@@ -9318,20 +9559,20 @@ init_src();
9318
9559
  init_git();
9319
9560
 
9320
9561
  // src/deps.ts
9321
- import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync13 } from "fs";
9322
- import { join as join18 } from "path";
9562
+ import { existsSync as existsSync11, readdirSync as readdirSync12, readFileSync as readFileSync13 } from "fs";
9563
+ import { join as join19 } from "path";
9323
9564
  import { execa } from "execa";
9324
9565
 
9325
9566
  // src/prepareLock.ts
9326
9567
  import { createHash } from "crypto";
9327
- import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as statSync8, writeSync } from "fs";
9328
- import { join as join17 } from "path";
9568
+ import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as statSync9, writeSync } from "fs";
9569
+ import { join as join18 } from "path";
9329
9570
  import { tmpdir } from "os";
9330
9571
  var WAIT_TIMEOUT_MS = 5 * 6e4;
9331
9572
  var POLL_MS = 300;
9332
9573
  var STALE_MS = 20 * 6e4;
9333
9574
  function machineLockPath(key, dir = tmpdir()) {
9334
- return join17(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
9575
+ return join18(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
9335
9576
  }
9336
9577
  function defaultTryCreate(path, content) {
9337
9578
  try {
@@ -9348,7 +9589,7 @@ function defaultTryCreate(path, content) {
9348
9589
  }
9349
9590
  function defaultAgeMs(path) {
9350
9591
  try {
9351
- return Date.now() - statSync8(path).mtimeMs;
9592
+ return Date.now() - statSync9(path).mtimeMs;
9352
9593
  } catch {
9353
9594
  return null;
9354
9595
  }
@@ -9412,14 +9653,14 @@ function safeReadText(p) {
9412
9653
  }
9413
9654
  function safeListDirs(p) {
9414
9655
  try {
9415
- return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
9656
+ return readdirSync12(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
9416
9657
  } catch {
9417
9658
  return [];
9418
9659
  }
9419
9660
  }
9420
9661
  function safeListFiles(p) {
9421
9662
  try {
9422
- return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
9663
+ return readdirSync12(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
9423
9664
  } catch {
9424
9665
  return [];
9425
9666
  }
@@ -9445,21 +9686,21 @@ function overridePlan(override) {
9445
9686
  };
9446
9687
  }
9447
9688
  function detectPackageManager(worktree, io = {}) {
9448
- const exists = io.exists ?? existsSync10;
9689
+ const exists = io.exists ?? existsSync11;
9449
9690
  const readText = io.readText ?? safeReadText;
9450
- const hasPkgJson = exists(join18(worktree, "package.json"));
9691
+ const hasPkgJson = exists(join19(worktree, "package.json"));
9451
9692
  if (!hasPkgJson) {
9452
- if (exists(join18(worktree, "deno.json")) || exists(join18(worktree, "deno.jsonc")) || exists(join18(worktree, "deno.lock"))) return "deno";
9693
+ if (exists(join19(worktree, "deno.json")) || exists(join19(worktree, "deno.jsonc")) || exists(join19(worktree, "deno.lock"))) return "deno";
9453
9694
  return null;
9454
9695
  }
9455
- const pkg = readJson(readText, join18(worktree, "package.json"));
9696
+ const pkg = readJson(readText, join19(worktree, "package.json"));
9456
9697
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
9457
9698
  if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
9458
- if (exists(join18(worktree, "bun.lock")) || exists(join18(worktree, "bun.lockb"))) return "bun";
9459
- if (exists(join18(worktree, "pnpm-lock.yaml"))) return "pnpm";
9460
- if (exists(join18(worktree, "yarn.lock"))) return "yarn";
9461
- if (exists(join18(worktree, "package-lock.json"))) return "npm";
9462
- if (exists(join18(worktree, "deno.lock"))) return "deno";
9699
+ if (exists(join19(worktree, "bun.lock")) || exists(join19(worktree, "bun.lockb"))) return "bun";
9700
+ if (exists(join19(worktree, "pnpm-lock.yaml"))) return "pnpm";
9701
+ if (exists(join19(worktree, "yarn.lock"))) return "yarn";
9702
+ if (exists(join19(worktree, "package-lock.json"))) return "npm";
9703
+ if (exists(join19(worktree, "deno.lock"))) return "deno";
9463
9704
  return "npm";
9464
9705
  }
9465
9706
  function jsInstallCommand(pm) {
@@ -9512,7 +9753,7 @@ var ECOSYSTEM_MARKERS = [
9512
9753
  // конфигурация — дело сборки, не подготовки
9513
9754
  ];
9514
9755
  function detectPlan(worktree, io) {
9515
- const exists = io.exists ?? existsSync10;
9756
+ const exists = io.exists ?? existsSync11;
9516
9757
  const listFiles = io.listFiles ?? safeListFiles;
9517
9758
  const pm = detectPackageManager(worktree, io);
9518
9759
  if (pm) {
@@ -9520,7 +9761,7 @@ function detectPlan(worktree, io) {
9520
9761
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
9521
9762
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
9522
9763
  // заново, но он дешёвый: тянет из того же общего кэша.
9523
- { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join18(worktree, "node_modules"), source: "detect" }
9764
+ { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join19(worktree, "node_modules"), source: "detect" }
9524
9765
  ];
9525
9766
  for (const target of workspaceBuildTargets(worktree, io)) {
9526
9767
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
@@ -9529,12 +9770,12 @@ function detectPlan(worktree, io) {
9529
9770
  return { source: "detect", ecosystem: `node/${pm}`, commands };
9530
9771
  }
9531
9772
  for (const marker of ECOSYSTEM_MARKERS) {
9532
- const hit = marker.files.some((f) => exists(join18(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
9773
+ const hit = marker.files.some((f) => exists(join19(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
9533
9774
  if (!hit) continue;
9534
9775
  let ecosystem = marker.ecosystem;
9535
9776
  let command = marker.command;
9536
9777
  for (const refine of marker.refine ?? []) {
9537
- if (!exists(join18(worktree, refine.file))) continue;
9778
+ if (!exists(join19(worktree, refine.file))) continue;
9538
9779
  ecosystem = refine.ecosystem;
9539
9780
  command = refine.command;
9540
9781
  break;
@@ -9591,12 +9832,12 @@ var CI_INSTALL_PATTERNS = [
9591
9832
  function ciWorkflowPlan(worktree, io) {
9592
9833
  const readText = io.readText ?? safeReadText;
9593
9834
  const listFiles = io.listFiles ?? safeListFiles;
9594
- const dir = join18(worktree, ".github", "workflows");
9835
+ const dir = join19(worktree, ".github", "workflows");
9595
9836
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
9596
9837
  const seen = /* @__PURE__ */ new Set();
9597
9838
  const commands = [];
9598
9839
  for (const name of files.sort()) {
9599
- const yaml = readText(join18(dir, name));
9840
+ const yaml = readText(join19(dir, name));
9600
9841
  if (yaml === null) continue;
9601
9842
  for (const raw of workflowRunLines(yaml)) {
9602
9843
  const line = raw.replace(/\s+/g, " ").trim();
@@ -9611,7 +9852,7 @@ function ciWorkflowPlan(worktree, io) {
9611
9852
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
9612
9853
  }
9613
9854
  function workspaceGlobs(worktree, readText) {
9614
- const yaml = readText(join18(worktree, "pnpm-workspace.yaml"));
9855
+ const yaml = readText(join19(worktree, "pnpm-workspace.yaml"));
9615
9856
  if (yaml !== null) {
9616
9857
  const globs = [];
9617
9858
  let inPackages = false;
@@ -9631,7 +9872,7 @@ function workspaceGlobs(worktree, readText) {
9631
9872
  }
9632
9873
  if (globs.length) return globs;
9633
9874
  }
9634
- const pkg = readJson(readText, join18(worktree, "package.json"));
9875
+ const pkg = readJson(readText, join19(worktree, "package.json"));
9635
9876
  const ws = pkg?.workspaces;
9636
9877
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
9637
9878
  return list.filter((g) => typeof g === "string");
@@ -9642,14 +9883,14 @@ function workspaceDirs(worktree, globs, io) {
9642
9883
  if (glob.startsWith("!")) continue;
9643
9884
  const star = glob.indexOf("*");
9644
9885
  if (star === -1) {
9645
- const dir = join18(worktree, glob);
9646
- if (io.exists(join18(dir, "package.json"))) dirs.add(dir);
9886
+ const dir = join19(worktree, glob);
9887
+ if (io.exists(join19(dir, "package.json"))) dirs.add(dir);
9647
9888
  continue;
9648
9889
  }
9649
- const base3 = join18(worktree, glob.slice(0, star).replace(/\/$/, ""));
9890
+ const base3 = join19(worktree, glob.slice(0, star).replace(/\/$/, ""));
9650
9891
  for (const name of io.listDirs(base3)) {
9651
- const dir = join18(base3, name);
9652
- if (io.exists(join18(dir, "package.json"))) dirs.add(dir);
9892
+ const dir = join19(base3, name);
9893
+ if (io.exists(join19(dir, "package.json"))) dirs.add(dir);
9653
9894
  }
9654
9895
  }
9655
9896
  return [...dirs];
@@ -9670,18 +9911,18 @@ function entryPaths(pkg) {
9670
9911
  return out;
9671
9912
  }
9672
9913
  function workspaceBuildTargets(worktree, io = {}) {
9673
- const exists = io.exists ?? existsSync10;
9914
+ const exists = io.exists ?? existsSync11;
9674
9915
  const readText = io.readText ?? safeReadText;
9675
9916
  const listDirs = io.listDirs ?? safeListDirs;
9676
9917
  const globs = workspaceGlobs(worktree, readText);
9677
9918
  if (!globs.length) return [];
9678
9919
  const pkgs = [];
9679
9920
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
9680
- const pkg = readJson(readText, join18(dir, "package.json"));
9921
+ const pkg = readJson(readText, join19(dir, "package.json"));
9681
9922
  if (!pkg || typeof pkg.name !== "string") continue;
9682
9923
  const scripts = pkg.scripts ?? {};
9683
9924
  const entries = entryPaths(pkg);
9684
- const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join18(dir, e)));
9925
+ const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join19(dir, e)));
9685
9926
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
9686
9927
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
9687
9928
  }
@@ -9709,7 +9950,7 @@ function devDatabaseUrl(worktree, io = {}) {
9709
9950
  const fromEnv = (io.env ?? process.env).DATABASE_URL;
9710
9951
  if (fromEnv && fromEnv.trim()) return fromEnv.trim();
9711
9952
  for (const file of [".env", ".env.example"]) {
9712
- const raw = readText(join18(worktree, file));
9953
+ const raw = readText(join19(worktree, file));
9713
9954
  if (raw === null) continue;
9714
9955
  const m = /^\s*(?:export\s+)?DATABASE_URL\s*=\s*(.*)$/m.exec(raw);
9715
9956
  const value = (m?.[1] ?? "").trim().replace(/^['"]|['"]$/g, "");
@@ -9725,11 +9966,11 @@ function isLocalDatabase(url) {
9725
9966
  }
9726
9967
  }
9727
9968
  function prismaPackages(worktree, io) {
9728
- const exists = io.exists ?? existsSync10;
9969
+ const exists = io.exists ?? existsSync11;
9729
9970
  const readText = io.readText ?? safeReadText;
9730
9971
  const listDirs = io.listDirs ?? safeListDirs;
9731
9972
  const dirs = [worktree, ...workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs })];
9732
- return dirs.filter((dir) => exists(join18(dir, "prisma", "schema.prisma")) && listDirs(join18(dir, "prisma", "migrations")).length > 0);
9973
+ return dirs.filter((dir) => exists(join19(dir, "prisma", "schema.prisma")) && listDirs(join19(dir, "prisma", "migrations")).length > 0);
9733
9974
  }
9734
9975
  function jsExecBin(pm, command) {
9735
9976
  switch (pm) {
@@ -9762,7 +10003,7 @@ function planPrepare(worktree, io = {}) {
9762
10003
  return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
9763
10004
  }
9764
10005
  async function ensureWorktreeDeps(worktree, io = {}) {
9765
- const exists = io.exists ?? existsSync10;
10006
+ const exists = io.exists ?? existsSync11;
9766
10007
  const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
9767
10008
  const approve = io.approve ?? (() => true);
9768
10009
  const plan = planPrepare(worktree, io);
@@ -9947,6 +10188,43 @@ function pruneDefers(episodes, live) {
9947
10188
  for (const key of episodes.keys()) if (!live.has(key)) episodes.delete(key);
9948
10189
  }
9949
10190
 
10191
+ // src/spawnAttempts.ts
10192
+ init_src();
10193
+ var PRE_START_ATTEMPT_LIMIT = 3;
10194
+ var PRE_START_MARK = "\u0434\u0440\u043E\u043D \u043D\u0435 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u0430\u043B";
10195
+ var SPAWN_PARK_MARK = "\u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0442\u043B\u043E\u0436\u0435\u043D\u0430 \u0440\u043E\u0435\u043C";
10196
+ function isPreStartFailure(run) {
10197
+ return run.status === "failed" && (run.stop_reason ?? "").includes(PRE_START_MARK);
10198
+ }
10199
+ function isSpawnParked(run) {
10200
+ return (run.stop_reason ?? "").includes(SPAWN_PARK_MARK);
10201
+ }
10202
+ var SPAWN_ATTEMPT_WINDOW_MS = 60 * 6e4;
10203
+ function preStartAttempts(runs, taskSlug, opts = {}) {
10204
+ const windowMs = opts.windowMs ?? SPAWN_ATTEMPT_WINDOW_MS;
10205
+ const now = opts.now ?? Date.now();
10206
+ const own = runs.filter((r) => r.task_slug === taskSlug && r.reviews_run_id == null && r.resolves_run_id == null && r.verifies_run_id == null).sort((a, b) => b.id - a.id);
10207
+ let n = 0;
10208
+ for (const run of own) {
10209
+ if (run.status === "queued" || run.status === "running") continue;
10210
+ if (isSpawnParked(run) && run.status !== "failed") continue;
10211
+ if (!isPreStartFailure(run)) break;
10212
+ const at = run.created_at ? stampMs(run.created_at) : Number.NaN;
10213
+ if (Number.isFinite(at) && now - at >= windowMs) break;
10214
+ n += 1;
10215
+ }
10216
+ return n;
10217
+ }
10218
+ function unresolvedRepoReason(repo, served) {
10219
+ const raw = repo.trim();
10220
+ if (!raw || raw.startsWith("/") || raw.startsWith("~") || /^[a-z]:[\\/]/i.test(raw)) return null;
10221
+ const list = served.length ? served.join(", ") : "\u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E (\u0440\u0435\u0435\u0441\u0442\u0440 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u043F\u0443\u0441\u0442)";
10222
+ return `\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 ${raw} \u043D\u0435 \u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u043B\u0441\u044F \u0432 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0443\u0442\u044C: \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u0435\u0433\u043E \u043A\u043B\u043E\u043D\u0430 \u043D\u0435\u0442. \u041C\u0430\u0448\u0438\u043D\u0430 \u043E\u0431\u0441\u043B\u0443\u0436\u0438\u0432\u0430\u0435\u0442: ${list}. \u041A\u043B\u043E\u043D\u0438\u0440\u0443\u0439 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u0438 \u0434\u043E\u0431\u0430\u0432\u044C \u0435\u0433\u043E \u0432 SWARM_REPO_PATHS (\u0438\u043B\u0438 swarm start --repo <\u043F\u0443\u0442\u044C>), \u043B\u0438\u0431\u043E \u043E\u0442\u0434\u0430\u0439 \u0437\u0430\u0434\u0430\u0447\u0443 \u043C\u0430\u0448\u0438\u043D\u0435, \u0433\u0434\u0435 \u043E\u043D \u0435\u0441\u0442\u044C.`;
10223
+ }
10224
+ function parkReason(attempts, reason) {
10225
+ return `${PRE_START_MARK} ${attempts} \u0440\u0430\u0437\u0430 \u043F\u043E\u0434\u0440\u044F\u0434 \u2014 ${SPAWN_PARK_MARK}, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u043F\u043E \u043A\u0440\u0443\u0433\u0443. \u0427\u0435\u0433\u043E \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E: ${reason}`;
10226
+ }
10227
+
9950
10228
  // src/overmind.ts
9951
10229
  init_git();
9952
10230
 
@@ -11204,7 +11482,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11204
11482
  };
11205
11483
  const closedBeforeStart = (run) => opts.daemonStartedAt != null && sqliteMs(run.updated_at) < opts.daemonStartedAt;
11206
11484
  const recentFailed = allRuns.filter(
11207
- (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r)
11485
+ (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r) && !isPreStartFailure(r)
11208
11486
  );
11209
11487
  if (recentFailed.length >= thresholds.failedSpikeCount) {
11210
11488
  candidates.push({
@@ -11263,6 +11541,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11263
11541
  if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
11264
11542
  if (killedByOperator(run)) continue;
11265
11543
  if (closedBeforeStart(run)) continue;
11544
+ if (isPreStartFailure(run)) continue;
11266
11545
  const repeat = isRepeatDown(run);
11267
11546
  candidates.push({
11268
11547
  key: `run_failed:${run.id}`,
@@ -11277,6 +11556,23 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11277
11556
  })
11278
11557
  });
11279
11558
  }
11559
+ for (const run of allRuns) {
11560
+ if (run.status !== "failed" || !isSpawnParked(run)) continue;
11561
+ if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
11562
+ if (closedBeforeStart(run)) continue;
11563
+ candidates.push({
11564
+ key: `spawn_parked:${run.id}`,
11565
+ minDurationMs: 0,
11566
+ // серия попыток УЖЕ прошла свой PRE_START_ATTEMPT_LIMIT — отлёживать нечего
11567
+ build: () => ({
11568
+ runId: run.id,
11569
+ level: "decision",
11570
+ title: runTitle(run, "\u0440\u043E\u0439 \u043D\u0435 \u0441\u043C\u043E\u0433 \u0432\u0437\u044F\u0442\u044C\u0441\u044F \u0437\u0430 \u0437\u0430\u0434\u0430\u0447\u0443"),
11571
+ body: `${run.task_slug}: ${run.stop_reason ?? SPAWN_PARK_MARK}`,
11572
+ ...hrefOf(run.task_url)
11573
+ })
11574
+ });
11575
+ }
11280
11576
  const queuedCount = allRuns.filter((r) => r.status === "queued").length;
11281
11577
  const activeDroneCount2 = allRuns.filter((r) => r.kind !== "hero" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
11282
11578
  if (queuedCount > 0 && activeDroneCount2 === 0) {
@@ -11294,6 +11590,31 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11294
11590
  })
11295
11591
  });
11296
11592
  }
11593
+ const GROUP_DEP_MISSING = {
11594
+ id: "dep_missing",
11595
+ title: (n) => `${n} ${plural2(n, ["\u043F\u0440\u043E\u0433\u043E\u043D \u0436\u0434\u0451\u0442", "\u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0436\u0434\u0443\u0442", "\u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u0436\u0434\u0443\u0442"])} \u0431\u043B\u043E\u043A\u0435\u0440\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u043D\u0435\u0442`
11596
+ };
11597
+ for (const run of allRuns) {
11598
+ if (run.status !== "queued") continue;
11599
+ if (!run.depends_on || run.depends_on.length === 0) continue;
11600
+ const lost = db.depVerdicts(run.depends_on).filter((d) => d.outcome === "unknown");
11601
+ if (!lost.length) continue;
11602
+ candidates.push({
11603
+ key: `dep_missing:${run.task_slug}`,
11604
+ // по СЛАГУ, а не по id зеркала: тот меняется на рестарте (TAS-485)
11605
+ minDurationMs: thresholds.depMissingMs,
11606
+ group: GROUP_DEP_MISSING,
11607
+ build: () => ({
11608
+ runId: run.id,
11609
+ // Пуш: рой сам из этого не выйдет НИКОГДА, а лечение — одно действие человека.
11610
+ // Ровно тот случай, ради которого уровень decision и заведён.
11611
+ level: "decision",
11612
+ title: runTitle(run, "\u0436\u0434\u0451\u0442 \u0431\u043B\u043E\u043A\u0435\u0440\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u043D\u0435\u0442"),
11613
+ body: `${droneOf(run)}: \u043F\u0440\u043E\u0433\u043E\u043D \u0441\u0442\u043E\u0438\u0442 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0441 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C\u044E ${joinCapped(lost.map((d) => d.slug))}, \u043D\u043E \u043F\u043E \u044D\u0442\u043E\u0439 \u0441\u0441\u044B\u043B\u043A\u0435 \u043D\u0435\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u2014 \u043D\u0438 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435, \u043D\u0438 \u0432 \u043F\u0440\u043E\u0434\u0435. \u0421\u0430\u043C \u043E\u043D \u043D\u0435 \u043F\u043E\u0435\u0434\u0435\u0442: \u0441\u043D\u0438\u043C\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C (swarm_run_upsert{runId, dependsOn:[]}) \u0438\u043B\u0438 \u0437\u0430\u0432\u0435\u0434\u0438 \u0431\u043B\u043E\u043A\u0435\u0440 \u0437\u0430\u043D\u043E\u0432\u043E.`,
11614
+ ...hrefOf(run.task_url)
11615
+ })
11616
+ });
11617
+ }
11297
11618
  if (opts.gateReason && queuedCount > 0) {
11298
11619
  const reason = opts.gateReason;
11299
11620
  candidates.push({
@@ -11378,6 +11699,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
11378
11699
  for (const q of db.openQuestions()) {
11379
11700
  const run = runsById.get(q.run_id);
11380
11701
  if (!run) continue;
11702
+ if (isTerminalStatus(run.status)) continue;
11381
11703
  candidates.push({
11382
11704
  key: `question_unanswered:${q.id}`,
11383
11705
  minDurationMs: thresholds.questionUnansweredMs,
@@ -11860,22 +12182,22 @@ function makeMergeGateResolver(deps) {
11860
12182
  }
11861
12183
 
11862
12184
  // src/envGate.ts
11863
- import { existsSync as existsSync13 } from "fs";
11864
- import { join as join24 } from "path";
12185
+ import { existsSync as existsSync14 } from "fs";
12186
+ import { join as join25 } from "path";
11865
12187
  import { createConnection } from "net";
11866
12188
  import { execa as execa2 } from "execa";
11867
12189
 
11868
12190
  // src/platform.ts
11869
- import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
11870
- import { delimiter, join as join23 } from "path";
12191
+ import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
12192
+ import { delimiter, join as join24 } from "path";
11871
12193
  function resolveCommandPath(name, env = process.env, platform = process.platform) {
11872
- if (name.includes("/") || name.includes("\\")) return existsSync12(name) ? name : null;
12194
+ if (name.includes("/") || name.includes("\\")) return existsSync13(name) ? name : null;
11873
12195
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
11874
12196
  const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
11875
12197
  for (const dir of dirs) {
11876
12198
  for (const ext of exts) {
11877
- const candidate = join23(dir, name + ext);
11878
- if (existsSync12(candidate)) return candidate;
12199
+ const candidate = join24(dir, name + ext);
12200
+ if (existsSync13(candidate)) return candidate;
11879
12201
  }
11880
12202
  }
11881
12203
  return null;
@@ -11958,8 +12280,8 @@ var COMPOSE_MARKERS = [
11958
12280
  "compose.yaml"
11959
12281
  ];
11960
12282
  function detectRequirements(repo, io = {}) {
11961
- const exists = io.exists ?? existsSync13;
11962
- const marker = COMPOSE_MARKERS.find((f) => exists(join24(repo, f)));
12283
+ const exists = io.exists ?? existsSync14;
12284
+ const marker = COMPOSE_MARKERS.find((f) => exists(join25(repo, f)));
11963
12285
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
11964
12286
  }
11965
12287
  function resolveRequirements(repo, declared, io = {}) {
@@ -12095,8 +12417,8 @@ import { basename as basename3 } from "path";
12095
12417
  import { serve as serve2 } from "@hono/node-server";
12096
12418
 
12097
12419
  // src/diff.ts
12098
- import { statSync as statSync9 } from "fs";
12099
- import { join as join25 } from "path";
12420
+ import { statSync as statSync10 } from "fs";
12421
+ import { join as join26 } from "path";
12100
12422
  import { $ as $4 } from "execa";
12101
12423
  var MAX_FILES = 200;
12102
12424
  var MAX_LINES_PER_FILE = 1500;
@@ -12239,7 +12561,7 @@ async function resolveBase(wt, baseBranch) {
12239
12561
  }
12240
12562
  function mtime(path) {
12241
12563
  try {
12242
- return statSync9(path).mtimeMs;
12564
+ return statSync10(path).mtimeMs;
12243
12565
  } catch {
12244
12566
  return 0;
12245
12567
  }
@@ -12264,7 +12586,7 @@ async function collectRunDiff(worktree, opts = {}) {
12264
12586
  files.push(pf);
12265
12587
  }
12266
12588
  }
12267
- files.sort((a, b) => mtime(join25(worktree, b.path)) - mtime(join25(worktree, a.path)));
12589
+ files.sort((a, b) => mtime(join26(worktree, b.path)) - mtime(join26(worktree, a.path)));
12268
12590
  const truncatedFiles = files.length > MAX_FILES;
12269
12591
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
12270
12592
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -12297,7 +12619,7 @@ async function changedPaths(worktree, opts = {}) {
12297
12619
  // package.json
12298
12620
  var package_default = {
12299
12621
  name: "@taskless-app/swarm",
12300
- version: "0.6.14",
12622
+ version: "0.6.15",
12301
12623
  type: "module",
12302
12624
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
12303
12625
  keywords: [
@@ -12347,7 +12669,7 @@ var package_default = {
12347
12669
  execa: "^10.0.0",
12348
12670
  zod: "3.25.76"
12349
12671
  },
12350
- gitHead: "a3b11498dbed5db3642fe4fc83bcd2054efde055"
12672
+ gitHead: "5bc8e0c067de58fd72b0eb434e80ad29c7a00223"
12351
12673
  };
12352
12674
 
12353
12675
  // src/version.ts
@@ -13980,10 +14302,10 @@ async function checkDaemonVersion(start, opts = {}) {
13980
14302
 
13981
14303
  // src/hostCommands.ts
13982
14304
  init_src();
13983
- import { readFileSync as readFileSync18, renameSync as renameSync8, writeFileSync as writeFileSync13 } from "fs";
13984
- import { join as join26 } from "path";
14305
+ import { readFileSync as readFileSync18, renameSync as renameSync9, writeFileSync as writeFileSync13 } from "fs";
14306
+ import { join as join27 } from "path";
13985
14307
  function hostCommandStatePath(home) {
13986
- return join26(home, "host-command.json");
14308
+ return join27(home, "host-command.json");
13987
14309
  }
13988
14310
  function readState(path) {
13989
14311
  try {
@@ -13995,7 +14317,7 @@ function readState(path) {
13995
14317
  function writeState(path, state) {
13996
14318
  try {
13997
14319
  writeFileSync13(`${path}.tmp`, JSON.stringify(state));
13998
- renameSync8(`${path}.tmp`, path);
14320
+ renameSync9(`${path}.tmp`, path);
13999
14321
  } catch {
14000
14322
  }
14001
14323
  }
@@ -14157,8 +14479,8 @@ async function doRestart(cmd, opts, note) {
14157
14479
 
14158
14480
  // src/selfUpdate.ts
14159
14481
  init_git();
14160
- import { existsSync as existsSync14 } from "fs";
14161
- import { join as join27 } from "path";
14482
+ import { existsSync as existsSync15 } from "fs";
14483
+ import { join as join28 } from "path";
14162
14484
  import { $ as $5 } from "execa";
14163
14485
  var DAEMON_PACKAGE = "@taskless-app/swarm";
14164
14486
  var defaultRun = async (cmd, args, cwd) => {
@@ -14171,7 +14493,7 @@ function tail(text, max = 400) {
14171
14493
  }
14172
14494
  async function runSelfUpdate(io) {
14173
14495
  const run = io.run ?? defaultRun;
14174
- const exists = io.exists ?? existsSync14;
14496
+ const exists = io.exists ?? existsSync15;
14175
14497
  const repo = io.repo;
14176
14498
  await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
14177
14499
  const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
@@ -14200,7 +14522,7 @@ async function runSelfUpdate(io) {
14200
14522
  };
14201
14523
  }
14202
14524
  await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
14203
- if (!exists(join27(repo, "pnpm-lock.yaml"))) {
14525
+ if (!exists(join28(repo, "pnpm-lock.yaml"))) {
14204
14526
  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" };
14205
14527
  }
14206
14528
  const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
@@ -14258,6 +14580,47 @@ function makeOvermindNarrator() {
14258
14580
  };
14259
14581
  }
14260
14582
 
14583
+ // src/orphanQuestions.ts
14584
+ init_src();
14585
+ var ORPHAN_NOTE_WINDOW_MS = DAY_MS;
14586
+ var ORPHAN_NOTE_PREFIX = "\u0412\u043E\u043F\u0440\u043E\u0441 \u043E\u0441\u0442\u0430\u043B\u0441\u044F \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u0441\u044F \u0440\u0430\u043D\u044C\u0448\u0435";
14587
+ function orphanWithdrawReason(status) {
14588
+ return `\u0441\u043D\u044F\u0442 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438: \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0432\u0435\u0440\u0448\u0451\u043D (${status}), \u043E\u0442\u0432\u0435\u0447\u0430\u0442\u044C \u043D\u0435\u043A\u043E\u043C\u0443`;
14589
+ }
14590
+ function reapOrphanedQuestions(db, now, windowMs = ORPHAN_NOTE_WINDOW_MS) {
14591
+ const result = { withdrawn: 0, noted: 0 };
14592
+ let open;
14593
+ let runsById;
14594
+ try {
14595
+ open = db.openQuestions();
14596
+ if (open.length === 0) return result;
14597
+ runsById = new Map(db.listRuns().map((r) => [r.id, r]));
14598
+ } catch {
14599
+ return result;
14600
+ }
14601
+ for (const q of open) {
14602
+ const run = runsById.get(q.run_id);
14603
+ if (!run) continue;
14604
+ if (!isTerminalStatus(run.status) && run.superseded_by == null) continue;
14605
+ try {
14606
+ if (now - sqliteMs(run.updated_at) <= windowMs) {
14607
+ db.addEvent(run.id, NOTE_KIND, orphanNoteText(run, q), NOTE_ACTOR);
14608
+ result.noted++;
14609
+ }
14610
+ db.withdrawQuestion(q.id, orphanWithdrawReason(run.status));
14611
+ result.withdrawn++;
14612
+ } catch (e) {
14613
+ console.error(`[orphan-questions] \u0432\u043E\u043F\u0440\u043E\u0441 #${q.id} \u043D\u0435 \u0441\u043D\u044F\u0442, \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:`, e);
14614
+ }
14615
+ }
14616
+ return result;
14617
+ }
14618
+ var NOTE_KIND = "note";
14619
+ var NOTE_ACTOR = "orchestrator";
14620
+ function orphanNoteText(run, q) {
14621
+ return `${ORPHAN_NOTE_PREFIX} (${run.status}). \u041E\u0442\u0432\u0435\u0447\u0430\u0442\u044C \u043D\u0435\u043A\u043E\u043C\u0443, \u0437\u0432\u0430\u0442\u044C \u043D\u0438\u043A\u043E\u0433\u043E \u043D\u0435 \u0431\u0443\u0434\u0435\u043C \u2014 \u0442\u0435\u043A\u0441\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u044E: \xAB${q.text}\xBB`;
14622
+ }
14623
+
14261
14624
  // src/probe.ts
14262
14625
  init_executors();
14263
14626
  import { spawn as spawn2 } from "child_process";
@@ -14318,18 +14681,18 @@ async function fetchExternalStorm(url, opts = {}) {
14318
14681
  }
14319
14682
 
14320
14683
  // src/anomalyState.ts
14321
- import { existsSync as existsSync15, readFileSync as readFileSync19, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
14322
- import { join as join28 } from "path";
14684
+ import { existsSync as existsSync16, readFileSync as readFileSync19, renameSync as renameSync10, writeFileSync as writeFileSync14 } from "fs";
14685
+ import { join as join29 } from "path";
14323
14686
  var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
14324
14687
  var MAX_RESOLVED = 200;
14325
14688
  function emptyState() {
14326
14689
  return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
14327
14690
  }
14328
14691
  function anomalyStatePath(home) {
14329
- return join28(home, "anomaly-state.json");
14692
+ return join29(home, "anomaly-state.json");
14330
14693
  }
14331
14694
  function loadAnomalyState(home, now = Date.now(), io = {}) {
14332
- const exists = io.exists ?? existsSync15;
14695
+ const exists = io.exists ?? existsSync16;
14333
14696
  const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
14334
14697
  const path = anomalyStatePath(home);
14335
14698
  if (!exists(path)) return emptyState();
@@ -14361,7 +14724,7 @@ function saveAnomalyState(home, state, io = {}) {
14361
14724
  const path = anomalyStatePath(home);
14362
14725
  const writeFile3 = io.writeFile ?? ((p, data) => {
14363
14726
  writeFileSync14(`${p}.tmp`, data);
14364
- renameSync9(`${p}.tmp`, p);
14727
+ renameSync10(`${p}.tmp`, p);
14365
14728
  });
14366
14729
  writeFile3(path, JSON.stringify(state, null, 2));
14367
14730
  }
@@ -14687,6 +15050,9 @@ function short(files, n = 3) {
14687
15050
  return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
14688
15051
  }
14689
15052
  var SPINE_PREDICTION_WEIGHT = 0.7;
15053
+ function spineOfPrediction(spine, predicted) {
15054
+ return predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
15055
+ }
14690
15056
  function decideConflict({ model, candidate, active }) {
14691
15057
  if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
14692
15058
  if (!hasZoneModel(model)) return { kind: "unknown" };
@@ -14694,23 +15060,16 @@ function decideConflict({ model, candidate, active }) {
14694
15060
  if (!predicted.length) return { kind: "unknown" };
14695
15061
  if (!active.length) return { kind: "clear" };
14696
15062
  const spine = new Set(model.spine);
14697
- const candidateSpine = predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
14698
- if (candidateSpine.length) {
14699
- const blocker = active[0];
14700
- return {
14701
- kind: "wait",
14702
- blockedBy: blocker.run,
14703
- message: `\u0437\u0430\u0434\u0430\u0447\u0430 \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${short(candidateSpine)}) \u2014 \u0442\u0430\u043A\u0438\u0435 \u0438\u0434\u0443\u0442 \u043F\u043E\u0441\u043B\u0435\u0434\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u0437\u043E\u043D\u044B; \u0436\u0434\u0443 ${blocker.run.task_slug} (#${blocker.run.id})`
14704
- };
14705
- }
14706
- for (const w of active) {
14707
- if (!w.actual) continue;
14708
- const theirSpine = w.files.filter((f) => spine.has(f));
14709
- if (theirSpine.length && zoneForFiles(model, w.files)?.zone === SPINE_ZONE) {
15063
+ const candidateSpine = new Set(spineOfPrediction(spine, predicted));
15064
+ if (candidateSpine.size) {
15065
+ for (const w of active) {
15066
+ const theirSpine = w.actual ? w.files.filter((f) => spine.has(f)) : spineOfPrediction(spine, predictForRun(model, w.run));
15067
+ const shared = theirSpine.filter((f) => candidateSpine.has(f));
15068
+ if (!shared.length) continue;
14710
15069
  return {
14711
15070
  kind: "wait",
14712
15071
  blockedBy: w.run,
14713
- message: `${w.run.task_slug} (#${w.run.id}) \u043F\u0440\u044F\u043C\u043E \u0441\u0435\u0439\u0447\u0430\u0441 \u043F\u0440\u0430\u0432\u0438\u0442 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${short(theirSpine)}) \u2014 \u043E\u0442\u0432\u0435\u0442\u0432\u043B\u044F\u0442\u044C\u0441\u044F \u043E\u0442 \u043D\u0435\u0433\u043E \u0440\u0430\u043D\u043E, \u0436\u0434\u0443`
15072
+ message: `\u044D\u0442\u0438 \u0434\u0432\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0442\u0440\u043E\u0433\u0430\u044E\u0442 \u043E\u0434\u043D\u0438 \u0444\u0430\u0439\u043B\u044B \u043E\u0431\u0449\u0435\u0433\u043E \u0445\u0440\u0435\u0431\u0442\u0430 (${short(shared)}) \u2014 \u0442\u0430\u043A\u0438\u0435 \u0438\u0434\u0443\u0442 \u043F\u043E\u0441\u043B\u0435\u0434\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u0437\u043E\u043D\u044B: \u0436\u0434\u0443 ${w.run.task_slug} (#${w.run.id})` + (w.actual ? "" : " (\u043F\u043E \u043F\u0440\u0435\u0434\u0441\u043A\u0430\u0437\u0430\u043D\u0438\u044E \u2014 \u0434\u0440\u043E\u043D \u0435\u0449\u0451 \u043D\u0435 \u043D\u0430\u0447\u0430\u043B \u043F\u0438\u0441\u0430\u0442\u044C \u043A\u043E\u0434)")
14714
15073
  };
14715
15074
  }
14716
15075
  }
@@ -15322,7 +15681,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
15322
15681
  }
15323
15682
  return parts.join("\n");
15324
15683
  }
15325
- var HERO_CHARTER_PATH = join29(dirname4(THIS_FILE), "..", "docs", "HERO.md");
15684
+ var HERO_CHARTER_PATH = join30(dirname4(THIS_FILE), "..", "docs", "HERO.md");
15326
15685
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
15327
15686
  const name = run.name || `#${run.id}`;
15328
15687
  const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
@@ -15622,7 +15981,7 @@ async function wakeRun(deps, runId) {
15622
15981
  if (!run) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0437\u0435\u0440\u043A\u0430\u043B\u0435 \u0440\u043E\u044F` };
15623
15982
  if (run.pid != null && deps.isAlive(run.pid)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 (pid ${run.pid}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
15624
15983
  if (isTerminalStatus(run.status)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435\u043D (${run.status}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
15625
- const worktreeExists = deps.worktreeExists ?? existsSync16;
15984
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
15626
15985
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
15627
15986
  const wt = run.worktree_path;
15628
15987
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -15707,9 +16066,9 @@ function repoHasCi(deps, run) {
15707
16066
  function repoHasCiWorkflows(worktreePath, repo) {
15708
16067
  for (const base3 of [worktreePath, repo]) {
15709
16068
  if (!base3) continue;
15710
- const dir = join29(base3, ".github", "workflows");
16069
+ const dir = join30(base3, ".github", "workflows");
15711
16070
  try {
15712
- if (readdirSync12(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
16071
+ if (readdirSync13(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
15713
16072
  } catch {
15714
16073
  }
15715
16074
  }
@@ -15858,7 +16217,7 @@ async function wakeReviewerOnCiReady(deps, run, over) {
15858
16217
  return true;
15859
16218
  }
15860
16219
  function worktreeAndTranscriptAlive(deps, worktree) {
15861
- const exists = deps.worktreeExists ?? existsSync16;
16220
+ const exists = deps.worktreeExists ?? existsSync17;
15862
16221
  const transcript = deps.hasTranscript ?? executorHasSession;
15863
16222
  return exists(worktree) && transcript(worktree);
15864
16223
  }
@@ -16161,6 +16520,15 @@ async function trySpawnQueuedRun(deps, run) {
16161
16520
  // причина одна на машину — в лог уходит одной строкой, а не по строке на прогон
16162
16521
  });
16163
16522
  }
16523
+ const spentAttempts = preStartAttempts(deps.db.listRuns(), run.task_slug);
16524
+ if (spentAttempts >= PRE_START_ATTEMPT_LIMIT) {
16525
+ const why = `${PRE_START_MARK} ${spentAttempts} \u0440\u0430\u0437\u0430 \u043F\u043E\u0434\u0440\u044F\u0434 \u2014 ${SPAWN_PARK_MARK}, \u0447\u0435\u0442\u0432\u0451\u0440\u0442\u043E\u0433\u043E \u0437\u0430\u0445\u043E\u0434\u0430 \u043D\u0435 \u0434\u0435\u043B\u0430\u044E (TASWM-422)`;
16526
+ deps.db.updateRun(run.id, { status: "stopped", stop_reason: why });
16527
+ deps.db.addEvent(run.id, "step", why);
16528
+ const d = noteDefer(deps.spawnDefer ??= /* @__PURE__ */ new Map(), `pre-start-exhausted:${run.task_slug}`, why, GATE_LOG_HEARTBEAT_TICKS);
16529
+ if (d.log) console.error(`[overmind] ${run.task_slug}: ${why}${d.changed ? "" : ` \u2014 \u0435\u0449\u0451 \u0432\u0438\u0441\u0438\u0442, \u0442\u0438\u043A ${d.ticks}`}`);
16530
+ return "closed";
16531
+ }
16164
16532
  const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
16165
16533
  if (mergedUrl) {
16166
16534
  await closeDuplicateTaskRun(deps, run, mergedUrl);
@@ -16171,7 +16539,11 @@ async function trySpawnQueuedRun(deps, run) {
16171
16539
  }
16172
16540
  const depVerdicts = deps.db.depVerdicts(run.depends_on);
16173
16541
  const unmet = depVerdicts.filter((d) => !d.met);
16174
- if (unmet.length) return deferSpawn(deps, run, `\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438: ${unmet.map((d) => d.slug).join(", ")}`);
16542
+ if (unmet.length) {
16543
+ const lost = unmet.filter((d) => d.outcome === "unknown");
16544
+ const why = lost.length ? `\u0436\u0434\u0451\u0442 \u0431\u043B\u043E\u043A\u0435\u0440\u0430, \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u041D\u0415\u0422: ${lost.map((d) => d.slug).join(", ")} \u2014 \u043F\u043E \u044D\u0442\u043E\u0439 \u0441\u0441\u044B\u043B\u043A\u0435 \u043D\u0435\u0442 \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430; \u0441\u0430\u043C \u043D\u0435 \u043F\u043E\u0435\u0434\u0435\u0442, \u0441\u043D\u0438\u043C\u0438 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u044C (swarm_run_upsert{dependsOn:[]}) \u0438\u043B\u0438 \u0437\u0430\u0432\u0435\u0434\u0438 \u0431\u043B\u043E\u043A\u0435\u0440 \u0437\u0430\u043D\u043E\u0432\u043E (TASWM-399)` : `\u0436\u0434\u0451\u0442 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438: ${unmet.map((d) => d.slug).join(", ")}`;
16545
+ return deferSpawn(deps, run, why);
16546
+ }
16175
16547
  if (!await envGateAllows(deps, run)) return "skipped";
16176
16548
  if (!await projectQuotaGateAllows(deps, run)) return "skipped";
16177
16549
  const prodOverride = await prodOverrideReason(deps, run, ["queued"]);
@@ -16181,6 +16553,8 @@ async function trySpawnQueuedRun(deps, run) {
16181
16553
  }
16182
16554
  noteExhaustedDeps(deps, run, depVerdicts);
16183
16555
  await fillSuggestedArea(deps, run);
16556
+ const repoGap = unresolvedRepoReason(run.repo, deps.reposServed ? deps.reposServed() : []);
16557
+ if (repoGap) return await failBeforeStart(deps, run, repoGap);
16184
16558
  try {
16185
16559
  await deps.spawn(run);
16186
16560
  if (deps.spawnDefer) clearDefer(deps.spawnDefer, `run:${run.id}`);
@@ -16197,10 +16571,32 @@ async function trySpawnQueuedRun(deps, run) {
16197
16571
  return "skipped";
16198
16572
  }
16199
16573
  if (e instanceof AreaBusyError) return deferSpawn(deps, run, e.message);
16200
- deps.db.updateRun(run.id, { status: "failed" });
16201
- deps.db.addEvent(run.id, "failed", String(e));
16574
+ return await failBeforeStart(deps, run, String(e));
16575
+ }
16576
+ }
16577
+ async function failBeforeStart(deps, run, reason) {
16578
+ deps.db.updateRun(run.id, { status: "failed", stop_reason: `${PRE_START_MARK}: ${reason}` });
16579
+ deps.db.addEvent(run.id, "failed", `${PRE_START_MARK} \u2014 ${reason}`);
16580
+ const attempts = preStartAttempts(deps.db.listRuns(), run.task_slug);
16581
+ if (attempts < PRE_START_ATTEMPT_LIMIT) {
16582
+ narrate(deps, `${droneLabel(run)} \u2014 ${PRE_START_MARK} (\u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempts} \u0438\u0437 ${PRE_START_ATTEMPT_LIMIT}): ${reason}`);
16202
16583
  return "failed";
16203
16584
  }
16585
+ const parked = parkReason(attempts, reason);
16586
+ deps.db.updateRun(run.id, { stop_reason: parked });
16587
+ deps.db.addEvent(run.id, "step", parked);
16588
+ narrate(deps, `${run.task_slug}: ${parked}`);
16589
+ if (deps.parkTask) {
16590
+ const ok = await deps.parkTask(run, parked).catch(() => false);
16591
+ if (!ok) {
16592
+ deps.db.addEvent(
16593
+ run.id,
16594
+ "step",
16595
+ `${SPAWN_PARK_MARK}: \u0443\u0432\u0435\u0441\u0442\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0440\u0435\u0437\u043E\u043B\u0432\u0438\u0442\u0441\u044F) \u2014 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u0435\u0451 \u044D\u0442\u0430 \u043C\u0430\u0448\u0438\u043D\u0430 \u0432\u0441\u0451 \u0440\u0430\u0432\u043D\u043E \u043D\u0435 \u0431\u0443\u0434\u0435\u0442, \u043D\u043E \u0443\u0431\u0440\u0430\u0442\u044C \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u043F\u0440\u0438\u0434\u0451\u0442\u0441\u044F \u0440\u0443\u043A\u0430\u043C\u0438`
16596
+ );
16597
+ }
16598
+ }
16599
+ return "failed";
16204
16600
  }
16205
16601
  async function batchSpawnQueue(deps, droneCap) {
16206
16602
  for (const run of orderQueue(deps.db.listRuns("queued"))) {
@@ -16222,7 +16618,7 @@ async function resumeWatchdogParked(deps, droneCap) {
16222
16618
  }
16223
16619
  if (!candidate) return;
16224
16620
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
16225
- const worktreeExists = deps.worktreeExists ?? existsSync16;
16621
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
16226
16622
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
16227
16623
  const wt = candidate.worktree_path;
16228
16624
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
@@ -16391,7 +16787,7 @@ async function manageRelief(deps, now, droneCap, stormActive) {
16391
16787
  if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
16392
16788
  if (!deps.respawn) return;
16393
16789
  const wt = resumeCandidate.worktree_path;
16394
- const worktreeExists = deps.worktreeExists ?? existsSync16;
16790
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
16395
16791
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
16396
16792
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
16397
16793
  const mode = canContinue ? "continue" : "fresh";
@@ -16660,7 +17056,7 @@ function buildEscalationCandidates(db) {
16660
17056
  }
16661
17057
  async function tickOnce(deps) {
16662
17058
  const now = deps.now ? deps.now() : Date.now();
16663
- const worktreeExists = deps.worktreeExists ?? existsSync16;
17059
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
16664
17060
  const hasTranscript = deps.hasTranscript ?? executorHasSession;
16665
17061
  deps.sampleHost?.();
16666
17062
  if (deps.rehydrate) {
@@ -16991,6 +17387,13 @@ async function tickOnce(deps) {
16991
17387
  liveChildPpids = null;
16992
17388
  logTransientSpawnError("ps (livePpids)", e);
16993
17389
  }
17390
+ if (deps.sweepCost) {
17391
+ try {
17392
+ deps.sweepCost();
17393
+ } catch (e) {
17394
+ console.error("[overmind] \u0441\u0432\u0438\u043F \u0443\u0447\u0451\u0442\u0430 \u0441\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u0438 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
17395
+ }
17396
+ }
16994
17397
  for (const run of [...deps.db.listRuns("running"), ...deps.db.listRuns("blocked")]) {
16995
17398
  const alive = run.pid != null && deps.isAlive(run.pid);
16996
17399
  if (!alive && deps.db.takeRun && !await deps.db.takeRun(run.id)) continue;
@@ -17216,6 +17619,16 @@ async function tickOnce(deps) {
17216
17619
  if (deps.respawn && deps.resourcesOk && !stormActive) {
17217
17620
  await resurrectStormDead(deps, worktreeExists, hasTranscript, { gateOpen: deps.resourcesOk(), cap: droneCap });
17218
17621
  }
17622
+ try {
17623
+ const reaped = reapOrphanedQuestions(deps.db, now);
17624
+ if (reaped.withdrawn > 0) {
17625
+ console.log(
17626
+ `[overmind] \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B: \u0441\u043D\u044F\u0442\u043E ${reaped.withdrawn}, \u0437\u0430\u043C\u0435\u0442\u043A\u043E\u0439 \u0432 \u043B\u0435\u043D\u0442\u0443 \u0443\u0448\u043B\u043E ${reaped.noted}`
17627
+ );
17628
+ }
17629
+ } catch (e) {
17630
+ console.error("[overmind] \u0443\u0431\u043E\u0440\u043A\u0430 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0445 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e);
17631
+ }
17219
17632
  if (deps.anomalies && !deps.control?.paused) {
17220
17633
  const az = deps.anomalies;
17221
17634
  let ready = [];
@@ -17367,7 +17780,7 @@ async function resolveConflict(deps, reviewRun) {
17367
17780
  model: reviewRun.model,
17368
17781
  review_model: reviewRun.review_model
17369
17782
  });
17370
- const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
17783
+ const wt = join30(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
17371
17784
  try {
17372
17785
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
17373
17786
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -17424,7 +17837,7 @@ async function spawnReviewer(deps, reviewRun, files) {
17424
17837
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
17425
17838
  workspace_id: reviewRun.workspace_id
17426
17839
  });
17427
- const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
17840
+ const wt = join30(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
17428
17841
  try {
17429
17842
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
17430
17843
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -17499,7 +17912,7 @@ async function spawnVerifier(deps, verifiedRun, files) {
17499
17912
  // штамп цикла: фикс-итерацию проверяет свежий верификатор
17500
17913
  workspace_id: verifiedRun.workspace_id
17501
17914
  });
17502
- const wt = join29(deps.worktreesDir, basename4(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
17915
+ const wt = join30(deps.worktreesDir, basename4(repo), `${verifiedRun.task_slug}-verify-${verifier.id}`);
17503
17916
  try {
17504
17917
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
17505
17918
  await deps.git.setWorktreeIdentity(wt, verifier.name);
@@ -17537,7 +17950,7 @@ async function respawnRun(deps, run, mode, nudge) {
17537
17950
  ]);
17538
17951
  const base3 = effectiveBaseBranch(run.base_branch, projectBase);
17539
17952
  const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
17540
- if (!existsSync16(wt)) {
17953
+ if (!existsSync17(wt)) {
17541
17954
  const branch = runToSpawn.branch || runToSpawn.task_slug;
17542
17955
  const source = await addWorktree(repo, wt, branch, {
17543
17956
  ...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
@@ -17564,7 +17977,7 @@ async function respawnRun(deps, run, mode, nudge) {
17564
17977
  deps.db.updateRun(run.id, { status: "running", pid });
17565
17978
  }
17566
17979
  async function syncCrashedOnStartup(deps) {
17567
- const worktreeExists = deps.worktreeExists ?? existsSync16;
17980
+ const worktreeExists = deps.worktreeExists ?? existsSync17;
17568
17981
  let orphaned = 0;
17569
17982
  let merged = 0;
17570
17983
  let requeued = 0;
@@ -17653,10 +18066,12 @@ async function makeStore(repo, worktreesDir, repoFlag) {
17653
18066
  roots: scanRootsList,
17654
18067
  maxDepth: SWARM_SCAN_DEPTH,
17655
18068
  cachePath: REPO_MAP_PATH,
18069
+ // TASWM-418: кэш обязан протухать — иначе новый клон на машине не находится никогда.
18070
+ scanTtlMs: SWARM_REPO_SCAN_TTL_MS,
17656
18071
  state: repoMap,
17657
18072
  log: (m) => console.log(m)
17658
18073
  });
17659
- const repoRegistry = await rebuildRegistry();
18074
+ let repoRegistry = await rebuildRegistry();
17660
18075
  if (repoRegistry.length === 0) {
17661
18076
  console.warn(
17662
18077
  `[overmind] \u041D\u0415 \u041D\u0410\u0428\u0401\u041B \u041D\u0418 \u041E\u0414\u041D\u041E\u0413\u041E \u0420\u0415\u041F\u041E\u0417\u0418\u0422\u041E\u0420\u0418\u042F \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u2014 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0431\u0440\u0430\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0443\u0442: \u0438\u0445 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043F\u0440\u0438\u0445\u043E\u0434\u0438\u0442 \u0438\u043C\u0435\u043D\u0435\u043C owner/repo, \u0430 \u0441\u043E\u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u0441 \u0447\u0435\u043C. \u0418\u0441\u043A\u0430\u043B \u0432: ${scanRootsList.slice(0, 6).join(", ")}. ` + // TASWM-336: почему пусто — три РАЗНЫЕ причины, и лечатся они по-разному. Одна
@@ -17686,8 +18101,13 @@ async function makeStore(repo, worktreesDir, repoFlag) {
17686
18101
  // Периодическая пересборка (шаг 0a4 тика): применяем новый реестр к ЖИВОМУ клиенту —
17687
18102
  // и список для claim/heartbeat, и обратную карту owner/repo → путь.
17688
18103
  refreshRepoMap: async () => {
17689
- client.setRepoRegistry(await rebuildRegistry());
17690
- }
18104
+ repoRegistry = await rebuildRegistry();
18105
+ client.setRepoRegistry(repoRegistry);
18106
+ },
18107
+ // TASWM-422: что машина обслуживает ПРЯМО СЕЙЧАС — читается из ЖИВОГО реестра (он
18108
+ // пересобирается тиком выше), чтобы причина «клона нет на этой машине» называла
18109
+ // актуальный список, а не снимок на старте демона.
18110
+ reposServed: () => repoRegistry.map((e) => e.repoFullName)
17691
18111
  };
17692
18112
  }
17693
18113
  var crashGuardsInstalled = false;
@@ -17707,22 +18127,22 @@ async function migrateWorktreesAtStartup() {
17707
18127
  legacyDir: WORKTREES_LEGACY_DIR,
17708
18128
  targetDir: WORKTREES_DIR,
17709
18129
  envOverride: process.env.SWARM_WORKTREES || "",
17710
- exists: (d) => existsSync16(d),
18130
+ exists: (d) => existsSync17(d),
17711
18131
  isNonEmptyDir: (d) => {
17712
18132
  try {
17713
- return readdirSync12(d).length > 0;
18133
+ return readdirSync13(d).length > 0;
17714
18134
  } catch {
17715
18135
  return false;
17716
18136
  }
17717
18137
  },
17718
18138
  liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
17719
- rename: (from, to) => renameSync10(from, to),
18139
+ rename: (from, to) => renameSync11(from, to),
17720
18140
  repairMoved: (t) => repairMovedWorktrees(t),
17721
18141
  log: (m) => console.log(m)
17722
18142
  });
17723
18143
  } catch (e) {
17724
18144
  console.error("[migrate] \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u044F worktrees \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C \u2014 \u0444\u043E\u043B\u0431\u044D\u043A:", e);
17725
- return existsSync16(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
18145
+ return existsSync17(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
17726
18146
  }
17727
18147
  }
17728
18148
  async function reportWorkspaceAccess(tl, db) {
@@ -17742,6 +18162,14 @@ async function runDaemon(opts) {
17742
18162
  const daemonStart = captureDaemonStart();
17743
18163
  const latestRelease = fetchLatestVersion();
17744
18164
  const worktreesDir = await migrateWorktreesAtStartup();
18165
+ {
18166
+ const swept = sweepLegacyRunLogs(RUN_LOGS_DIR);
18167
+ if (swept.moved || swept.failed) {
18168
+ console.log(
18169
+ `[overmind] run-logs: ${swept.moved} \u043A\u0430\u0442\u0430\u043B\u043E\u0433(\u043E\u0432) \u0441\u0442\u0430\u0440\u043E\u0439 \u0440\u0430\u0441\u043A\u043B\u0430\u0434\u043A\u0438 (\u0430\u0434\u0440\u0435\u0441\u0430\u0446\u0438\u044F \u043D\u043E\u043C\u0435\u0440\u043E\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u0430) \u0443\u0431\u0440\u0430\u043D\u043E \u0432 ${swept.legacyDir}` + (swept.failed ? `, \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0435\u0440\u0435\u043D\u0435\u0441\u0442\u0438: ${swept.failed}` : "")
18170
+ );
18171
+ }
18172
+ }
17745
18173
  if (!opts.repo) {
17746
18174
  const remembered = effectiveRepoPaths("", loadRememberedRepos(SWARM_HOME))[0];
17747
18175
  if (remembered) opts.repo = remembered;
@@ -17752,7 +18180,8 @@ async function runDaemon(opts) {
17752
18180
  host,
17753
18181
  repoMap: repoMapState,
17754
18182
  refreshRepoMap: rebuildRepoMap,
17755
- prodMcp
18183
+ prodMcp,
18184
+ reposServed
17756
18185
  } = await makeStore(opts.repo, worktreesDir, opts.repoFlag);
17757
18186
  await preflightAuth();
17758
18187
  const outdated = outdatedLine(PACKAGE_VERSION, await latestRelease);
@@ -17798,6 +18227,8 @@ async function runDaemon(opts) {
17798
18227
  overmindEntry: THIS_FILE,
17799
18228
  dbPath: DB_PATH,
17800
18229
  runLogsDir: RUN_LOGS_DIR,
18230
+ // TASWM-419: чем именуется каталог лога прогона — стабильные ключи из стора, не номер.
18231
+ runLogKeys: (run) => db.runLogKeys(run.id),
17801
18232
  // Кем исполнять (TASWM-349) — настройка SWARM_EXECUTOR, один резолв на весь демон.
17802
18233
  executor: EXECUTOR,
17803
18234
  droneIsolation: DRONE_ISOLATION,
@@ -17865,6 +18296,7 @@ async function runDaemon(opts) {
17865
18296
  lastOwnCapacity,
17866
18297
  hostResourcesForProd(),
17867
18298
  { active: stormActive, reason: stormStartedAt ? `\u043D\u043E\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441 ${stormStartedAt} UTC` : null },
18299
+ worktreesDir,
17868
18300
  // TASWM-393: ВХОД в Taskless — та же природа, что пауза и шторм: состояние
17869
18301
  // машины, о котором знает только она. Пульс идёт и без входа, поэтому «на
17870
18302
  // связи» без этого поля читалось как «возьмёт работу» (12.08 — шесть прогонов
@@ -17883,7 +18315,13 @@ async function runDaemon(opts) {
17883
18315
  {
17884
18316
  runLogsDir: RUN_LOGS_DIR,
17885
18317
  daemonLogPath: DAEMON_LOG_PATH,
17886
- localRunId: (prodRunId) => client.localRunIdForProd(prodRunId),
18318
+ // TASWM-419: каталог лога адресуется стабильными ключами прогона, а не его
18319
+ // локальным номером — номера переиспользуются между жизнями зеркала, и
18320
+ // оператору приезжал хвост чужого прогона.
18321
+ runLogKeys: (prodRunId) => {
18322
+ const local = client.localRunIdForProd(prodRunId);
18323
+ return local == null ? [] : db.runLogKeys(local);
18324
+ },
17887
18325
  deliver: (requestId, payload) => client.deliverLog(requestId, payload)
17888
18326
  },
17889
18327
  reply.logRequests
@@ -17941,6 +18379,7 @@ async function runDaemon(opts) {
17941
18379
  questionUnansweredMs: SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
17942
18380
  outboxStuckMs: SWARM_ANOMALY_OUTBOX_STUCK_MS,
17943
18381
  resourceGateStallMs: SWARM_ANOMALY_RESOURCE_GATE_STALL_MS,
18382
+ depMissingMs: SWARM_ANOMALY_DEP_MISSING_MS,
17944
18383
  runDownWindowMs: SWARM_ANOMALY_RUN_DOWN_WINDOW_MS
17945
18384
  },
17946
18385
  autoReviewEnabled: SWARM_AUTO_REVIEW,
@@ -18200,6 +18639,30 @@ async function runDaemon(opts) {
18200
18639
  taskClosed: (slug, ws) => tl.forWorkspace(ws).getTask(slug).then((t) => t ? isClosingTaskCategory(t.statusCategory) : null)
18201
18640
  } : {},
18202
18641
  finishTask: (slug, ws) => tlFor(ws)?.finishTask(slug) ?? Promise.resolve(),
18642
+ ...reposServed ? { reposServed } : {},
18643
+ // TASWM-422: ОТЛОЖИТЬ задачу, по которой рой трижды не сумел поднять дрона. Механика —
18644
+ // ровно та же, что у return_task дрона (TASWM-412): `claimableTaskWhere` (api) требует
18645
+ // категорию unstarted, и start_task её ломает — задача перестаёт быть claimable, то есть
18646
+ // авто-интейк больше не берёт её раз в минуту. Успех проверяем ФАКТОМ (перечитываем
18647
+ // задачу), а не кодом возврата: клиент глушит отказ сервера внутрь (safeCall), и по
18648
+ // возврату «отложил» было бы неотличимо от «сказал, но не доехало» — то самое молчание,
18649
+ // из-за которого 17.08 круг и крутился.
18650
+ parkTask: async (run, reason) => {
18651
+ const client = tlFor(run.workspace_id);
18652
+ if (!client) return false;
18653
+ await client.addComment(
18654
+ run.task_slug,
18655
+ `${reason}
18656
+
18657
+ \u0417\u0430\u0434\u0430\u0447\u0430 \u0441\u043D\u044F\u0442\u0430 \u0441 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u0440\u043E\u044F \u2014 \u043F\u043E\u043A\u0430 \u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u043D\u0435 \u0443\u0441\u0442\u0440\u0430\u043D\u0435\u043D\u0430, \u0431\u0440\u0430\u0442\u044C \u0435\u0451 \u0441\u043D\u043E\u0432\u0430 \u0441\u043C\u044B\u0441\u043B\u0430 \u043D\u0435\u0442. \u0420\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C \u0438 \u0432\u0435\u0440\u043D\u0438 \u0432 \u0431\u044D\u043A\u043B\u043E\u0433.`,
18658
+ { swarmKind: "report", swarmDroneName: run.name }
18659
+ ).catch(() => {
18660
+ });
18661
+ await client.startTask(run.task_slug).catch(() => {
18662
+ });
18663
+ const after = await client.getTask(run.task_slug).catch(() => null);
18664
+ return after != null && after.statusCategory !== "unstarted";
18665
+ },
18203
18666
  // TASWM-272: отчёт/вердикт-компенсация при усыновлении мёртвого дрона — реальный
18204
18667
  // TasklessClient.addComment + gh/git хелперы. Без tl (нет токена) ключи не задаём —
18205
18668
  // postAdoptionReport/postScrapedVerdictReport становятся no-op, как раньше.
@@ -18221,9 +18684,12 @@ async function runDaemon(opts) {
18221
18684
  // TASWM-364: swarmModels — ТОТ ЖЕ снимок, что уехал в launcher: цена токенов считается
18222
18685
  // по модели, на которой прогон реально работал.
18223
18686
  accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR, swarmModels: () => lastSwarmModels }),
18687
+ // TASWM-413: тот же аккаунтант, но поводом служит завершённый ЗАПУСК, а не смерть
18688
+ // прогона в running — иначе сданный через done() прогон не получает цену никогда.
18689
+ sweepCost: makeCostSweeper({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR, swarmModels: () => lastSwarmModels, isAlive: pidAlive }),
18224
18690
  // Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
18225
18691
  // (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
18226
- exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
18692
+ exitOutcome: makeExitClassifier(RUN_LOGS_DIR, (runId) => db.runLogKeys(runId), EXECUTOR),
18227
18693
  // Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
18228
18694
  livePpids,
18229
18695
  // Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
@@ -18370,7 +18836,7 @@ async function runDaemon(opts) {
18370
18836
  mode,
18371
18837
  nudge
18372
18838
  ),
18373
- worktreeExists: (p) => existsSync16(p),
18839
+ worktreeExists: (p) => existsSync17(p),
18374
18840
  hasTranscript: executorHasSession,
18375
18841
  // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
18376
18842
  // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary