@taskless-app/swarm 0.4.10 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +136 -16
  2. package/dist/cli.js +3016 -806
  3. package/dist/overmind.js +2334 -573
  4. package/package.json +1 -1
package/dist/overmind.js CHANGED
@@ -397,9 +397,14 @@ var init_windows = __esm({
397
397
  });
398
398
 
399
399
  // ../swarm-core/src/slug.ts
400
+ function isTaskSlug(s) {
401
+ return !!s && TASK_SLUG_RE.test(s);
402
+ }
403
+ var TASK_SLUG_RE;
400
404
  var init_slug = __esm({
401
405
  "../swarm-core/src/slug.ts"() {
402
406
  "use strict";
407
+ TASK_SLUG_RE = /^(?:[A-Za-z0-9]{1,8}-\d+|#[A-Za-z0-9]{4,})$/;
403
408
  }
404
409
  });
405
410
 
@@ -432,7 +437,7 @@ var init_text = __esm({
432
437
  "use strict";
433
438
  ANSI_CSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
434
439
  CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
435
- EVENT_TEXT_MAX = 4e3;
440
+ EVENT_TEXT_MAX = 16e3;
436
441
  }
437
442
  });
438
443
 
@@ -715,6 +720,14 @@ var init_hosts = __esm({
715
720
  }
716
721
  });
717
722
 
723
+ // ../swarm-core/src/hostPool.ts
724
+ var init_hostPool = __esm({
725
+ "../swarm-core/src/hostPool.ts"() {
726
+ "use strict";
727
+ init_hosts();
728
+ }
729
+ });
730
+
718
731
  // ../swarm-core/src/hostCommands.ts
719
732
  function isHostCommandKind(v) {
720
733
  return typeof v === "string" && HOST_COMMAND_KINDS.includes(v);
@@ -724,17 +737,20 @@ function isHostCommandExpired(expiresAt, nowMs) {
724
737
  const at = Date.parse(expiresAt);
725
738
  return Number.isFinite(at) && at <= nowMs;
726
739
  }
740
+ function isRunScopedCommand(kind) {
741
+ return RUN_SCOPED.has(kind);
742
+ }
727
743
  function hostCommandLabel(cmd) {
728
744
  const base3 = HOST_COMMAND_LABELS[cmd.kind];
729
745
  if (cmd.kind === "set_max_drones" && cmd.maxDrones != null) return `${base3} \u2192 ${cmd.maxDrones}`;
730
- if (cmd.kind === "stop_run" && cmd.runSlug) return `${base3} ${cmd.runSlug}`;
746
+ if (isRunScopedCommand(cmd.kind) && cmd.runSlug) return `${base3} ${cmd.runSlug}`;
731
747
  return base3;
732
748
  }
733
- var HOST_COMMAND_KINDS, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, UPDATE_STEP_LABELS, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_LABELS, MAX_DRONES_LIMIT;
749
+ var HOST_COMMAND_KINDS, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, UPDATE_STEP_LABELS, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_LABELS, RUN_SCOPED_COMMAND_KINDS, RUN_SCOPED, MAX_DRONES_LIMIT;
734
750
  var init_hostCommands = __esm({
735
751
  "../swarm-core/src/hostCommands.ts"() {
736
752
  "use strict";
737
- HOST_COMMAND_KINDS = ["pause", "resume", "restart", "update", "set_max_drones", "stop_run"];
753
+ HOST_COMMAND_KINDS = ["pause", "resume", "restart", "update", "set_max_drones", "stop_run", "wake_run"];
738
754
  HOST_COMMAND_STATUSES = ["pending", "delivered", "running", "done", "failed", "rejected", "expired"];
739
755
  TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "rejected", "expired"]);
740
756
  HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
@@ -753,8 +769,11 @@ var init_hostCommands = __esm({
753
769
  restart: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A",
754
770
  update: "\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0434\u043E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u043A\u043E\u0434\u0430",
755
771
  set_max_drones: "\u0441\u043C\u0435\u043D\u0430 \u0447\u0438\u0441\u043B\u0430 \u0434\u0440\u043E\u043D\u043E\u0432",
756
- stop_run: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430"
772
+ stop_run: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
773
+ wake_run: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0441 \u0442\u043E\u0433\u043E \u0436\u0435 \u043C\u0435\u0441\u0442\u0430"
757
774
  };
775
+ RUN_SCOPED_COMMAND_KINDS = ["stop_run", "wake_run"];
776
+ RUN_SCOPED = new Set(RUN_SCOPED_COMMAND_KINDS);
758
777
  MAX_DRONES_LIMIT = 8;
759
778
  }
760
779
  });
@@ -769,6 +788,436 @@ var init_relief = __esm({
769
788
  }
770
789
  });
771
790
 
791
+ // ../swarm-core/src/cost.ts
792
+ var EMPTY_TASK_COST;
793
+ var init_cost = __esm({
794
+ "../swarm-core/src/cost.ts"() {
795
+ "use strict";
796
+ EMPTY_TASK_COST = Object.freeze({
797
+ costUsd: 0,
798
+ tokensIn: 0,
799
+ tokensOut: 0,
800
+ durationMs: 0,
801
+ runs: 0,
802
+ respawns: 0
803
+ });
804
+ }
805
+ });
806
+
807
+ // ../swarm-core/src/runGuards.ts
808
+ var DEFAULT_RUN_LIMITS;
809
+ var init_runGuards = __esm({
810
+ "../swarm-core/src/runGuards.ts"() {
811
+ "use strict";
812
+ DEFAULT_RUN_LIMITS = {
813
+ maxCostUsd: 20,
814
+ runTimeoutMs: 8 * 60 * 6e4,
815
+ stepTimeoutMs: 90 * 6e4,
816
+ lostMs: 30 * 6e4
817
+ };
818
+ }
819
+ });
820
+
821
+ // ../swarm-core/src/codeZones.ts
822
+ function hasZoneModel(m) {
823
+ return m.tasks > 0 && m.zones.length > 0;
824
+ }
825
+ function testFileFeature(basename5) {
826
+ const head = basename5.replace(/\.(test|spec)\.[a-z]+$/i, "").split(".")[0].toLowerCase();
827
+ return head && !GENERIC_DIRS.has(head) ? head : null;
828
+ }
829
+ function pathZoneCandidate(path) {
830
+ const parts = path.split("/").filter(Boolean);
831
+ if (parts.length <= 1) return "root";
832
+ if (WORKSPACE_DIRS.has(parts[0]) && parts.length >= 3) {
833
+ const pkg = parts[1];
834
+ if (parts[2] === "src" && parts.length >= 5) {
835
+ const feature = parts[3];
836
+ if (!GENERIC_DIRS.has(feature)) return feature;
837
+ }
838
+ if (GENERIC_TEST_DIRS.has(parts[2])) {
839
+ const feature = testFileFeature(parts[parts.length - 1]);
840
+ if (feature) return feature;
841
+ }
842
+ return pkg;
843
+ }
844
+ if (parts[0] === "src" && parts.length >= 3 && !GENERIC_DIRS.has(parts[1])) return parts[1];
845
+ return parts[0];
846
+ }
847
+ function dirOf(path) {
848
+ const i = path.lastIndexOf("/");
849
+ return i < 0 ? "" : path.slice(0, i);
850
+ }
851
+ function tokenize(text) {
852
+ const out = [];
853
+ for (const raw of text.split(/[^\p{L}\p{N}]+/u)) {
854
+ if (!raw) continue;
855
+ for (const piece of raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")) {
856
+ const t = piece.toLowerCase();
857
+ if (t.length < 3 || STOP_TERMS.has(t) || /^\d+$/.test(t)) continue;
858
+ out.push(t);
859
+ }
860
+ }
861
+ return out;
862
+ }
863
+ function normalizeZoneName(raw) {
864
+ if (!raw) return null;
865
+ const s = raw.trim().toLowerCase().replace(/^area:/, "");
866
+ return s || null;
867
+ }
868
+ function bump(m, k, by = 1) {
869
+ m.set(k, (m.get(k) ?? 0) + by);
870
+ }
871
+ function topKey(m) {
872
+ let best = null;
873
+ let bestN = 0;
874
+ let total = 0;
875
+ for (const [k, n] of m) {
876
+ total += n;
877
+ if (n > bestN) {
878
+ best = k;
879
+ bestN = n;
880
+ }
881
+ }
882
+ return best === null || total === 0 ? null : { key: best, count: bestN, share: bestN / total };
883
+ }
884
+ function buildZoneModel(entries, opts = {}) {
885
+ const o = { ...DEFAULTS, ...opts };
886
+ const tasks = entries.filter((e) => e.files.length > 0);
887
+ if (tasks.length < o.minTasks) return EMPTY_ZONE_MODEL;
888
+ const fileTasks = /* @__PURE__ */ new Map();
889
+ const candidate = /* @__PURE__ */ new Map();
890
+ for (const t of tasks) {
891
+ for (const f of new Set(t.files)) {
892
+ bump(fileTasks, f);
893
+ if (!candidate.has(f)) candidate.set(f, pathZoneCandidate(f));
894
+ }
895
+ }
896
+ const zoneOf = new Map(candidate);
897
+ for (const [file, seen] of fileTasks) {
898
+ if (seen < 2) continue;
899
+ const votes = /* @__PURE__ */ new Map();
900
+ for (const t of tasks) {
901
+ const files = [...new Set(t.files)];
902
+ if (!files.includes(file)) continue;
903
+ const others = files.filter((f) => f !== file);
904
+ if (!others.length) continue;
905
+ for (const f of others) bump(votes, candidate.get(f), 1 / others.length);
906
+ }
907
+ const top = topKey(votes);
908
+ if (top && top.share >= 0.6 && top.key !== candidate.get(file)) zoneOf.set(file, top.key);
909
+ }
910
+ const voteWeight = (file) => 1 / Math.log2(2 + (fileTasks.get(file) ?? 1));
911
+ const taskZonePre = /* @__PURE__ */ new Map();
912
+ for (const t of tasks) {
913
+ const votes = /* @__PURE__ */ new Map();
914
+ for (const f of new Set(t.files)) bump(votes, zoneOf.get(f), voteWeight(f));
915
+ const top = topKey(votes);
916
+ if (top) taskZonePre.set(t.slug, top.key);
917
+ }
918
+ const fileZones = /* @__PURE__ */ new Map();
919
+ for (const t of tasks) {
920
+ const tz = taskZonePre.get(t.slug);
921
+ if (!tz) continue;
922
+ for (const f of new Set(t.files)) {
923
+ let m = fileZones.get(f);
924
+ if (!m) fileZones.set(f, m = /* @__PURE__ */ new Map());
925
+ bump(m, tz);
926
+ }
927
+ }
928
+ const spineSet = /* @__PURE__ */ new Set();
929
+ for (const [file, seen] of fileTasks) {
930
+ if (seen < o.spineMinTasks) continue;
931
+ const zonesOfFile = fileZones.get(file);
932
+ const dominant = zonesOfFile ? topKey(zonesOfFile) : null;
933
+ if ((zonesOfFile?.size ?? 0) < o.spineMinZones) continue;
934
+ if (dominant && dominant.share >= o.spineDominantShare) continue;
935
+ spineSet.add(file);
936
+ }
937
+ const fileZone = /* @__PURE__ */ new Map();
938
+ for (const [f, z2] of zoneOf) if (!spineSet.has(f)) fileZone.set(f, z2);
939
+ const taskZone = /* @__PURE__ */ new Map();
940
+ for (const t of tasks) {
941
+ const votes = /* @__PURE__ */ new Map();
942
+ for (const f of new Set(t.files)) {
943
+ const z2 = fileZone.get(f);
944
+ if (z2) bump(votes, z2, voteWeight(f));
945
+ }
946
+ const top = topKey(votes);
947
+ taskZone.set(t.slug, top ? top.key : SPINE_ZONE);
948
+ }
949
+ const labelVotes = /* @__PURE__ */ new Map();
950
+ for (const t of tasks) {
951
+ const manual = normalizeZoneName(t.area);
952
+ const z2 = taskZone.get(t.slug);
953
+ if (!manual || !z2 || z2 === SPINE_ZONE || manual === SPINE_ZONE) continue;
954
+ let m = labelVotes.get(z2);
955
+ if (!m) labelVotes.set(z2, m = /* @__PURE__ */ new Map());
956
+ bump(m, manual);
957
+ }
958
+ const rename = /* @__PURE__ */ new Map();
959
+ const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
960
+ const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
961
+ const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
962
+ return sum(b[1]) - sum(a[1]) || a[0].localeCompare(b[0]);
963
+ });
964
+ for (const [zone, votes] of clustersByEvidence) {
965
+ const top = topKey(votes);
966
+ if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
967
+ rename.set(zone, top.key);
968
+ takenNames.add(top.key);
969
+ }
970
+ const renamed = (z2) => rename.get(z2) ?? z2;
971
+ if (rename.size) {
972
+ for (const [f, z2] of fileZone) fileZone.set(f, renamed(z2));
973
+ for (const [s, z2] of taskZone) taskZone.set(s, renamed(z2));
974
+ }
975
+ const zoneTerms = /* @__PURE__ */ new Map();
976
+ const addTerm = (zone, term, w) => {
977
+ let m = zoneTerms.get(zone);
978
+ if (!m) zoneTerms.set(zone, m = /* @__PURE__ */ new Map());
979
+ bump(m, term, w);
980
+ };
981
+ for (const [f, z2] of fileZone) for (const term of tokenize(f)) addTerm(z2, term, 1);
982
+ for (const f of spineSet) for (const term of tokenize(f)) addTerm(SPINE_ZONE, term, 1);
983
+ for (const t of tasks) {
984
+ const z2 = taskZone.get(t.slug);
985
+ if (!z2 || !t.text) continue;
986
+ for (const term of tokenize(t.text)) addTerm(z2, term, 2);
987
+ }
988
+ const termZones = /* @__PURE__ */ new Map();
989
+ for (const terms of zoneTerms.values()) for (const term of terms.keys()) bump(termZones, term);
990
+ const zoneFiles = /* @__PURE__ */ new Map();
991
+ for (const [f, z2] of fileZone) {
992
+ const arr = zoneFiles.get(z2) ?? [];
993
+ arr.push(f);
994
+ zoneFiles.set(z2, arr);
995
+ }
996
+ const zoneTaskCount = /* @__PURE__ */ new Map();
997
+ for (const z2 of taskZone.values()) bump(zoneTaskCount, z2);
998
+ const byFreq = (a, b) => (fileTasks.get(b) ?? 0) - (fileTasks.get(a) ?? 0) || a.localeCompare(b);
999
+ const zones = [...zoneFiles.entries()].map(([zone, files]) => ({ zone, tasks: zoneTaskCount.get(zone) ?? 0, files: files.sort(byFreq) })).sort((a, b) => b.tasks - a.tasks || a.zone.localeCompare(b.zone));
1000
+ const spine = [...spineSet].sort(byFreq);
1001
+ if (spineSet.size) zones.push({ zone: SPINE_ZONE, tasks: zoneTaskCount.get(SPINE_ZONE) ?? 0, files: spine });
1002
+ const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
1003
+ return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
1004
+ }
1005
+ function zoneForFiles(model, files) {
1006
+ if (!hasZoneModel(model) || !files.length) return null;
1007
+ const spineSet = new Set(model.spine);
1008
+ const touchedSpine = files.filter((f) => spineSet.has(f));
1009
+ const votes = /* @__PURE__ */ new Map();
1010
+ for (const f of new Set(files)) {
1011
+ const z2 = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
1012
+ if (z2) bump(votes, z2);
1013
+ }
1014
+ const top = topKey(votes);
1015
+ if (!top) {
1016
+ return {
1017
+ zone: SPINE_ZONE,
1018
+ confidence: 1,
1019
+ 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(", ")})`,
1020
+ spine: true
1021
+ };
1022
+ }
1023
+ return {
1024
+ zone: top.key,
1025
+ confidence: top.share,
1026
+ 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}`,
1027
+ spine: touchedSpine.length > 0
1028
+ };
1029
+ }
1030
+ function suggestZonesFromText(model, text, limit = 3) {
1031
+ if (!hasZoneModel(model)) return [];
1032
+ const terms = tokenize(text);
1033
+ if (!terms.length) return [];
1034
+ const zoneCount = model.zoneTerms.size || 1;
1035
+ const idf = (term) => Math.log(1 + zoneCount / (model.termZones.get(term) ?? 1));
1036
+ const scores = /* @__PURE__ */ new Map();
1037
+ const hits = /* @__PURE__ */ new Map();
1038
+ const tasksOfZone = new Map(model.zones.map((z2) => [z2.zone, z2.tasks]));
1039
+ for (const [zone, dict] of model.zoneTerms) {
1040
+ if ((tasksOfZone.get(zone) ?? 0) < 2) continue;
1041
+ let dot = 0;
1042
+ let norm = 0;
1043
+ const matched = [];
1044
+ for (const [term, w] of dict) {
1045
+ const v = Math.log(1 + w) * idf(term);
1046
+ norm += v * v;
1047
+ }
1048
+ if (!norm) continue;
1049
+ for (const term of new Set(terms)) {
1050
+ const w = dict.get(term);
1051
+ if (!w) continue;
1052
+ dot += Math.log(1 + w) * idf(term) * idf(term);
1053
+ matched.push(term);
1054
+ }
1055
+ const score = dot / Math.sqrt(norm) * Math.log(1 + (tasksOfZone.get(zone) ?? 1));
1056
+ if (score > 0) {
1057
+ scores.set(zone, score);
1058
+ hits.set(zone, matched);
1059
+ }
1060
+ }
1061
+ const total = [...scores.values()].reduce((a, b) => a + b, 0);
1062
+ if (!total) return [];
1063
+ return [...scores.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([zone, s]) => ({
1064
+ zone,
1065
+ confidence: s / total,
1066
+ why: `\u0441\u043E\u0432\u043F\u0430\u043B\u043E \u043F\u043E \u0441\u043B\u043E\u0432\u0430\u043C: ${(hits.get(zone) ?? []).slice(0, 5).join(", ")}`,
1067
+ spine: zone === SPINE_ZONE
1068
+ }));
1069
+ }
1070
+ function similarTasks(model, text, limit = 5) {
1071
+ const terms = new Set(tokenize(text));
1072
+ if (!terms.size) return [];
1073
+ const scored = [];
1074
+ for (const [i, e] of model.entries.entries()) {
1075
+ const other = model.entryTerms[i];
1076
+ if (!other?.size) continue;
1077
+ let inter = 0;
1078
+ for (const t of terms) if (other.has(t)) inter += Math.log(1 + (model.zoneTerms.size || 1) / (model.termZones.get(t) ?? 1));
1079
+ if (inter <= 0) continue;
1080
+ scored.push({ entry: e, score: inter / Math.sqrt(terms.size * other.size) });
1081
+ }
1082
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit);
1083
+ }
1084
+ function predictFiles(model, opts) {
1085
+ if (!hasZoneModel(model)) return [];
1086
+ const limit = opts.limit ?? 40;
1087
+ const best = /* @__PURE__ */ new Map();
1088
+ const put = (path, weight, why) => {
1089
+ const cur = best.get(path);
1090
+ if (!cur || cur.weight < weight) best.set(path, { path, weight, why });
1091
+ };
1092
+ if (opts.text) {
1093
+ const sim = similarTasks(model, opts.text, opts.similar ?? 5);
1094
+ const topScore = sim[0]?.score ?? 0;
1095
+ for (const { entry, score } of sim) {
1096
+ const w = topScore > 0 ? Math.min(1, score / topScore) : 0;
1097
+ for (const f of new Set(entry.files)) put(f, Math.max(0.35, w * 0.9), `\u043F\u043E\u0445\u043E\u0436\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 ${entry.slug}`);
1098
+ }
1099
+ }
1100
+ const zone = normalizeZoneName(opts.zone) ?? (opts.text ? suggestZonesFromText(model, opts.text)[0]?.zone ?? null : null);
1101
+ if (zone) {
1102
+ const stat = model.zones.find((z2) => z2.zone === zone);
1103
+ if (stat) stat.files.slice(0, limit).forEach((f, i) => put(f, Math.max(0.12, 0.45 - i * 0.02), `\u0437\u043E\u043D\u0430 ${zone}`));
1104
+ }
1105
+ return [...best.values()].sort((a, b) => b.weight - a.weight || a.path.localeCompare(b.path)).slice(0, limit);
1106
+ }
1107
+ function overlapRisk(predicted, actual, opts = {}) {
1108
+ const o = { ...OVERLAP_DEFAULTS, ...opts };
1109
+ const actualSet = new Set(actual);
1110
+ const actualDirs = new Set([...actualSet].map(dirOf).filter(Boolean));
1111
+ const totalWeight = predicted.reduce((a, p) => a + p.weight, 0);
1112
+ if (!totalWeight) return { overlap: false, score: 0, sharedFiles: [], sharedDirs: [] };
1113
+ let hit = 0;
1114
+ let strong = false;
1115
+ const sharedFiles = [];
1116
+ const sharedDirs = /* @__PURE__ */ new Set();
1117
+ for (const p of predicted) {
1118
+ if (actualSet.has(p.path)) {
1119
+ hit += p.weight;
1120
+ sharedFiles.push(p.path);
1121
+ if (p.weight >= o.strongFile) strong = true;
1122
+ continue;
1123
+ }
1124
+ const d = dirOf(p.path);
1125
+ if (d && actualDirs.has(d)) {
1126
+ hit += p.weight * 0.25;
1127
+ sharedDirs.add(d);
1128
+ }
1129
+ }
1130
+ const score = Math.min(1, hit / totalWeight);
1131
+ return {
1132
+ overlap: strong || score >= o.threshold,
1133
+ score,
1134
+ sharedFiles: sharedFiles.slice(0, 10),
1135
+ sharedDirs: [...sharedDirs].slice(0, 5)
1136
+ };
1137
+ }
1138
+ var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS, OVERLAP_DEFAULTS;
1139
+ var init_codeZones = __esm({
1140
+ "../swarm-core/src/codeZones.ts"() {
1141
+ "use strict";
1142
+ SPINE_ZONE = "core";
1143
+ DEFAULTS = {
1144
+ minTasks: 5,
1145
+ spineMinTasks: 3,
1146
+ spineMinZones: 3,
1147
+ spineDominantShare: 0.6
1148
+ };
1149
+ EMPTY_ZONE_MODEL = {
1150
+ tasks: 0,
1151
+ zones: [],
1152
+ fileZone: /* @__PURE__ */ new Map(),
1153
+ spine: [],
1154
+ taskZone: /* @__PURE__ */ new Map(),
1155
+ zoneTerms: /* @__PURE__ */ new Map(),
1156
+ termZones: /* @__PURE__ */ new Map(),
1157
+ entries: [],
1158
+ entryTerms: []
1159
+ };
1160
+ GENERIC_DIRS = /* @__PURE__ */ new Set([
1161
+ "src",
1162
+ "source",
1163
+ "lib",
1164
+ "libs",
1165
+ "util",
1166
+ "utils",
1167
+ "common",
1168
+ "internal",
1169
+ "app",
1170
+ "test",
1171
+ "tests",
1172
+ "__tests__",
1173
+ "spec",
1174
+ "specs",
1175
+ "index"
1176
+ ]);
1177
+ WORKSPACE_DIRS = /* @__PURE__ */ new Set(["packages", "apps", "services", "libs", "modules"]);
1178
+ GENERIC_TEST_DIRS = /* @__PURE__ */ new Set(["tests", "test", "__tests__", "spec"]);
1179
+ STOP_TERMS = /* @__PURE__ */ new Set([
1180
+ "the",
1181
+ "and",
1182
+ "for",
1183
+ "with",
1184
+ "from",
1185
+ "into",
1186
+ "src",
1187
+ "test",
1188
+ "tests",
1189
+ "index",
1190
+ "packages",
1191
+ "package",
1192
+ "json",
1193
+ "yml",
1194
+ "yaml",
1195
+ "tsx",
1196
+ "mjs",
1197
+ "cjs",
1198
+ "\u0447\u0442\u043E",
1199
+ "\u043A\u0430\u043A",
1200
+ "\u0434\u043B\u044F",
1201
+ "\u043F\u0440\u0438",
1202
+ "\u044D\u0442\u043E",
1203
+ "\u0438\u043B\u0438",
1204
+ "\u0435\u0441\u043B\u0438",
1205
+ "\u0447\u0442\u043E\u0431\u044B",
1206
+ "\u0442\u043E\u0436\u0435",
1207
+ "\u0435\u0433\u043E",
1208
+ "\u0432\u0441\u0435",
1209
+ "feat",
1210
+ "fix",
1211
+ "chore",
1212
+ "docs",
1213
+ "refactor",
1214
+ "\u043F\u0435\u0440\u0444",
1215
+ "wip"
1216
+ ]);
1217
+ OVERLAP_DEFAULTS = { threshold: 0.2, strongFile: 0.35 };
1218
+ }
1219
+ });
1220
+
772
1221
  // ../swarm-core/src/index.ts
773
1222
  var init_src = __esm({
774
1223
  "../swarm-core/src/index.ts"() {
@@ -785,14 +1234,492 @@ var init_src = __esm({
785
1234
  init_names();
786
1235
  init_baseBranch();
787
1236
  init_hosts();
1237
+ init_hostPool();
788
1238
  init_hostCommands();
789
1239
  init_relief();
1240
+ init_cost();
1241
+ init_runGuards();
1242
+ init_codeZones();
1243
+ }
1244
+ });
1245
+
1246
+ // src/claudeBin.ts
1247
+ import { existsSync as existsSync2, readdirSync } from "fs";
1248
+ import { win32 as winPath } from "path";
1249
+ function compareVersionDirs(a, b) {
1250
+ const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
1251
+ const pa = parts(a);
1252
+ const pb = parts(b);
1253
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1254
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1255
+ if (d !== 0) return d;
1256
+ }
1257
+ return 0;
1258
+ }
1259
+ function realListDir(dir) {
1260
+ try {
1261
+ return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1262
+ } catch {
1263
+ return [];
1264
+ }
1265
+ }
1266
+ function winVersionRoots(env, listDir) {
1267
+ const roots = [];
1268
+ const appData = env.APPDATA;
1269
+ if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
1270
+ const localAppData = env.LOCALAPPDATA;
1271
+ if (localAppData) {
1272
+ const packages = winPath.join(localAppData, "Packages");
1273
+ for (const name of listDir(packages)) {
1274
+ if (!name.startsWith("Claude")) continue;
1275
+ roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
1276
+ }
1277
+ }
1278
+ return roots;
1279
+ }
1280
+ function findWindowsClaude(opts = {}) {
1281
+ const env = opts.env ?? process.env;
1282
+ const listDir = opts.listDir ?? realListDir;
1283
+ const fileExists = opts.fileExists ?? existsSync2;
1284
+ let best = null;
1285
+ for (const root of winVersionRoots(env, listDir)) {
1286
+ for (const version of listDir(root)) {
1287
+ const candidate = winPath.join(root, version, "claude.exe");
1288
+ if (!fileExists(candidate)) continue;
1289
+ if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
1290
+ }
1291
+ }
1292
+ return best?.path ?? null;
1293
+ }
1294
+ function resolveClaudeBin(opts = {}) {
1295
+ const env = opts.env ?? process.env;
1296
+ const platform = opts.platform ?? process.platform;
1297
+ const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
1298
+ if (explicit) return explicit;
1299
+ if (platform !== "win32") return CLAUDE_FALLBACK;
1300
+ if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
1301
+ windowsScanDone = true;
1302
+ const found = findWindowsClaude({ ...opts, env });
1303
+ cachedWindowsBin = found;
1304
+ if (found) return found;
1305
+ if (!warnedNoWindowsClaude) {
1306
+ warnedNoWindowsClaude = true;
1307
+ console.warn(
1308
+ "[overmind] claude.exe \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u043D\u0438 \u0432 %APPDATA%\\Claude\\claude-code, \u043D\u0438 \u0432 MSIX-\u043A\u043E\u043F\u0438\u0438 \u043F\u043E\u0434 %LOCALAPPDATA%\\Packages\\Claude* \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u0433\u043E\u043B\u043E\u0435 \xABclaude\xBB (\u043F\u043E\u0438\u0441\u043A \u043F\u043E PATH+PATHEXT). \u0423\u043F\u0430\u0434\u0451\u0442 ENOENT (bash-\u043E\u0431\u0451\u0440\u0442\u043A\u0430 \u0431\u0435\u0437 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0432 PATH \u043D\u0435 \u0432\u0438\u0434\u043D\u0430 spawn'\u0443) \u2014 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 SWARM_CLAUDE_BIN \u043F\u0443\u0442\u0451\u043C \u043A \u043D\u0430\u0441\u0442\u043E\u044F\u0449\u0435\u043C\u0443 claude.exe."
1309
+ );
1310
+ }
1311
+ return CLAUDE_FALLBACK;
1312
+ }
1313
+ var CLAUDE_FALLBACK, warnedNoWindowsClaude, cachedWindowsBin, windowsScanDone;
1314
+ var init_claudeBin = __esm({
1315
+ "src/claudeBin.ts"() {
1316
+ "use strict";
1317
+ CLAUDE_FALLBACK = "claude";
1318
+ warnedNoWindowsClaude = false;
1319
+ cachedWindowsBin = null;
1320
+ windowsScanDone = false;
1321
+ }
1322
+ });
1323
+
1324
+ // src/pricing.ts
1325
+ function costFromUsage(usage, model, table = MODEL_PRICING) {
1326
+ const pricing = model ? table[model] : void 0;
1327
+ if (!pricing) {
1328
+ return { costUsd: 0, warning: `\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u0430\u0439\u0441\u0438\u043D\u0433\u0430: "${model ?? ""}" \u2014 cost \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D` };
1329
+ }
1330
+ const input = usage.input_tokens ?? 0;
1331
+ const output = usage.output_tokens ?? 0;
1332
+ const cacheWrite = usage.cache_creation_input_tokens ?? 0;
1333
+ const cacheRead = usage.cache_read_input_tokens ?? 0;
1334
+ const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
1335
+ return { costUsd };
1336
+ }
1337
+ var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
1338
+ var init_pricing = __esm({
1339
+ "src/pricing.ts"() {
1340
+ "use strict";
1341
+ MODEL_PRICING = {
1342
+ // Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
1343
+ // Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
1344
+ // ветвление по времени внутри расчёта стоимости.
1345
+ "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
1346
+ "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
1347
+ // Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
1348
+ "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
1349
+ "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
1350
+ };
1351
+ CACHE_READ_FACTOR = 0.1;
1352
+ CACHE_WRITE_5M_FACTOR = 1.25;
1353
+ }
1354
+ });
1355
+
1356
+ // src/claudeResult.ts
1357
+ function parseClaudeResultJson(raw) {
1358
+ const trimmed = raw.trim();
1359
+ if (!trimmed) return null;
1360
+ try {
1361
+ const parsed = JSON.parse(trimmed);
1362
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
1363
+ } catch {
1364
+ return null;
1365
+ }
1366
+ }
1367
+ function launchCostUsd(result, model) {
1368
+ if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
1369
+ return { costUsd: result.total_cost_usd };
1370
+ }
1371
+ if (!result.usage) return { costUsd: 0 };
1372
+ return costFromUsage(result.usage, model);
1373
+ }
1374
+ function launchSpend(result) {
1375
+ const u = result.usage ?? {};
1376
+ const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1377
+ return {
1378
+ tokensIn: num(u.input_tokens) + num(u.cache_creation_input_tokens) + num(u.cache_read_input_tokens),
1379
+ tokensOut: num(u.output_tokens),
1380
+ durationMs: num(result.duration_ms)
1381
+ };
1382
+ }
1383
+ var init_claudeResult = __esm({
1384
+ "src/claudeResult.ts"() {
1385
+ "use strict";
1386
+ init_pricing();
1387
+ }
1388
+ });
1389
+
1390
+ // src/executors/claudeCode.ts
1391
+ import { readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
1392
+ import { homedir } from "os";
1393
+ import { join as join2 } from "path";
1394
+ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
1395
+ const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
1396
+ const base3 = [
1397
+ "--dangerously-skip-permissions",
1398
+ ...ISOLATION_FLAGS,
1399
+ "--mcp-config",
1400
+ cfgPath,
1401
+ ...settings ? ["--settings", settings] : [],
1402
+ ...pluginFlags,
1403
+ "--output-format",
1404
+ "json",
1405
+ "-p",
1406
+ prompt
1407
+ ];
1408
+ const withModel = model ? ["--model", model, ...base3] : base3;
1409
+ return continueSession ? ["--continue", ...withModel] : withModel;
1410
+ }
1411
+ function droneDenyRules(paths) {
1412
+ const abs = (p) => `//${p.replace(/^\/+/, "")}`;
1413
+ return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
1414
+ }
1415
+ function droneSettingsJson(paths) {
1416
+ return JSON.stringify({ permissions: { deny: droneDenyRules(paths) } });
1417
+ }
1418
+ function transcriptDir(worktree, projectsDir) {
1419
+ return join2(projectsDir, worktree.replace(/[/.]/g, "-"));
1420
+ }
1421
+ function sessionTranscriptExists(worktree, projectsDir = join2(homedir(), ".claude", "projects")) {
1422
+ try {
1423
+ return readdirSync2(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
1424
+ } catch {
1425
+ return false;
1426
+ }
1427
+ }
1428
+ function transcriptMtimeMs(worktree, projectsDir = join2(homedir(), ".claude", "projects")) {
1429
+ const dir = transcriptDir(worktree, projectsDir);
1430
+ let files;
1431
+ try {
1432
+ files = readdirSync2(dir);
1433
+ } catch {
1434
+ return null;
1435
+ }
1436
+ let latest = null;
1437
+ for (const f of files) {
1438
+ if (!f.endsWith(".jsonl")) continue;
1439
+ try {
1440
+ const mtime2 = statSync(join2(dir, f)).mtimeMs;
1441
+ if (latest === null || mtime2 > latest) latest = mtime2;
1442
+ } catch {
1443
+ }
1444
+ }
1445
+ return latest;
1446
+ }
1447
+ function writeMcpConfig(spec) {
1448
+ const cfg = {
1449
+ mcpServers: {
1450
+ [spec.mcp.name]: { command: spec.mcp.command, args: spec.mcp.args, env: spec.mcp.env }
1451
+ }
1452
+ };
1453
+ const path = join2(spec.worktree, MCP_CONFIG_FILE);
1454
+ writeFileSync2(path, JSON.stringify(cfg, null, 2));
1455
+ return path;
1456
+ }
1457
+ var ISOLATION_FLAGS, MCP_CONFIG_FILE, claudeCodeExecutor;
1458
+ var init_claudeCode = __esm({
1459
+ "src/executors/claudeCode.ts"() {
1460
+ "use strict";
1461
+ init_claudeBin();
1462
+ init_claudeResult();
1463
+ ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
1464
+ MCP_CONFIG_FILE = ".swarm-mcp.json";
1465
+ claudeCodeExecutor = {
1466
+ id: "claude-code",
1467
+ title: "Claude Code (claude)",
1468
+ binEnv: "SWARM_CLAUDE_BIN",
1469
+ install: { what: "Claude Code CLI \u2014 \u0438\u043C \u0434\u0440\u043E\u043D \u043F\u0438\u0448\u0435\u0442 \u043A\u043E\u0434", pkg: "@anthropic-ai/claude-code" },
1470
+ resolveBin: () => resolveClaudeBin(),
1471
+ // Стрелка, а не метод: `plan` могут вызвать в отрыве от объекта (деструктуризация в тестах,
1472
+ // проброс колбэком), и `this` тогда уедет в undefined.
1473
+ plan: (spec) => {
1474
+ const cfgPath = writeMcpConfig(spec);
1475
+ const settings = spec.denyPaths.length > 0 ? droneSettingsJson(spec.denyPaths) : void 0;
1476
+ return {
1477
+ bin: resolveClaudeBin(),
1478
+ args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings)
1479
+ };
1480
+ },
1481
+ parseResult(raw, model) {
1482
+ const result = parseClaudeResultJson(raw);
1483
+ if (!result) return null;
1484
+ const est = launchCostUsd(result, model);
1485
+ const spend = launchSpend(result);
1486
+ return {
1487
+ costUsd: est.costUsd,
1488
+ ...est.warning ? { warning: est.warning } : {},
1489
+ tokensIn: spend.tokensIn,
1490
+ tokensOut: spend.tokensOut,
1491
+ durationMs: spend.durationMs
1492
+ };
1493
+ },
1494
+ session: {
1495
+ exists: (worktree) => sessionTranscriptExists(worktree),
1496
+ mtimeMs: (worktree) => transcriptMtimeMs(worktree)
1497
+ },
1498
+ // `claude -p "ok" --max-turns 1` (TASWM-23) — прямее, чем HEAD на статус-хост: дёргает ТОТ
1499
+ // ЖЕ путь (auth + реальный ответ модели), что и дрон. ISOLATION_FLAGS те же, что у дрона
1500
+ // (TASWM-90): SessionStart-хуки юзера могли бы подвесить пробу и дать ложный шторм.
1501
+ probe: {
1502
+ args: [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"],
1503
+ // TASWM-214: исчерпанный лимит подписки — НЕ шторм провайдера, и путать их нельзя.
1504
+ limitPattern: /(weekly limit|usage limit|hit your .*limit)/i
1505
+ }
1506
+ };
1507
+ }
1508
+ });
1509
+
1510
+ // src/swarmHome.ts
1511
+ import { homedir as homedir2 } from "os";
1512
+ import { join as join3 } from "path";
1513
+ import { mkdirSync } from "fs";
1514
+ var SWARM_HOME;
1515
+ var init_swarmHome = __esm({
1516
+ "src/swarmHome.ts"() {
1517
+ "use strict";
1518
+ SWARM_HOME = process.env.SWARM_HOME || join3(homedir2(), ".taskless-swarm");
1519
+ mkdirSync(SWARM_HOME, { recursive: true });
1520
+ }
1521
+ });
1522
+
1523
+ // src/executors/codex.ts
1524
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync3, statSync as statSync2, symlinkSync } from "fs";
1525
+ import { join as join4 } from "path";
1526
+ function resolveCodexBin(env = process.env) {
1527
+ return (env.SWARM_CODEX_BIN ?? "").trim() || "codex";
1528
+ }
1529
+ function codexHomePath(worktree, swarmHome = SWARM_HOME) {
1530
+ return join4(swarmHome, "executor-state", "codex", worktree.replace(/[/.]/g, "-"));
1531
+ }
1532
+ function ensureCodexHome(worktree, realHome, env = process.env, swarmHome = SWARM_HOME) {
1533
+ const home = codexHomePath(worktree, swarmHome);
1534
+ mkdirSync2(home, { recursive: true });
1535
+ const real = (env.CODEX_HOME ?? "").trim() || join4(realHome, ".codex");
1536
+ for (const file of ["auth.json", "config.toml"]) {
1537
+ const target = join4(real, file);
1538
+ const link = join4(home, file);
1539
+ if (!existsSync3(target) || existsSync3(link)) continue;
1540
+ try {
1541
+ symlinkSync(target, link);
1542
+ } catch {
1543
+ }
1544
+ }
1545
+ return home;
1546
+ }
1547
+ function tomlValue(v) {
1548
+ if (typeof v === "string") {
1549
+ const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
1550
+ return `"${escaped}"`;
1551
+ }
1552
+ if (Array.isArray(v)) return `[${v.map((x) => tomlValue(x)).join(", ")}]`;
1553
+ return `{${Object.entries(v).map(([k, val]) => `${k} = ${tomlValue(val)}`).join(", ")}}`;
1554
+ }
1555
+ function mcpConfigArgs(spec) {
1556
+ const inline = `{command = ${tomlValue(spec.mcp.command)}, args = ${tomlValue(spec.mcp.args)}, env = ${tomlValue(spec.mcp.env)}}`;
1557
+ return ["-c", `mcp_servers.${spec.mcp.name}=${inline}`];
1558
+ }
1559
+ function skillsNotice(skillDirs) {
1560
+ if (skillDirs.length === 0) return "";
1561
+ return "\u0421\u043A\u0438\u043B\u043B\u044B \u0440\u043E\u043B\u0438 \u043B\u0435\u0436\u0430\u0442 \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C\u0438 \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 (\u044D\u0442\u043E\u0442 \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0438\u0445 \u0441\u0430\u043C): " + skillDirs.join(", ") + ". \u041F\u0440\u043E\u0447\u0438\u0442\u0430\u0439 \u0438\u0445 SKILL.md \u041F\u0415\u0420\u0415\u0414 \u0440\u0430\u0431\u043E\u0442\u043E\u0439 \u2014 \u0442\u0430\u043C \u0434\u0438\u0441\u0446\u0438\u043F\u043B\u0438\u043D\u0430 \u0440\u043E\u044F \u0438 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.";
1562
+ }
1563
+ function readUsage(node) {
1564
+ if (typeof node !== "object" || node === null) return null;
1565
+ const rec = node;
1566
+ const direct = rec.total_token_usage ?? rec.usage ?? rec.token_usage;
1567
+ if (direct && typeof direct === "object") return direct;
1568
+ const info = rec.info;
1569
+ if (info && typeof info === "object") return readUsage(info);
1570
+ return null;
1571
+ }
1572
+ function parseCodexStream(raw) {
1573
+ let usage = null;
1574
+ let completed = false;
1575
+ for (const line of raw.split("\n")) {
1576
+ const trimmed = line.trim();
1577
+ if (!trimmed) continue;
1578
+ let parsed;
1579
+ try {
1580
+ parsed = JSON.parse(trimmed);
1581
+ } catch {
1582
+ continue;
1583
+ }
1584
+ if (typeof parsed !== "object" || parsed === null) continue;
1585
+ const rec = parsed;
1586
+ const msg = typeof rec.msg === "object" && rec.msg !== null ? rec.msg : rec;
1587
+ const type = typeof msg.type === "string" ? msg.type : null;
1588
+ const found = readUsage(msg);
1589
+ if (found) usage = found;
1590
+ if (type && TERMINAL_EVENTS.has(type)) completed = true;
1591
+ }
1592
+ return { usage, completed };
1593
+ }
1594
+ function newestRolloutMtime(dir, depth = 4) {
1595
+ let entries;
1596
+ try {
1597
+ entries = readdirSync3(dir);
1598
+ } catch {
1599
+ return null;
1600
+ }
1601
+ let latest = null;
1602
+ for (const name of entries) {
1603
+ const full = join4(dir, name);
1604
+ let st;
1605
+ try {
1606
+ st = statSync2(full);
1607
+ } catch {
1608
+ continue;
1609
+ }
1610
+ if (st.isDirectory()) {
1611
+ if (depth <= 0) continue;
1612
+ const nested = newestRolloutMtime(full, depth - 1);
1613
+ if (nested !== null && (latest === null || nested > latest)) latest = nested;
1614
+ } else if (name.endsWith(".jsonl")) {
1615
+ if (latest === null || st.mtimeMs > latest) latest = st.mtimeMs;
1616
+ }
1617
+ }
1618
+ return latest;
1619
+ }
1620
+ var BYPASS_FLAG, SKIP_GIT_CHECK_FLAG, CODEX_MODEL_PRICING, TERMINAL_EVENTS, codexExecutor;
1621
+ var init_codex = __esm({
1622
+ "src/executors/codex.ts"() {
1623
+ "use strict";
1624
+ init_swarmHome();
1625
+ init_pricing();
1626
+ BYPASS_FLAG = "--dangerously-bypass-approvals-and-sandbox";
1627
+ SKIP_GIT_CHECK_FLAG = "--skip-git-repo-check";
1628
+ CODEX_MODEL_PRICING = {};
1629
+ TERMINAL_EVENTS = /* @__PURE__ */ new Set(["task_complete", "turn.completed", "thread.completed", "session.completed"]);
1630
+ codexExecutor = {
1631
+ id: "codex",
1632
+ title: "Codex CLI (codex)",
1633
+ binEnv: "SWARM_CODEX_BIN",
1634
+ install: { what: "Codex CLI \u2014 \u0430\u043B\u044C\u0442\u0435\u0440\u043D\u0430\u0442\u0438\u0432\u043D\u044B\u0439 \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C \u0434\u0440\u043E\u043D\u0430", pkg: "@openai/codex" },
1635
+ resolveBin: () => resolveCodexBin(),
1636
+ // Стрелка, а не метод — см. тот же комментарий в claudeCode.ts.
1637
+ plan: (spec) => {
1638
+ const notice = skillsNotice(spec.skillDirs);
1639
+ const prompt = notice ? `${spec.prompt}
1640
+
1641
+ ${notice}` : spec.prompt;
1642
+ const args = [
1643
+ "exec",
1644
+ // Продолжение оборванной сессии — ПОДКОМАНДА, а не флаг: `exec resume --last` берёт
1645
+ // последнюю сессию этого CODEX_HOME (то есть этого worktree, см. codexHome).
1646
+ ...spec.continueSession ? ["resume", "--last"] : [],
1647
+ "--json",
1648
+ SKIP_GIT_CHECK_FLAG,
1649
+ BYPASS_FLAG,
1650
+ ...spec.model ? ["-m", spec.model] : [],
1651
+ ...mcpConfigArgs(spec),
1652
+ prompt
1653
+ ];
1654
+ return { bin: resolveCodexBin(), args, env: { CODEX_HOME: ensureCodexHome(spec.worktree, spec.realHome) } };
1655
+ },
1656
+ parseResult(raw, model) {
1657
+ const { usage, completed } = parseCodexStream(raw);
1658
+ if (!completed) return null;
1659
+ if (!usage) return { costUsd: 0, warning: "codex: \u043F\u043E\u0442\u043E\u043A \u0431\u0435\u0437 usage \u2014 cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D" };
1660
+ const cached = usage.cached_input_tokens ?? 0;
1661
+ const common = {
1662
+ input_tokens: Math.max(0, (usage.input_tokens ?? 0) - cached),
1663
+ output_tokens: usage.output_tokens ?? 0,
1664
+ cache_read_input_tokens: cached
1665
+ };
1666
+ const est = costFromUsage(common, model, CODEX_MODEL_PRICING);
1667
+ return est.warning ? { costUsd: est.costUsd, warning: est.warning } : { costUsd: est.costUsd };
1668
+ },
1669
+ session: {
1670
+ exists: (worktree) => newestRolloutMtime(join4(codexHomePath(worktree), "sessions")) !== null,
1671
+ mtimeMs: (worktree) => newestRolloutMtime(join4(codexHomePath(worktree), "sessions"))
1672
+ },
1673
+ probe: {
1674
+ // Тот же принцип, что у claude: дешёвый реальный прогон — он проверяет и auth, и живость
1675
+ // провайдера разом. Один короткий ответ, без тулов и без сети сверх модели.
1676
+ args: ["exec", "--json", SKIP_GIT_CHECK_FLAG, BYPASS_FLAG, "ok"],
1677
+ limitPattern: /(usage limit|rate limit|quota exceeded|insufficient_quota)/i
1678
+ }
1679
+ };
1680
+ }
1681
+ });
1682
+
1683
+ // src/executors/types.ts
1684
+ var init_types2 = __esm({
1685
+ "src/executors/types.ts"() {
1686
+ "use strict";
1687
+ }
1688
+ });
1689
+
1690
+ // src/executors/index.ts
1691
+ function resolveExecutor(id) {
1692
+ const key = (id ?? "").trim();
1693
+ if (!key) return EXECUTORS[DEFAULT_EXECUTOR_ID];
1694
+ const found = EXECUTORS[key];
1695
+ if (!found) {
1696
+ throw new Error(`\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u044B\u0439 \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C "${key}" (SWARM_EXECUTOR). \u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B: ${Object.keys(EXECUTORS).join(", ")}`);
1697
+ }
1698
+ return found;
1699
+ }
1700
+ var DEFAULT_EXECUTOR_ID, EXECUTORS;
1701
+ var init_executors = __esm({
1702
+ "src/executors/index.ts"() {
1703
+ "use strict";
1704
+ init_claudeCode();
1705
+ init_codex();
1706
+ init_types2();
1707
+ init_claudeCode();
1708
+ init_codex();
1709
+ DEFAULT_EXECUTOR_ID = "claude-code";
1710
+ EXECUTORS = {
1711
+ [claudeCodeExecutor.id]: claudeCodeExecutor,
1712
+ [codexExecutor.id]: codexExecutor
1713
+ };
790
1714
  }
791
1715
  });
792
1716
 
793
1717
  // src/config.ts
794
1718
  var config_exports = {};
795
1719
  __export(config_exports, {
1720
+ DAEMON_LOG_CHECK_MS: () => DAEMON_LOG_CHECK_MS,
1721
+ DAEMON_LOG_KEEP: () => DAEMON_LOG_KEEP,
1722
+ DAEMON_LOG_MAX_BYTES: () => DAEMON_LOG_MAX_BYTES,
796
1723
  DAEMON_LOG_PATH: () => DAEMON_LOG_PATH,
797
1724
  DB_PATH: () => DB_PATH,
798
1725
  DRONE_CHANNEL_ENABLED: () => DRONE_CHANNEL_ENABLED,
@@ -803,8 +1730,10 @@ __export(config_exports, {
803
1730
  DRONE_HOME: () => DRONE_HOME,
804
1731
  DRONE_HOME_ISOLATION: () => DRONE_HOME_ISOLATION,
805
1732
  DRONE_ISOLATION: () => DRONE_ISOLATION,
1733
+ EXECUTOR: () => EXECUTOR,
806
1734
  HEARTBEAT_PATH: () => HEARTBEAT_PATH,
807
1735
  HEARTBEAT_STALE_MS: () => HEARTBEAT_STALE_MS,
1736
+ REPO_MAP_PATH: () => REPO_MAP_PATH,
808
1737
  REVIEW_CUTOFF_PATH: () => REVIEW_CUTOFF_PATH,
809
1738
  RUN_LOGS_DIR: () => RUN_LOGS_DIR,
810
1739
  SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
@@ -842,9 +1771,11 @@ __export(config_exports, {
842
1771
  SWARM_RELIEF_MAX_FD_LOAD_FACTOR: () => SWARM_RELIEF_MAX_FD_LOAD_FACTOR,
843
1772
  SWARM_RELIEF_MIN_FREE_GB: () => SWARM_RELIEF_MIN_FREE_GB,
844
1773
  SWARM_REPO: () => SWARM_REPO,
1774
+ SWARM_REPO_MAP_MS: () => SWARM_REPO_MAP_MS,
845
1775
  SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
846
1776
  SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
847
1777
  SWARM_SCAN_DEPTH: () => SWARM_SCAN_DEPTH,
1778
+ SWARM_SCAN_ROOTS_RAW: () => SWARM_SCAN_ROOTS_RAW,
848
1779
  SWARM_SHOT_TIMEOUT_MS: () => SWARM_SHOT_TIMEOUT_MS,
849
1780
  SWARM_STORM_URL: () => SWARM_STORM_URL,
850
1781
  SWARM_SWEEP_INTERVAL_MS: () => SWARM_SWEEP_INTERVAL_MS,
@@ -869,9 +1800,8 @@ __export(config_exports, {
869
1800
  resolveEffectiveWorktreesDir: () => resolveEffectiveWorktreesDir,
870
1801
  reviewCutoffAt: () => reviewCutoffAt
871
1802
  });
872
- import { homedir } from "os";
873
- import { basename, join as join2, resolve } from "path";
874
- import { mkdirSync, readFileSync as readFileSync2, realpathSync, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
1803
+ import { basename, join as join5, resolve } from "path";
1804
+ import { readFileSync as readFileSync2, realpathSync, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
875
1805
  function normalizeRepo(repo) {
876
1806
  const r = (repo ?? "").trim();
877
1807
  if (!r) return "";
@@ -882,7 +1812,7 @@ function normalizeRepo(repo) {
882
1812
  }
883
1813
  }
884
1814
  function droneWorktreePath(worktreesDir, repo, slug) {
885
- return join2(worktreesDir, basename(normalizeRepo(repo)), slug);
1815
+ return join5(worktreesDir, basename(normalizeRepo(repo)), slug);
886
1816
  }
887
1817
  function resolveEffectiveWorktreesDir(deps) {
888
1818
  if (deps.envOverride) return deps.envOverride;
@@ -924,7 +1854,7 @@ function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
924
1854
  }
925
1855
  function tokenFromFile() {
926
1856
  try {
927
- return readFileSync2(join2(SWARM_HOME, "token"), "utf8").trim();
1857
+ return readFileSync2(join5(SWARM_HOME, "token"), "utf8").trim();
928
1858
  } catch {
929
1859
  return "";
930
1860
  }
@@ -938,31 +1868,36 @@ function reviewCutoffAt() {
938
1868
  const at = stamp();
939
1869
  try {
940
1870
  const tmp = `${REVIEW_CUTOFF_PATH}.tmp`;
941
- writeFileSync2(tmp, JSON.stringify({ at }));
1871
+ writeFileSync3(tmp, JSON.stringify({ at }));
942
1872
  renameSync2(tmp, REVIEW_CUTOFF_PATH);
943
1873
  } catch {
944
1874
  }
945
1875
  return at;
946
1876
  }
947
- var SWARM_HOME, DB_PATH, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, 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_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, 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, 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;
1877
+ 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_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, 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;
948
1878
  var init_config = __esm({
949
1879
  "src/config.ts"() {
950
1880
  "use strict";
951
1881
  init_src();
952
- SWARM_HOME = process.env.SWARM_HOME || join2(homedir(), ".taskless-swarm");
953
- mkdirSync(SWARM_HOME, { recursive: true });
954
- DB_PATH = process.env.SWARM_DB || join2(SWARM_HOME, "swarm.db");
955
- DRONE_HOME = process.env.SWARM_DRONE_HOME || join2(SWARM_HOME, "drone-home");
1882
+ init_executors();
1883
+ init_swarmHome();
1884
+ init_swarmHome();
1885
+ DB_PATH = process.env.SWARM_DB || join5(SWARM_HOME, "swarm.db");
1886
+ EXECUTOR = resolveExecutor(process.env.SWARM_EXECUTOR);
1887
+ DRONE_HOME = process.env.SWARM_DRONE_HOME || join5(SWARM_HOME, "drone-home");
956
1888
  DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
957
1889
  DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
958
- WORKTREES_NOINDEX_DIR = join2(SWARM_HOME, "worktrees.noindex");
959
- WORKTREES_LEGACY_DIR = join2(SWARM_HOME, "worktrees");
1890
+ WORKTREES_NOINDEX_DIR = join5(SWARM_HOME, "worktrees.noindex");
1891
+ WORKTREES_LEGACY_DIR = join5(SWARM_HOME, "worktrees");
960
1892
  WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
961
- RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join2(SWARM_HOME, "run-logs");
962
- DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join2(SWARM_HOME, "overmind.log");
1893
+ RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join5(SWARM_HOME, "run-logs");
1894
+ DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join5(SWARM_HOME, "overmind.log");
1895
+ DAEMON_LOG_MAX_BYTES = envNum("SWARM_DAEMON_LOG_MAX_BYTES", 16 * 1024 * 1024);
1896
+ DAEMON_LOG_KEEP = envNum("SWARM_DAEMON_LOG_KEEP", 2);
1897
+ DAEMON_LOG_CHECK_MS = envNum("SWARM_DAEMON_LOG_CHECK_MS", 6e4);
963
1898
  DIST_BUILD = true;
964
1899
  SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
965
- SWARM_OUTBOX = process.env.SWARM_OUTBOX || join2(SWARM_HOME, "outbox.jsonl");
1900
+ SWARM_OUTBOX = process.env.SWARM_OUTBOX || join5(SWARM_HOME, "outbox.jsonl");
966
1901
  SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
967
1902
  SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
968
1903
  SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
@@ -987,13 +1922,16 @@ var init_config = __esm({
987
1922
  DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
988
1923
  DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
989
1924
  DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
990
- DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join2(SWARM_HOME, "drone-channel.key");
1925
+ DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join5(SWARM_HOME, "drone-channel.key");
991
1926
  DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
992
- HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join2(SWARM_HOME, "overmind.heartbeat");
1927
+ HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join5(SWARM_HOME, "overmind.heartbeat");
993
1928
  HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
994
1929
  SWARM_REPO = process.env.SWARM_REPO || "";
995
1930
  SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
996
1931
  SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
1932
+ SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
1933
+ REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join5(SWARM_HOME, "repo-map.json");
1934
+ SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
997
1935
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
998
1936
  SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
999
1937
  TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
@@ -1003,7 +1941,7 @@ var init_config = __esm({
1003
1941
  TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
1004
1942
  SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
1005
1943
  SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
1006
- REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join2(SWARM_HOME, "review-cutoff.json");
1944
+ REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join5(SWARM_HOME, "review-cutoff.json");
1007
1945
  SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
1008
1946
  SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
1009
1947
  WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
@@ -1360,10 +2298,10 @@ var init_storm = __esm({
1360
2298
  });
1361
2299
 
1362
2300
  // src/heartbeat.ts
1363
- import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
2301
+ import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
1364
2302
  function writeHeartbeat(path, now) {
1365
2303
  const tmp = `${path}.tmp`;
1366
- writeFileSync4(tmp, String(now));
2304
+ writeFileSync5(tmp, String(now));
1367
2305
  renameSync3(tmp, path);
1368
2306
  }
1369
2307
  function readHeartbeat(path) {
@@ -1486,7 +2424,7 @@ var init_procList = __esm({
1486
2424
 
1487
2425
  // src/procCleanup.ts
1488
2426
  function sleep(ms) {
1489
- return new Promise((resolve3) => setTimeout(resolve3, ms));
2427
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
1490
2428
  }
1491
2429
  function parseLsofCwd(output) {
1492
2430
  const rows = [];
@@ -1704,16 +2642,21 @@ var init_procCleanup = __esm({
1704
2642
  });
1705
2643
 
1706
2644
  // src/git.ts
1707
- import { existsSync as existsSync7, readdirSync as readdirSync8, statSync as statSync5 } from "fs";
2645
+ import { existsSync as existsSync9, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
1708
2646
  import { appendFile, mkdir, readFile, rm } from "fs/promises";
1709
- import { isAbsolute, join as join11 } from "path";
2647
+ import { isAbsolute as isAbsolute2, join as join15 } from "path";
1710
2648
  import { $ as $2 } from "execa";
2649
+ function isUnrecoverableRefError(e) {
2650
+ if (e instanceof MissingRefError) return true;
2651
+ const msg = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
2652
+ return MISSING_REF_RE.test(msg);
2653
+ }
1711
2654
  async function reclaimStaleWorktree(repo, worktree) {
1712
2655
  await detachWorktree(repo, worktree);
1713
2656
  }
1714
2657
  function isLinkedWorktree(dir) {
1715
2658
  try {
1716
- return statSync5(join11(dir, ".git")).isFile();
2659
+ return statSync7(join15(dir, ".git")).isFile();
1717
2660
  } catch {
1718
2661
  return false;
1719
2662
  }
@@ -1725,7 +2668,7 @@ async function detachWorktree(repo, worktree) {
1725
2668
  }
1726
2669
  }
1727
2670
  async function ensureWorktreeDirGone(worktree) {
1728
- if (!existsSync7(worktree)) return false;
2671
+ if (!existsSync9(worktree)) return false;
1729
2672
  if (!isLinkedWorktree(worktree)) {
1730
2673
  console.error(
1731
2674
  `[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.`
@@ -1740,7 +2683,7 @@ async function ensureWorktreeDirGone(worktree) {
1740
2683
  );
1741
2684
  return true;
1742
2685
  }
1743
- if (existsSync7(worktree)) {
2686
+ if (existsSync9(worktree)) {
1744
2687
  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.`);
1745
2688
  }
1746
2689
  return true;
@@ -1783,7 +2726,7 @@ async function addWorktree(repo, path, branch, opts = {}) {
1783
2726
  }
1784
2727
  const busyAt = await worktreeForBranch(repo, branch);
1785
2728
  if (busyAt) {
1786
- const exists = existsSync7(busyAt);
2729
+ const exists = existsSync9(busyAt);
1787
2730
  const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
1788
2731
  if (live) throw new WorktreeBusyError(branch, busyAt);
1789
2732
  if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
@@ -1819,9 +2762,9 @@ async function ensureGitExcluded(worktree, pattern) {
1819
2762
  const out = await $2({ reject: false })`git -C ${worktree} rev-parse --git-common-dir`;
1820
2763
  const raw = out.stdout.trim();
1821
2764
  if (out.exitCode !== 0 || !raw) return false;
1822
- const commonDir = isAbsolute(raw) ? raw : join11(worktree, raw);
1823
- const infoDir = join11(commonDir, "info");
1824
- const excludeFile = join11(infoDir, "exclude");
2765
+ const commonDir = isAbsolute2(raw) ? raw : join15(worktree, raw);
2766
+ const infoDir = join15(commonDir, "info");
2767
+ const excludeFile = join15(infoDir, "exclude");
1825
2768
  let current = "";
1826
2769
  try {
1827
2770
  current = await readFile(excludeFile, "utf8");
@@ -1854,7 +2797,7 @@ async function worktreeDirty(path) {
1854
2797
  async function repairMovedWorktrees(targetDir) {
1855
2798
  const subdirs = (dir) => {
1856
2799
  try {
1857
- return readdirSync8(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join11(dir, d.name));
2800
+ return readdirSync10(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join15(dir, d.name));
1858
2801
  } catch {
1859
2802
  return [];
1860
2803
  }
@@ -1898,7 +2841,9 @@ async function journalForBranch(repo, branch, slug) {
1898
2841
  }
1899
2842
  async function prepareDetachedWorktree(repo, branch, worktree) {
1900
2843
  await $2`git -C ${repo} fetch --quiet origin`;
1901
- await $2`git -C ${repo} worktree add --detach --force ${worktree} ${`origin/${branch}`}`;
2844
+ const ref = `origin/${branch}`;
2845
+ if (!await revParse(repo, `${ref}^{commit}`)) throw new MissingRefError(ref, repo);
2846
+ await $2`git -C ${repo} worktree add --detach --force ${worktree} ${ref}`;
1902
2847
  }
1903
2848
  async function mergeRef(worktree, ref) {
1904
2849
  const before = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
@@ -1931,7 +2876,7 @@ async function worktreeLastCommitMs(worktree) {
1931
2876
  const sec = Number(out);
1932
2877
  return Number.isInteger(sec) && sec > 0 ? sec * 1e3 : null;
1933
2878
  }
1934
- var WorktreeBusyError, GITHUB_REMOTE_RE;
2879
+ var WorktreeBusyError, MissingRefError, MISSING_REF_RE, GITHUB_REMOTE_RE;
1935
2880
  var init_git = __esm({
1936
2881
  "src/git.ts"() {
1937
2882
  "use strict";
@@ -1946,6 +2891,17 @@ var init_git = __esm({
1946
2891
  branch;
1947
2892
  worktreePath;
1948
2893
  };
2894
+ MissingRefError = class extends Error {
2895
+ constructor(ref, repo) {
2896
+ super(`ref ${ref} \u043D\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 ${repo} \u2014 \u0432\u0435\u0442\u043A\u0443 \u043D\u0435\u043B\u044C\u0437\u044F \u0432\u044B\u0447\u0435\u043A\u0430\u0442\u044C (\u043F\u0440\u043E\u0432\u0435\u0440\u044C, \u0438\u0437 \u043A\u0430\u043A\u043E\u0439 \u0432\u0435\u0442\u043A\u0438 \u043E\u0442\u043A\u0440\u044B\u0442 PR)`);
2897
+ this.ref = ref;
2898
+ this.repo = repo;
2899
+ this.name = "MissingRefError";
2900
+ }
2901
+ ref;
2902
+ repo;
2903
+ };
2904
+ MISSING_REF_RE = /invalid reference|unknown revision|not a valid object name|couldn't find remote ref/i;
1949
2905
  GITHUB_REMOTE_RE = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/i;
1950
2906
  }
1951
2907
  });
@@ -1966,18 +2922,18 @@ __export(droneChannel_exports, {
1966
2922
  verifyLocalCliToken: () => verifyLocalCliToken
1967
2923
  });
1968
2924
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
1969
- import { chmodSync, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
2925
+ import { chmodSync, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
1970
2926
  import { dirname as dirname3 } from "path";
1971
2927
  import { serve } from "@hono/node-server";
1972
2928
  function loadChannelSecret(path) {
1973
2929
  try {
1974
- const s = readFileSync12(path, "utf8").trim();
2930
+ const s = readFileSync13(path, "utf8").trim();
1975
2931
  if (s.length >= 32) return s;
1976
2932
  } catch {
1977
2933
  }
1978
2934
  const secret = randomBytes(32).toString("hex");
1979
- mkdirSync5(dirname3(path), { recursive: true });
1980
- writeFileSync8(path, secret, { mode: 384 });
2935
+ mkdirSync7(dirname3(path), { recursive: true });
2936
+ writeFileSync9(path, secret, { mode: 384 });
1981
2937
  try {
1982
2938
  chmodSync(path, 384);
1983
2939
  } catch {
@@ -2184,8 +3140,8 @@ function startDroneChannel(deps, port) {
2184
3140
  return { port, server };
2185
3141
  }
2186
3142
  function closeDroneChannel(server) {
2187
- return new Promise((resolve3) => {
2188
- server.close(() => resolve3());
3143
+ return new Promise((resolve4) => {
3144
+ server.close(() => resolve4());
2189
3145
  server.closeAllConnections?.();
2190
3146
  });
2191
3147
  }
@@ -2307,7 +3263,7 @@ var init_droneChannel = __esm({
2307
3263
 
2308
3264
  // src/attachments.ts
2309
3265
  import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
2310
- import { join as join13 } from "path";
3266
+ import { join as join17 } from "path";
2311
3267
  function attachmentsFromDoc(doc, origin) {
2312
3268
  const out = [];
2313
3269
  const walk2 = (node) => {
@@ -2398,7 +3354,7 @@ async function materializeAttachments(opts) {
2398
3354
  };
2399
3355
  const result = { files: [], skipped: [] };
2400
3356
  if (opts.refs.length === 0) return result;
2401
- const dir = join13(opts.worktree, ATTACHMENTS_DIR);
3357
+ const dir = join17(opts.worktree, ATTACHMENTS_DIR);
2402
3358
  await io.removeDir(dir);
2403
3359
  let dirReady = false;
2404
3360
  let total = 0;
@@ -2444,7 +3400,7 @@ async function materializeAttachments(opts) {
2444
3400
  await io.mkdir(dir);
2445
3401
  dirReady = true;
2446
3402
  }
2447
- await io.writeFile(join13(dir, name), file.bytes);
3403
+ await io.writeFile(join17(dir, name), file.bytes);
2448
3404
  } catch (e) {
2449
3405
  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)})`);
2450
3406
  continue;
@@ -2597,7 +3553,7 @@ var init_attachments = __esm({
2597
3553
 
2598
3554
  // src/shotUpload.ts
2599
3555
  import { readFile as fsReadFile } from "fs/promises";
2600
- import { basename as basename2, isAbsolute as isAbsolute2, join as join14 } from "path";
3556
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join18 } from "path";
2601
3557
  function shotMimetype(path) {
2602
3558
  const dot = path.lastIndexOf(".");
2603
3559
  if (dot < 0) return null;
@@ -2623,7 +3579,7 @@ async function readShots(paths, opts) {
2623
3579
  }
2624
3580
  let bytes;
2625
3581
  try {
2626
- bytes = await read(isAbsolute2(path) ? path : join14(opts.worktree, path));
3582
+ bytes = await read(isAbsolute3(path) ? path : join18(opts.worktree, path));
2627
3583
  } catch (e) {
2628
3584
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
2629
3585
  continue;
@@ -2701,8 +3657,8 @@ var init_shotUpload = __esm({
2701
3657
 
2702
3658
  // src/oauth.ts
2703
3659
  import { createHash, randomBytes as randomBytes2 } from "crypto";
2704
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2 } from "fs";
2705
- import { join as join15 } from "path";
3660
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
3661
+ import { join as join19 } from "path";
2706
3662
  function makePkce() {
2707
3663
  const verifier = randomBytes2(48).toString("base64url");
2708
3664
  const challenge = createHash("sha256").update(verifier).digest("base64url");
@@ -2786,23 +3742,23 @@ function toStore(clientId, t, now) {
2786
3742
  }
2787
3743
  function loadStore() {
2788
3744
  try {
2789
- return JSON.parse(readFileSync13(STORE_PATH, "utf8"));
3745
+ return JSON.parse(readFileSync14(STORE_PATH, "utf8"));
2790
3746
  } catch {
2791
3747
  return null;
2792
3748
  }
2793
3749
  }
2794
3750
  function saveStore(s) {
2795
- writeFileSync9(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
3751
+ writeFileSync10(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
2796
3752
  }
2797
3753
  function clearStore() {
2798
3754
  try {
2799
- unlinkSync2(STORE_PATH);
3755
+ unlinkSync3(STORE_PATH);
2800
3756
  } catch {
2801
3757
  }
2802
3758
  }
2803
3759
  function loadClientId() {
2804
3760
  try {
2805
- return JSON.parse(readFileSync13(CLIENT_PATH, "utf8")).clientId;
3761
+ return JSON.parse(readFileSync14(CLIENT_PATH, "utf8")).clientId;
2806
3762
  } catch {
2807
3763
  return null;
2808
3764
  }
@@ -2811,7 +3767,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
2811
3767
  const existing = loadClientId();
2812
3768
  if (existing) return existing;
2813
3769
  const clientId = await registerClient({ api, redirectUri }, fetchFn);
2814
- writeFileSync9(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
3770
+ writeFileSync10(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
2815
3771
  return clientId;
2816
3772
  }
2817
3773
  async function ensureAccess(store, opts = {}) {
@@ -2837,18 +3793,18 @@ var init_oauth = __esm({
2837
3793
  AUTHORIZE = "/api/oauth/authorize";
2838
3794
  TOKEN = "/api/oauth/token";
2839
3795
  SCOPE = "openid profile email taskless:full";
2840
- STORE_PATH = join15(SWARM_HOME, "oauth.json");
2841
- CLIENT_PATH = join15(SWARM_HOME, "oauth-client.json");
3796
+ STORE_PATH = join19(SWARM_HOME, "oauth.json");
3797
+ CLIENT_PATH = join19(SWARM_HOME, "oauth-client.json");
2842
3798
  base = (api) => api.replace(/\/$/, "");
2843
3799
  }
2844
3800
  });
2845
3801
 
2846
3802
  // src/agentToken.ts
2847
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
2848
- import { join as join16 } from "path";
3803
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
3804
+ import { join as join20 } from "path";
2849
3805
  function loadAgentToken() {
2850
3806
  try {
2851
- const s = JSON.parse(readFileSync14(STORE_PATH2, "utf8"));
3807
+ const s = JSON.parse(readFileSync15(STORE_PATH2, "utf8"));
2852
3808
  return s.token ? s : null;
2853
3809
  } catch {
2854
3810
  return null;
@@ -2885,17 +3841,17 @@ var init_agentToken = __esm({
2885
3841
  "src/agentToken.ts"() {
2886
3842
  "use strict";
2887
3843
  init_config();
2888
- STORE_PATH2 = join16(SWARM_HOME, "agent.json");
3844
+ STORE_PATH2 = join20(SWARM_HOME, "agent.json");
2889
3845
  base2 = (api) => api.replace(/\/$/, "");
2890
3846
  }
2891
3847
  });
2892
3848
 
2893
3849
  // src/workspace.ts
2894
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
2895
- import { join as join17 } from "path";
3850
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
3851
+ import { join as join21 } from "path";
2896
3852
  function loadWorkspace() {
2897
3853
  try {
2898
- const w = JSON.parse(readFileSync15(WORKSPACE_PATH, "utf8"));
3854
+ const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
2899
3855
  return w && typeof w.id === "string" && w.id ? w : null;
2900
3856
  } catch {
2901
3857
  return null;
@@ -2909,7 +3865,7 @@ var init_workspace = __esm({
2909
3865
  "src/workspace.ts"() {
2910
3866
  "use strict";
2911
3867
  init_config();
2912
- WORKSPACE_PATH = join17(SWARM_HOME, "workspace.json");
3868
+ WORKSPACE_PATH = join21(SWARM_HOME, "workspace.json");
2913
3869
  }
2914
3870
  });
2915
3871
 
@@ -2992,6 +3948,11 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
2992
3948
  if (!doc) return null;
2993
3949
  return { title: doc.title ?? "", body: extractText(doc.body) };
2994
3950
  }
3951
+ async function getTaskContext(idOrSlug) {
3952
+ const res = await safeCall("get_task_context", { id: idOrSlug });
3953
+ const text = res?.content?.find((c) => c.type === "text")?.text;
3954
+ return typeof text === "string" && text.trim() ? text : null;
3955
+ }
2995
3956
  async function resolveId(idOrSlug) {
2996
3957
  const cached = idCache.get(idOrSlug);
2997
3958
  if (cached) return cached;
@@ -3059,6 +4020,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
3059
4020
  },
3060
4021
  backlogStatusId,
3061
4022
  getDocument,
4023
+ getTaskContext,
3062
4024
  createTask: async (input) => {
3063
4025
  if (!input.projectId) {
3064
4026
  throw new Error("createTask: projectId \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u2014 \u0430\u0432\u0442\u043E-\u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0440\u043E\u0435\u043C \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u043E (TASWM-204)");
@@ -3098,7 +4060,19 @@ function requireTaskBody(task, slug, workspaceId) {
3098
4060
  if (!task) throw new TaskBodyUnavailableError(slug, workspaceId);
3099
4061
  return task;
3100
4062
  }
4063
+ function wrapUntrusted(parts) {
4064
+ if (parts.length === 0) return "";
4065
+ return [
4066
+ UNTRUSTED_TASK_INPUT_NOTICE,
4067
+ "",
4068
+ "\u2500\u2500\u2500\u2500\u2500 \u041D\u0415\u0414\u041E\u0412\u0415\u0420\u0415\u041D\u041D\u042B\u0419 \u0412\u0412\u041E\u0414: \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438/\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B \u0437\u0430\u0434\u0430\u0447\u0438 \u2500\u2500\u2500\u2500\u2500",
4069
+ parts.join("\n\n"),
4070
+ "\u2500\u2500\u2500\u2500\u2500 \u041A\u041E\u041D\u0415\u0426 \u041D\u0415\u0414\u041E\u0412\u0415\u0420\u0415\u041D\u041D\u041E\u0413\u041E \u0412\u0412\u041E\u0414\u0410 \u2500\u2500\u2500\u2500\u2500"
4071
+ ].join("\n");
4072
+ }
3101
4073
  async function fetchTaskContext(client, task) {
4074
+ const brief = await client.getTaskContext(task.id);
4075
+ if (brief) return wrapUntrusted([brief]);
3102
4076
  const parts = [];
3103
4077
  const comments = task.comments ?? [];
3104
4078
  if (comments.length > 0) {
@@ -3134,14 +4108,7 @@ ${text || "(\u043F\u0443\u0441\u0442\u043E)"}`;
3134
4108
  )
3135
4109
  );
3136
4110
  }
3137
- if (parts.length === 0) return "";
3138
- return [
3139
- UNTRUSTED_TASK_INPUT_NOTICE,
3140
- "",
3141
- "\u2500\u2500\u2500\u2500\u2500 \u041D\u0415\u0414\u041E\u0412\u0415\u0420\u0415\u041D\u041D\u042B\u0419 \u0412\u0412\u041E\u0414: \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438/\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B \u0437\u0430\u0434\u0430\u0447\u0438 \u2500\u2500\u2500\u2500\u2500",
3142
- parts.join("\n\n"),
3143
- "\u2500\u2500\u2500\u2500\u2500 \u041A\u041E\u041D\u0415\u0426 \u041D\u0415\u0414\u041E\u0412\u0415\u0420\u0415\u041D\u041D\u041E\u0413\u041E \u0412\u0412\u041E\u0414\u0410 \u2500\u2500\u2500\u2500\u2500"
3144
- ].join("\n");
4111
+ return wrapUntrusted(parts);
3145
4112
  }
3146
4113
  function commentIdOf(res) {
3147
4114
  const comment = res?.structuredContent?.comment;
@@ -3336,8 +4303,8 @@ __export(db_exports, {
3336
4303
  SwarmDb: () => SwarmDb
3337
4304
  });
3338
4305
  import { createRequire as createRequire2 } from "module";
3339
- import { existsSync as existsSync9 } from "fs";
3340
- import { resolve as resolve2 } from "path";
4306
+ import { existsSync as existsSync11 } from "fs";
4307
+ import { resolve as resolve3 } from "path";
3341
4308
  function rowToQuestion(row) {
3342
4309
  return {
3343
4310
  id: row.id,
@@ -3370,6 +4337,10 @@ function rowToRun(row) {
3370
4337
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
3371
4338
  model: row.model ?? null,
3372
4339
  cost_usd: row.cost_usd ?? 0,
4340
+ // Токены/время работы (TASWM-353) — старые прогоны без колонок → 0.
4341
+ tokens_in: row.tokens_in ?? 0,
4342
+ tokens_out: row.tokens_out ?? 0,
4343
+ duration_ms: row.duration_ms ?? 0,
3373
4344
  pr_url: row.pr_url ?? null,
3374
4345
  superseded_by: row.superseded_by ?? null,
3375
4346
  decision: row.decision ? JSON.parse(row.decision) : null,
@@ -3435,6 +4406,9 @@ CREATE TABLE IF NOT EXISTS runs (
3435
4406
  depends_on TEXT,
3436
4407
  model TEXT,
3437
4408
  cost_usd REAL NOT NULL DEFAULT 0,
4409
+ tokens_in INTEGER NOT NULL DEFAULT 0,
4410
+ tokens_out INTEGER NOT NULL DEFAULT 0,
4411
+ duration_ms INTEGER NOT NULL DEFAULT 0,
3438
4412
  pr_url TEXT,
3439
4413
  superseded_by INTEGER,
3440
4414
  decision TEXT,
@@ -3528,7 +4502,11 @@ CREATE TABLE IF NOT EXISTS imports (
3528
4502
  // Базовая ветка PR (TASWM-285): куда дрон открывает PR и от чего ответвляется.
3529
4503
  "ALTER TABLE runs ADD COLUMN base_branch TEXT",
3530
4504
  // Воркспейс задачи прогона (TASWM-288): им пинятся задачные MCP-вызовы прогона.
3531
- "ALTER TABLE runs ADD COLUMN workspace_id TEXT"
4505
+ "ALTER TABLE runs ADD COLUMN workspace_id TEXT",
4506
+ // Чем обошёлся прогон, кроме денег (TASWM-353): токены вход/выход и время работы.
4507
+ "ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
4508
+ "ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
4509
+ "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0"
3532
4510
  ]) {
3533
4511
  try {
3534
4512
  this.db.exec(alter);
@@ -3817,11 +4795,11 @@ CREATE TABLE IF NOT EXISTS imports (
3817
4795
  * транзакции: прервётся — маркер не встанет, повтор безопасен.
3818
4796
  */
3819
4797
  importDb(sourcePath, repo) {
3820
- const abs = resolve2(sourcePath);
4798
+ const abs = resolve3(sourcePath);
3821
4799
  const repoN = normalizeRepo(repo);
3822
4800
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
3823
4801
  if (already) return { imported: false, runs: already.run_count };
3824
- if (!existsSync9(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
4802
+ if (!existsSync11(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
3825
4803
  const src = new this.databaseCtor(abs);
3826
4804
  try {
3827
4805
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -4189,9 +5167,9 @@ var init_droneStore = __esm({
4189
5167
  });
4190
5168
 
4191
5169
  // src/overmind.ts
4192
- import { homedir as homedir3 } from "os";
4193
- import { existsSync as existsSync13, readFileSync as readFileSync18, readdirSync as readdirSync10, renameSync as renameSync8 } from "fs";
4194
- import { basename as basename4, dirname as dirname5, join as join23 } from "path";
5170
+ import { homedir as homedir5 } from "os";
5171
+ import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync12, renameSync as renameSync10 } from "fs";
5172
+ import { basename as basename4, dirname as dirname5, join as join28 } from "path";
4195
5173
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
4196
5174
  import { spawn as spawn3 } from "child_process";
4197
5175
 
@@ -4271,13 +5249,13 @@ init_config();
4271
5249
  init_resources();
4272
5250
  init_names2();
4273
5251
  init_storm();
4274
- import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
5252
+ import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
4275
5253
  import { dirname } from "path";
4276
5254
  import { randomUUID } from "crypto";
4277
5255
 
4278
5256
  // src/logTail.ts
4279
- import { closeSync, existsSync as existsSync2, openSync, readSync, readdirSync, statSync } from "fs";
4280
- import { join as join3 } from "path";
5257
+ import { closeSync, existsSync as existsSync4, openSync, readSync, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
5258
+ import { join as join6 } from "path";
4281
5259
  var TAIL_LINES_FALLBACK = 200;
4282
5260
  var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
4283
5261
  function fileHeader(name, size, shown) {
@@ -4287,7 +5265,7 @@ function fileHeader(name, size, shown) {
4287
5265
  function readTail(path, opts) {
4288
5266
  let size;
4289
5267
  try {
4290
- size = statSync(path).size;
5268
+ size = statSync3(path).size;
4291
5269
  } catch {
4292
5270
  return null;
4293
5271
  }
@@ -4318,10 +5296,10 @@ function readTail(path, opts) {
4318
5296
  return { text, size, shown: Buffer.byteLength(text, "utf8") };
4319
5297
  }
4320
5298
  function latestRunLaunch(runLogsDir, localRunId) {
4321
- const dir = join3(runLogsDir, String(localRunId));
5299
+ const dir = join6(runLogsDir, String(localRunId));
4322
5300
  let files;
4323
5301
  try {
4324
- files = readdirSync(dir);
5302
+ files = readdirSync4(dir);
4325
5303
  } catch {
4326
5304
  return null;
4327
5305
  }
@@ -4332,13 +5310,13 @@ function latestRunLaunch(runLogsDir, localRunId) {
4332
5310
  }
4333
5311
  if (indexes.size === 0) return null;
4334
5312
  const index = Math.max(...indexes);
4335
- const out = [join3(dir, `${index}.json`), join3(dir, `${index}.json.done`)].find((p) => existsSync2(p)) ?? null;
4336
- const err = existsSync2(join3(dir, `${index}.err.log`)) ? join3(dir, `${index}.err.log`) : null;
5313
+ const out = [join6(dir, `${index}.json`), join6(dir, `${index}.json.done`)].find((p) => existsSync4(p)) ?? null;
5314
+ const err = existsSync4(join6(dir, `${index}.err.log`)) ? join6(dir, `${index}.err.log`) : null;
4337
5315
  return { index, total: indexes.size, out, err };
4338
5316
  }
4339
5317
  function runLogTail(runLogsDir, localRunId, opts) {
4340
5318
  const launch = latestRunLaunch(runLogsDir, localRunId);
4341
- 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 (${join3(runLogsDir, String(localRunId))} \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442)` };
5319
+ 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)` };
4342
5320
  const parts = [];
4343
5321
  let budget = opts.maxBytes;
4344
5322
  if (launch.err) {
@@ -4471,6 +5449,15 @@ var SwarmClient = class _SwarmClient {
4471
5449
  nextRunId = 1;
4472
5450
  nextEventId = 1;
4473
5451
  nextQuestionId = 1;
5452
+ // TASWM-350: ключ НЕЗАКРЫТОЙ попытки взять работу. Живёт от первого вызова claim до
5453
+ // первого настоящего ответа прода; всё это время ретраи идут с ним, и повтор после
5454
+ // обрыва связи не превращается во второе взятие. См. claim().
5455
+ pendingClaimKey = null;
5456
+ // TASWM-357: последнее НАПЕЧАТАННОЕ про пропущенные claim'ом задачи (родители и
5457
+ // заблокированные). Дедуп по тексту: claim идёт раз в ~60 c, а набор пропущенных меняется
5458
+ // редко — без этого лог был бы простынёй из одной и той же строки, и в ней потонуло бы
5459
+ // всё остальное. Меняется набор (эпик закрыли / блокер прошли) → печатаем снова.
5460
+ lastClaimSkipLog = null;
4474
5461
  // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
4475
5462
  // Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
4476
5463
  // переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
@@ -4481,6 +5468,9 @@ var SwarmClient = class _SwarmClient {
4481
5468
  // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
4482
5469
  // каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
4483
5470
  notifyLevelUnsupported = false;
5471
+ // TAS-485: то же для гашения разрешившихся тревог (swarm_notify_resolve) — прод старее
5472
+ // демона не знает тула; ретраить это каждый тик бессмысленно, см. resolveAnomalies.
5473
+ notifyResolveUnsupported = false;
4484
5474
  // Корреляция с продом.
4485
5475
  keyByLocal = /* @__PURE__ */ new Map();
4486
5476
  // local run id → runKey
@@ -4523,24 +5513,24 @@ var SwarmClient = class _SwarmClient {
4523
5513
  this.repoFullNames = registry.map((e) => e.repoFullName);
4524
5514
  this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
4525
5515
  this.host = opts.host ?? null;
4526
- this.repoExists = opts.repoExists ?? existsSync3;
5516
+ this.repoExists = opts.repoExists ?? existsSync5;
4527
5517
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
4528
5518
  this.outboxPath = opts.outboxPath;
4529
5519
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
4530
5520
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
4531
- mkdirSync2(dirname(this.outboxPath), { recursive: true });
5521
+ mkdirSync4(dirname(this.outboxPath), { recursive: true });
4532
5522
  this.recover();
4533
5523
  }
4534
5524
  // ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
4535
5525
  recover() {
4536
- if (existsSync3(this.mapPath)) {
5526
+ if (existsSync5(this.mapPath)) {
4537
5527
  try {
4538
5528
  const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
4539
5529
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
4540
5530
  } catch {
4541
5531
  }
4542
5532
  }
4543
- if (existsSync3(this.outboxPath)) {
5533
+ if (existsSync5(this.outboxPath)) {
4544
5534
  const txt = readFileSync4(this.outboxPath, "utf8");
4545
5535
  for (const line of txt.split("\n")) {
4546
5536
  const s = line.trim();
@@ -4561,7 +5551,7 @@ var SwarmClient = class _SwarmClient {
4561
5551
  // eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
4562
5552
  // переприсваиваем локально (id событий — только порядок, не корреляция с продом).
4563
5553
  replayGlobalEvents() {
4564
- if (!existsSync3(this.stormPath)) return;
5554
+ if (!existsSync5(this.stormPath)) return;
4565
5555
  let txt;
4566
5556
  try {
4567
5557
  txt = readFileSync4(this.stormPath, "utf8");
@@ -4601,6 +5591,18 @@ var SwarmClient = class _SwarmClient {
4601
5591
  runKeyFor(localId) {
4602
5592
  return this.keyByLocal.get(localId);
4603
5593
  }
5594
+ /**
5595
+ * TASWM-336: заменить репо-реестр на лету. До этой задачи реестр собирался ОДИН РАЗ на
5596
+ * старте из окружения и жил до рестарта демона; теперь машина сама находит клоны своих
5597
+ * репозиториев и переспрашивает прод по TTL — у владельца появился новый swarm-проект,
5598
+ * и машина обязана подхватить его без рестарта. Меняем обе стороны разом: и список для
5599
+ * claim/heartbeat, и обратную карту `owner/repo` → путь (иначе прогон нового репо
5600
+ * пришёл бы именем, которое не во что перевести).
5601
+ */
5602
+ setRepoRegistry(registry) {
5603
+ this.repoFullNames = registry.map((e) => e.repoFullName);
5604
+ this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
5605
+ }
4604
5606
  // cuid прода → локальный id (по runKey гидрированного прогона `p:<cuid>`).
4605
5607
  // Для маппинга прод heroId → локальный hero_run_id при гидрации. Резолвится, когда
4606
5608
  // прогон-герой присутствует в зеркале; иначе null (уточнится следующим рефрешем).
@@ -4618,8 +5620,8 @@ var SwarmClient = class _SwarmClient {
4618
5620
  this.scheduleFlush();
4619
5621
  }
4620
5622
  // Прод-патч прогона: локальные поля Run → поля swarm_run_upsert (см. TASWM-74).
4621
- // pid/worktree_path (рантайм), respawns (нет в схеме тула), снимок задачи (task_*,
4622
- // прод берёт join'ом из Task) и priority (локальная очередь ЭТОГО демона, TASWM-269)
5623
+ // pid/worktree_path (рантайм), снимок задачи (task_*, прод берёт join'ом из Task)
5624
+ // и priority (локальная очередь ЭТОГО демона, TASWM-269)
4623
5625
  // в прод НЕ уходят. kind → role (плоский enum). Self-ref
4624
5626
  // прогонов hero_run_id/resolves_run_id/superseded_by → heroId/resolvesRunId/
4625
5627
  // supersededById как ОТЛОЖЕННЫЕ ссылки (RunRef) — cuid резолвится на флаше.
@@ -4630,6 +5632,10 @@ var SwarmClient = class _SwarmClient {
4630
5632
  if ("model" in patch) out.model = patch.model ?? null;
4631
5633
  if ("name" in patch) out.droneName = patch.name ?? null;
4632
5634
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
5635
+ if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
5636
+ if ("tokens_out" in patch && patch.tokens_out !== void 0) out.tokensOut = patch.tokens_out;
5637
+ if ("duration_ms" in patch && patch.duration_ms !== void 0) out.durationMs = patch.duration_ms;
5638
+ if ("respawns" in patch && patch.respawns !== void 0) out.respawns = patch.respawns;
4633
5639
  if ("pr_url" in patch) out.prUrl = patch.pr_url ?? null;
4634
5640
  if ("branch" in patch) out.branch = patch.branch ?? null;
4635
5641
  if ("repo" in patch) out.repo = patch.repo ?? null;
@@ -4695,6 +5701,10 @@ var SwarmClient = class _SwarmClient {
4695
5701
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
4696
5702
  model: input.model ?? null,
4697
5703
  cost_usd: 0,
5704
+ tokens_in: 0,
5705
+ // копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
5706
+ tokens_out: 0,
5707
+ duration_ms: 0,
4698
5708
  pr_url: null,
4699
5709
  superseded_by: null,
4700
5710
  decision: input.decision ?? null,
@@ -4817,6 +5827,7 @@ var SwarmClient = class _SwarmClient {
4817
5827
  if (!d) return 0;
4818
5828
  d.respawns += 1;
4819
5829
  d.updated_at = stamp();
5830
+ this.enqueueRun(d, this.runFields({ respawns: d.respawns }));
4820
5831
  return d.respawns;
4821
5832
  }
4822
5833
  cancel(id, reason) {
@@ -5232,7 +6243,7 @@ var SwarmClient = class _SwarmClient {
5232
6243
  * транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
5233
6244
  */
5234
6245
  reconcileRuntime(opts) {
5235
- const wtExists = opts.worktreeExists ?? existsSync3;
6246
+ const wtExists = opts.worktreeExists ?? existsSync5;
5236
6247
  const pidByWorktree = /* @__PURE__ */ new Map();
5237
6248
  try {
5238
6249
  for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
@@ -5320,23 +6331,69 @@ var SwarmClient = class _SwarmClient {
5320
6331
  * шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
5321
6332
  */
5322
6333
  async claim() {
6334
+ this.pendingClaimKey ??= randomUUID();
5323
6335
  let res;
5324
6336
  try {
5325
6337
  res = await this.call("swarm_claim_work", {
5326
6338
  ...this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {},
5327
6339
  // TASWM-281: взятая из общей очереди задача сразу принадлежит ЭТОЙ машине —
5328
6340
  // прод помечает созданный прогон hostId, чужой демон его не увидит.
5329
- ...this.host ? { hostId: this.host.hostId } : {}
6341
+ ...this.host ? { hostId: this.host.hostId } : {},
6342
+ idempotencyKey: this.pendingClaimKey
5330
6343
  });
5331
6344
  } catch {
5332
6345
  return null;
5333
6346
  }
5334
6347
  if (res?.isError) return null;
6348
+ this.pendingClaimKey = null;
5335
6349
  const sc = res.structuredContent ?? {};
6350
+ this.logClaimSkips(sc);
5336
6351
  if (!sc.claimed || typeof sc.run !== "object" || sc.run === null) return null;
5337
6352
  const local = this.hydrateRun(sc.run);
5338
6353
  return local ? this.mat(local) : null;
5339
6354
  }
6355
+ /**
6356
+ * TASWM-357: назвать в логе задачи, которые прод НЕ отдал в работу осознанно — родителей с
6357
+ * незавершёнными подзадачами (работа всегда в листьях) и листья с незакрытым блокером
6358
+ * (явный порядок, связь задач «блокирует»). Печатаем ЗДЕСЬ, а не на проде: оба фильтра
6359
+ * живут SQL-условиями и молчат по природе, а `display` claim'а демон не логирует — без
6360
+ * этой строки «почему эпик стоит нетронутым» не видно ниоткуда, и оператор ровно так же
6361
+ * гадает, как гадал, пока дрон вставал на эпике с вопросом «меня спавнили на родителя».
6362
+ *
6363
+ * Формат прода — `data.skippedParents: [{slug,title,openChildren}]` и
6364
+ * `data.skippedBlocked: [{slug,title,blockers[]}]`. Читаем защищённо: у старого прода
6365
+ * полей нет вовсе (тогда молчим), а разбирать чужую строку `display` вместо структуры —
6366
+ * тот же класс хрупкости, что уже кусал нас в notifyAnomaly.
6367
+ */
6368
+ logClaimSkips(sc) {
6369
+ const rows = (raw) => (Array.isArray(raw) ? raw : []).map((p) => typeof p === "object" && p !== null ? p : null).filter((p) => p !== null && typeof p.slug === "string");
6370
+ const others = (flag) => flag === true ? " \u0438 \u0434\u0440\u0443\u0433\u0438\u0435" : "";
6371
+ const lines = [];
6372
+ const parents = rows(sc.skippedParents).map(
6373
+ (p) => `${p.slug} (${typeof p.openChildren === "number" ? p.openChildren : "?"} \u043D\u0435\u0437\u0430\u043A\u0440\u044B\u0442\u044B\u0445 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447)`
6374
+ );
6375
+ if (parents.length > 0) {
6376
+ lines.push(
6377
+ `[swarm-client] claim \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438\u043B \u0437\u0430\u0434\u0430\u0447\u0438-\u0440\u043E\u0434\u0438\u0442\u0435\u043B\u0438 \u2014 \u0440\u0430\u0431\u043E\u0442\u0430 \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0432 \u043B\u0438\u0441\u0442\u044C\u044F\u0445, \u0434\u0440\u043E\u043D\u0430 \u043D\u0430 \u043D\u0438\u0445 \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0438\u043C: ${parents.join(", ")}${others(sc.skippedParentsMore)}`
6378
+ );
6379
+ }
6380
+ const blocked = rows(sc.skippedBlocked).map(
6381
+ (b) => `${b.slug} \u0436\u0434\u0451\u0442 ${Array.isArray(b.blockers) && b.blockers.length > 0 ? b.blockers.join(", ") : "\u0431\u043B\u043E\u043A\u0435\u0440\u0430"}`
6382
+ );
6383
+ if (blocked.length > 0) {
6384
+ lines.push(
6385
+ `[swarm-client] claim \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438\u043B \u0437\u0430\u0434\u0430\u0447\u0438 \u0441 \u043D\u0435\u0437\u0430\u043A\u0440\u044B\u0442\u044B\u043C \u0431\u043B\u043E\u043A\u0435\u0440\u043E\u043C \u2014 \u043F\u043E\u0440\u044F\u0434\u043E\u043A \u0437\u0430\u0434\u0430\u043D \u0441\u0432\u044F\u0437\u044C\u044E \xAB\u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442\xBB, \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u0438\u0442\u044C \u043D\u0435 \u0431\u0443\u0434\u0443: ${blocked.join("; ")}${others(sc.skippedBlockedMore)}`
6386
+ );
6387
+ }
6388
+ const text = lines.join("\n");
6389
+ if (text.length === 0) {
6390
+ this.lastClaimSkipLog = null;
6391
+ return;
6392
+ }
6393
+ if (text === this.lastClaimSkipLog) return;
6394
+ this.lastClaimSkipLog = text;
6395
+ for (const line of lines) console.log(line);
6396
+ }
5340
6397
  /**
5341
6398
  * TASWM-281: пометить своими прогоны, под которыми на ЭТОЙ машине нашёлся ЖИВОЙ
5342
6399
  * процесс (после reconcileRuntime на старте). Это ровно те прогоны, где сомнений в
@@ -5610,12 +6667,20 @@ var SwarmClient = class _SwarmClient {
5610
6667
  * Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
5611
6668
  * означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
5612
6669
  *
6670
+ * TASWM-352: тем же пульсом уходит ЁМКОСТЬ этой машины (`capacity` — живой потолок
6671
+ * дронов ЭТОГО демона: машинный оверрайд, иначе общая настройка владельца). По ней
6672
+ * планировщик роя раскладывает очередь между машинами: до этого у прода было ОДНО
6673
+ * число на владельца, и ноутбук с рабочей станцией делили его поровну, хотя тянут
6674
+ * разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
6675
+ * трогает прошлое значение — та же логика, что у repoFullNames.
6676
+ *
5613
6677
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
5614
6678
  */
5615
- async heartbeat(daemonVersion, pause) {
6679
+ async heartbeat(daemonVersion, pause, capacity) {
5616
6680
  try {
5617
6681
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
5618
6682
  if (daemonVersion) args.daemonVersion = daemonVersion;
6683
+ if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
5619
6684
  if (pause !== void 0) {
5620
6685
  args.paused = !!pause?.paused;
5621
6686
  if (pause?.reason) args.pauseReason = pause.reason;
@@ -5721,6 +6786,35 @@ var SwarmClient = class _SwarmClient {
5721
6786
  const res = await this.call("swarm_notify", base3);
5722
6787
  if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
5723
6788
  }
6789
+ /**
6790
+ * TAS-485: погасить в инбоксе владельца тревоги, чьё условие ушло. Ключи — те же, с
6791
+ * которыми уходил notifyAnomaly (прод сам ставит им префикс `swarm:`).
6792
+ *
6793
+ * Бросает при сбое — СОЗНАТЕЛЬНО, как notifyAnomaly: вызывающий (tickOnce) вычёркивает
6794
+ * ключи из durable-очереди только после успеха, иначе «погашено» разошлось бы с тем,
6795
+ * что человек видит в инбоксе, и разошлось бы навсегда — повторить будет некому.
6796
+ *
6797
+ * ДЕПЛОЙ-ОКНО. Демон из свежего чекаута против ещё не задеплоенного прода получит
6798
+ * «неизвестный тул». Это не ошибка доставки, а отсутствие фичи на той стороне: ретраить
6799
+ * её каждые ~4 секунды бессмысленно, поэтому первую такую осечку запоминаем на жизнь
6800
+ * процесса и дальше молча пропускаем гашение (надзор в остальном работает как раньше).
6801
+ * Тот же приём, что у notifyAnomaly с аргументом `level`.
6802
+ */
6803
+ async resolveAnomalies(keys) {
6804
+ const tags = [...new Set(keys.filter((k) => k.length > 0 && k.length <= 120))];
6805
+ if (tags.length === 0 || this.notifyResolveUnsupported) return;
6806
+ const res = await this.call("swarm_notify_resolve", { tags });
6807
+ if (!res?.isError) return;
6808
+ const text = JSON.stringify(res);
6809
+ if (text.includes("swarm_notify_resolve") && (text.includes("Unknown tool") || text.includes("\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D"))) {
6810
+ this.notifyResolveUnsupported = true;
6811
+ console.warn(
6812
+ "[swarm-client] \u043F\u0440\u043E\u0434 \u0441\u0442\u0430\u0440\u0435\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 (\u043D\u0435\u0442 swarm_notify_resolve) \u2014 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0432\u0448\u0438\u0435\u0441\u044F \u0442\u0440\u0435\u0432\u043E\u0433\u0438 \u0440\u043E\u044F \u043F\u0440\u0438\u0434\u0451\u0442\u0441\u044F \u0433\u0430\u0441\u0438\u0442\u044C \u0432 \u0438\u043D\u0431\u043E\u043A\u0441\u0435 \u0440\u0443\u043A\u0430\u043C\u0438. \u041E\u0431\u043D\u043E\u0432\u0438 \u043F\u0440\u043E\u0434 \u0438 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438 \u0434\u0435\u043C\u043E\u043D."
6813
+ );
6814
+ return;
6815
+ }
6816
+ throw new Error(`swarm_notify_resolve \u043E\u0442\u0432\u0435\u0440\u0433 ${tags.length} \u043A\u043B\u044E\u0447(\u0435\u0439)`);
6817
+ }
5724
6818
  /**
5725
6819
  * TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
5726
6820
  * через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
@@ -5785,6 +6879,10 @@ var SwarmClient = class _SwarmClient {
5785
6879
  depends_on: [],
5786
6880
  model: null,
5787
6881
  cost_usd: 0,
6882
+ tokens_in: 0,
6883
+ // токены и время работы (TASWM-353) — прод-поля, заполняются ниже
6884
+ tokens_out: 0,
6885
+ duration_ms: 0,
5788
6886
  pr_url: null,
5789
6887
  superseded_by: null,
5790
6888
  decision: null,
@@ -5795,9 +6893,9 @@ var SwarmClient = class _SwarmClient {
5795
6893
  review_verdict: null,
5796
6894
  review_phase: null,
5797
6895
  // вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
5798
- // Локальная бухгалтерия (в прод-схему не уходит, как respawns) — на гидрации из
5799
- // прода дефолт 0; существующая локальная запись НЕ перетирается (поле не в field-copy
5800
- // ниже), в отличие от respawns, — авто-ревью v2, TASWM-162.
6896
+ // Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
6897
+ // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
6898
+ // в отличие от respawns, — авто-ревью v2, TASWM-162.
5801
6899
  review_cycle: 0,
5802
6900
  task_title: null,
5803
6901
  task_desc: null,
@@ -5820,8 +6918,9 @@ var SwarmClient = class _SwarmClient {
5820
6918
  this.runs.set(id, local);
5821
6919
  this.keyByLocal.set(id, runKey);
5822
6920
  }
5823
- const slug = pr.taskSlug ?? pr.task?.slug ?? pr.taskId ?? local.task_slug;
5824
- local.task_slug = slug || local.task_slug;
6921
+ const prodSlug = pr.taskSlug ?? pr.task?.slug ?? null;
6922
+ if (prodSlug) local.task_slug = prodSlug;
6923
+ else if (!isTaskSlug(local.task_slug)) local.task_slug = pr.taskId || local.task_slug;
5825
6924
  local.task_title = pr.taskTitle ?? local.task_title;
5826
6925
  local.task_desc = pr.taskDesc ?? local.task_desc;
5827
6926
  local.name = pr.droneName ?? local.name;
@@ -5848,6 +6947,9 @@ var SwarmClient = class _SwarmClient {
5848
6947
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
5849
6948
  local.model = pr.model ?? null;
5850
6949
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
6950
+ local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
6951
+ local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
6952
+ local.duration_ms = typeof pr.durationMs === "number" ? pr.durationMs : 0;
5851
6953
  local.pr_url = pr.prUrl ?? null;
5852
6954
  local.stop_reason = pr.stopReason ?? null;
5853
6955
  local.respawns = typeof pr.respawns === "number" ? pr.respawns : 0;
@@ -6105,13 +7207,13 @@ var SwarmClient = class _SwarmClient {
6105
7207
  }
6106
7208
  persistPending() {
6107
7209
  try {
6108
- writeFileSync3(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
7210
+ writeFileSync4(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
6109
7211
  } catch {
6110
7212
  }
6111
7213
  }
6112
7214
  persistMap() {
6113
7215
  try {
6114
- writeFileSync3(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
7216
+ writeFileSync4(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
6115
7217
  } catch {
6116
7218
  }
6117
7219
  }
@@ -6140,6 +7242,66 @@ function parseDecision(v) {
6140
7242
  init_config();
6141
7243
  init_heartbeat();
6142
7244
 
7245
+ // src/logRotate.ts
7246
+ import { copyFileSync, renameSync as renameSync4, statSync as statSync4, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
7247
+ var realIo = {
7248
+ size: (path) => {
7249
+ try {
7250
+ return statSync4(path).size;
7251
+ } catch {
7252
+ return null;
7253
+ }
7254
+ },
7255
+ copy: copyFileSync,
7256
+ rename: renameSync4,
7257
+ truncate: (path) => truncateSync(path, 0),
7258
+ unlink: unlinkSync2,
7259
+ exists: existsSync6
7260
+ };
7261
+ var realOwnerIo = {
7262
+ statFd: (fd) => {
7263
+ try {
7264
+ const s = fstatSync(fd);
7265
+ return { dev: s.dev, ino: s.ino, isFile: s.isFile() };
7266
+ } catch {
7267
+ return null;
7268
+ }
7269
+ },
7270
+ statPath: (path) => {
7271
+ try {
7272
+ const s = statSync4(path);
7273
+ return { dev: s.dev, ino: s.ino };
7274
+ } catch {
7275
+ return null;
7276
+ }
7277
+ }
7278
+ };
7279
+ function stdoutGoesTo(path, io = realOwnerIo) {
7280
+ const out = io.statFd(1);
7281
+ if (!out || !out.isFile) return false;
7282
+ const file = io.statPath(path);
7283
+ return file != null && file.dev === out.dev && file.ino === out.ino;
7284
+ }
7285
+ var STRAY_GENERATIONS = 8;
7286
+ function rotateDaemonLog(path, opts, io = realIo) {
7287
+ const size = io.size(path);
7288
+ if (size == null) return "missing";
7289
+ if (size <= opts.maxBytes) return "kept";
7290
+ const keep = Math.max(0, Math.trunc(opts.keep));
7291
+ const gen = (n) => `${path}.${n}`;
7292
+ for (let i = keep; i >= 1; i--) {
7293
+ if (!io.exists(gen(i))) continue;
7294
+ if (i === keep) io.unlink(gen(i));
7295
+ else io.rename(gen(i), gen(i + 1));
7296
+ }
7297
+ for (let i = keep + 1; i <= keep + STRAY_GENERATIONS; i++) {
7298
+ if (io.exists(gen(i))) io.unlink(gen(i));
7299
+ }
7300
+ if (keep >= 1) io.copy(path, gen(1));
7301
+ io.truncate(path);
7302
+ return "rotated";
7303
+ }
7304
+
6143
7305
  // src/repoRegistry.ts
6144
7306
  init_config();
6145
7307
  import { $ } from "execa";
@@ -6178,13 +7340,13 @@ async function buildRepoRegistry(paths) {
6178
7340
  }
6179
7341
 
6180
7342
  // src/repoMemory.ts
6181
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
6182
- import { join as join4 } from "path";
7343
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
7344
+ import { join as join7 } from "path";
6183
7345
  var FILE = "repos.json";
6184
7346
  function loadRememberedRepos(home) {
6185
7347
  let raw;
6186
7348
  try {
6187
- raw = readFileSync6(join4(home, FILE), "utf8");
7349
+ raw = readFileSync6(join7(home, FILE), "utf8");
6188
7350
  } catch {
6189
7351
  return [];
6190
7352
  }
@@ -6199,7 +7361,7 @@ function loadRememberedRepos(home) {
6199
7361
  function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
6200
7362
  const clean = paths.filter((p) => p.trim().length > 0);
6201
7363
  if (clean.length === 0) return;
6202
- writeFileSync5(join4(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
7364
+ writeFileSync6(join7(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
6203
7365
  `);
6204
7366
  }
6205
7367
  function pickRepoPaths(args) {
@@ -6209,9 +7371,16 @@ function pickRepoPaths(args) {
6209
7371
  return { paths: [], source: "none" };
6210
7372
  }
6211
7373
 
7374
+ // src/repoDiscovery.ts
7375
+ init_src();
7376
+ init_config();
7377
+ import { homedir as homedir3 } from "os";
7378
+ import { readFileSync as readFileSync8, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
7379
+ import { isAbsolute, join as join9, resolve as resolve2 } from "path";
7380
+
6212
7381
  // src/repoScan.ts
6213
- import { readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
6214
- import { join as join5 } from "path";
7382
+ import { readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
7383
+ import { join as join8 } from "path";
6215
7384
  var SKIP = /* @__PURE__ */ new Set([
6216
7385
  "node_modules",
6217
7386
  "vendor",
@@ -6241,12 +7410,16 @@ function parseOriginFromGitConfig(text) {
6241
7410
  }
6242
7411
  function originOf(dir) {
6243
7412
  try {
6244
- return parseOriginFromGitConfig(readFileSync7(join5(dir, ".git", "config"), "utf8"));
7413
+ return parseOriginFromGitConfig(readFileSync7(join8(dir, ".git", "config"), "utf8"));
6245
7414
  } catch {
6246
7415
  return null;
6247
7416
  }
6248
7417
  }
6249
- function scanRoots(roots, opts = {}) {
7418
+ function repoFullNameAt(dir) {
7419
+ const origin = originOf(dir);
7420
+ return origin ? parseRepoFullName(origin) : null;
7421
+ }
7422
+ function scanRootsAll(roots, opts = {}) {
6250
7423
  const maxDepth = opts.maxDepth ?? 3;
6251
7424
  const found = /* @__PURE__ */ new Map();
6252
7425
  for (const root of roots) walk(root, 0, maxDepth, found);
@@ -6257,20 +7430,24 @@ function walk(dir, depth, maxDepth, found) {
6257
7430
  const origin = originOf(dir);
6258
7431
  if (origin) {
6259
7432
  const full = parseRepoFullName(origin);
6260
- if (full && !found.has(full)) found.set(full, dir);
7433
+ if (full) {
7434
+ const paths = found.get(full) ?? [];
7435
+ if (!paths.includes(dir)) paths.push(dir);
7436
+ found.set(full, paths);
7437
+ }
6261
7438
  return;
6262
7439
  }
6263
7440
  let entries;
6264
7441
  try {
6265
- entries = readdirSync2(dir);
7442
+ entries = readdirSync5(dir);
6266
7443
  } catch {
6267
7444
  return;
6268
7445
  }
6269
7446
  for (const name of entries) {
6270
7447
  if (name.startsWith(".") || SKIP.has(name)) continue;
6271
- const child = join5(dir, name);
7448
+ const child = join8(dir, name);
6272
7449
  try {
6273
- if (!statSync2(child).isDirectory()) continue;
7450
+ if (!statSync5(child).isDirectory()) continue;
6274
7451
  } catch {
6275
7452
  continue;
6276
7453
  }
@@ -6290,25 +7467,178 @@ function defaultScanRoots(home, knownPaths = []) {
6290
7467
  if (parent && parent !== p) add(parent);
6291
7468
  }
6292
7469
  for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
6293
- add(join5(home, name));
7470
+ add(join8(home, name));
6294
7471
  }
6295
7472
  return out;
6296
7473
  }
6297
7474
 
7475
+ // src/repoDiscovery.ts
7476
+ function resolveScanRoots(raw, home = homedir3(), knownPaths = []) {
7477
+ const v = (raw ?? "").trim();
7478
+ if (!v) return defaultScanRoots(home, knownPaths);
7479
+ if (["off", "none", "0"].includes(v.toLowerCase())) return [];
7480
+ const out = [];
7481
+ for (const part of v.split(",")) {
7482
+ const p = part.trim();
7483
+ if (!p) continue;
7484
+ const abs = p === "~" ? home : p.startsWith("~/") ? join9(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
7485
+ if (!out.includes(abs)) out.push(abs);
7486
+ }
7487
+ return out;
7488
+ }
7489
+ function normalizeFullNames(list) {
7490
+ const out = [];
7491
+ for (const raw of list) {
7492
+ const v = (raw ?? "").trim().toLowerCase();
7493
+ if (v && !out.includes(v)) out.push(v);
7494
+ }
7495
+ return out;
7496
+ }
7497
+ async function fetchSwarmRepos(call) {
7498
+ let res;
7499
+ try {
7500
+ res = await call("swarm_my_repos", {});
7501
+ } catch {
7502
+ return null;
7503
+ }
7504
+ if (!res || res.isError) return null;
7505
+ const raw = res.structuredContent?.repos;
7506
+ if (!Array.isArray(raw)) return null;
7507
+ const out = [];
7508
+ for (const item of raw) {
7509
+ if (!item || typeof item !== "object") continue;
7510
+ const r = item;
7511
+ const repoFullName = typeof r.repoFullName === "string" ? r.repoFullName.trim().toLowerCase() : "";
7512
+ if (!repoFullName) continue;
7513
+ out.push({
7514
+ repoFullName,
7515
+ taskPrefix: typeof r.taskPrefix === "string" ? r.taskPrefix : null,
7516
+ workspaceSlug: typeof r.workspaceSlug === "string" ? r.workspaceSlug : ""
7517
+ });
7518
+ }
7519
+ return out;
7520
+ }
7521
+ function emptyRepoMapState(roots) {
7522
+ return { clones: [], missing: [], roots: [...roots], at: null, unknown: true };
7523
+ }
7524
+ async function refreshRepoMap(opts) {
7525
+ const log = opts.log ?? (() => {
7526
+ });
7527
+ const scan = opts.scan ?? scanRootsAll;
7528
+ const repoAt = opts.repoAt ?? repoFullNameAt;
7529
+ const wanted = await fetchSwarmRepos(opts.call);
7530
+ const clones = /* @__PURE__ */ new Map();
7531
+ const take = (repoFullName, path, source, alternatives = []) => {
7532
+ if (clones.has(repoFullName)) return;
7533
+ clones.set(repoFullName, { repoFullName, path, source, alternatives });
7534
+ };
7535
+ for (const e of opts.configured) take(e.repoFullName.toLowerCase(), e.path, "configured");
7536
+ for (const e of readRepoMapCache(opts.cachePath)) {
7537
+ const name = e.repoFullName.toLowerCase();
7538
+ if (clones.has(name)) continue;
7539
+ if (repoAt(e.path) === name) take(name, e.path, "cache");
7540
+ }
7541
+ const wantedNames = wanted ? normalizeFullNames(wanted.map((r) => r.repoFullName)) : [];
7542
+ const needScan = wanted ? wantedNames.some((n) => !clones.has(n)) : clones.size === 0;
7543
+ if (needScan) {
7544
+ const startedAt = Date.now();
7545
+ const scanned = scan(opts.roots, opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {});
7546
+ let added = 0;
7547
+ for (const [full, paths] of scanned) {
7548
+ if (clones.has(full) || !paths.length) continue;
7549
+ take(full, paths[0], "scan", paths.slice(1));
7550
+ added++;
7551
+ }
7552
+ if (added > 0) {
7553
+ 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`);
7554
+ }
7555
+ }
7556
+ const registry = [...clones.values()].map((c) => ({
7557
+ repoFullName: c.repoFullName,
7558
+ path: normalizeRepo(c.path)
7559
+ }));
7560
+ writeRepoMapCache(
7561
+ opts.cachePath,
7562
+ [...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path }))
7563
+ );
7564
+ if (!wanted) {
7565
+ log(
7566
+ "[overmind] \u043F\u0440\u043E\u0434 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B, \u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443 \u0440\u043E\u044F (swarm_my_repos) \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. \u0421\u0435\u0440\u0432\u0435\u0440 \u0441\u0442\u0430\u0440\u0435\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u044D\u0442\u043E \u043E\u0436\u0438\u0434\u0430\u0435\u043C\u043E \u0438 \u043B\u0435\u0447\u0438\u0442\u0441\u044F \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435\u043C \u043F\u0440\u043E\u0434\u0430."
7567
+ );
7568
+ return registry;
7569
+ }
7570
+ opts.state.unknown = false;
7571
+ opts.state.clones = wantedNames.map((n) => clones.get(n)).filter((c) => !!c);
7572
+ opts.state.missing = wantedNames.filter((n) => !clones.has(n));
7573
+ opts.state.roots = [...opts.roots];
7574
+ opts.state.at = stamp();
7575
+ if (wantedNames.length === 0) {
7576
+ 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");
7577
+ return registry;
7578
+ }
7579
+ 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:`);
7580
+ for (const c of opts.state.clones) log(`[overmind] ${describeClone(c)}`);
7581
+ const prefixOf = new Map(wanted.map((r) => [r.repoFullName, r.taskPrefix]));
7582
+ for (const name of opts.state.missing) {
7583
+ const prefix = prefixOf.get(name);
7584
+ log(`[overmind] ${describeMissing(prefix ? `${name} (\u043F\u0440\u043E\u0435\u043A\u0442 ${prefix})` : name, opts.roots)}`);
7585
+ }
7586
+ return registry;
7587
+ }
7588
+ function describeClone(c) {
7589
+ 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";
7590
+ 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` : "";
7591
+ return `${c.repoFullName} \u2192 ${c.path} (${how})${alt}`;
7592
+ }
7593
+ function describeMissing(repoFullName, roots) {
7594
+ const where = roots.length ? `\u0438\u0441\u043A\u0430\u043B \u0432: ${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)";
7595
+ return `${repoFullName} \u2014 \u041A\u041B\u041E\u041D\u0410 \u041D\u0410 \u042D\u0422\u041E\u0419 \u041C\u0410\u0428\u0418\u041D\u0415 \u041D\u0415\u0422 (${where}). \u0421\u0430\u043C \u043D\u0435 \u043A\u043B\u043E\u043D\u0438\u0440\u0443\u044E: \u0441\u043A\u043B\u043E\u043D\u0438\u0440\u0443\u0439 \u0440\u0443\u043A\u0430\u043C\u0438 \u043B\u0438\u0431\u043E \u0443\u043A\u0430\u0436\u0438 \u043F\u0443\u0442\u044C \`swarm start --repo <\u043F\u0443\u0442\u044C>\`.`;
7596
+ }
7597
+ function repoMapRows(state) {
7598
+ return [
7599
+ ...state.clones.map((c) => ({
7600
+ repoFullName: c.repoFullName,
7601
+ path: c.path,
7602
+ source: c.source,
7603
+ alternatives: c.alternatives
7604
+ })),
7605
+ ...state.missing.map((repoFullName) => ({ repoFullName, path: null, source: null, alternatives: [] }))
7606
+ ];
7607
+ }
7608
+ function readRepoMapCache(path) {
7609
+ try {
7610
+ const raw = JSON.parse(readFileSync8(path, "utf8"));
7611
+ if (!Array.isArray(raw?.entries)) return [];
7612
+ return raw.entries.filter(
7613
+ (e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
7614
+ );
7615
+ } catch {
7616
+ return [];
7617
+ }
7618
+ }
7619
+ function writeRepoMapCache(path, entries) {
7620
+ try {
7621
+ const tmp = `${path}.tmp`;
7622
+ writeFileSync7(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
7623
+ renameSync5(tmp, path);
7624
+ } catch {
7625
+ }
7626
+ }
7627
+
6298
7628
  // src/hostIdentity.ts
6299
7629
  import { hostname } from "os";
6300
- import { existsSync as existsSync4, readFileSync as readFileSync8, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
6301
- import { join as join6 } from "path";
7630
+ import { existsSync as existsSync7, readFileSync as readFileSync9, renameSync as renameSync6, writeFileSync as writeFileSync8 } from "fs";
7631
+ import { join as join10 } from "path";
6302
7632
  import { randomUUID as randomUUID2 } from "crypto";
6303
7633
  function hostIdentityPath(home) {
6304
- return join6(home, "host.json");
7634
+ return join10(home, "host.json");
6305
7635
  }
6306
7636
  function loadHostIdentity(deps) {
6307
- const exists = deps.exists ?? existsSync4;
6308
- const readFile2 = deps.readFile ?? ((p) => readFileSync8(p, "utf8"));
7637
+ const exists = deps.exists ?? existsSync7;
7638
+ const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
6309
7639
  const writeFile2 = deps.writeFile ?? ((p, data) => {
6310
- writeFileSync6(`${p}.tmp`, data);
6311
- renameSync4(`${p}.tmp`, p);
7640
+ writeFileSync8(`${p}.tmp`, data);
7641
+ renameSync6(`${p}.tmp`, p);
6312
7642
  });
6313
7643
  const host = deps.hostname ?? hostname;
6314
7644
  const newId = deps.newId ?? randomUUID2;
@@ -6481,96 +7811,24 @@ function reliefAlertBody(cost, pressure, advice, windowMs, minSteps) {
6481
7811
  return body.length <= MAX_ALERT_BODY_CHARS ? body : `${body.slice(0, MAX_ALERT_BODY_CHARS - 1)}\u2026`;
6482
7812
  }
6483
7813
  function reliefAlertTitle(cost, windowMs) {
6484
- return `\u041C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u0442\u044F\u043D\u0435\u0442 \u0440\u043E\u0439: ${cost.pauses} \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043E\u043A \u0437\u0430 ${humanMinutes(windowMs)}, \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E ${humanMinutes(cost.lostMs)}`;
6485
- }
6486
- function buildReliefCostCandidates(cost, t, sustainMs, build) {
6487
- if (!reliefAlertDue(cost, t)) return [];
6488
- return [{ key: "relief_cost", minDurationMs: sustainMs, build }];
6489
- }
6490
-
6491
- // src/launcher.ts
6492
- import { spawn } from "child_process";
6493
- import { closeSync as closeSync2, existsSync as existsSync6, mkdirSync as mkdirSync4, openSync as openSync2, readdirSync as readdirSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
6494
- import { createRequire } from "module";
6495
- import { homedir as homedir2 } from "os";
6496
- import { dirname as dirname2, join as join8 } from "path";
6497
- import { fileURLToPath } from "url";
6498
-
6499
- // src/claudeBin.ts
6500
- import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
6501
- import { win32 as winPath } from "path";
6502
- var CLAUDE_FALLBACK = "claude";
6503
- function compareVersionDirs(a, b) {
6504
- const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
6505
- const pa = parts(a);
6506
- const pb = parts(b);
6507
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
6508
- const d = (pa[i] ?? 0) - (pb[i] ?? 0);
6509
- if (d !== 0) return d;
6510
- }
6511
- return 0;
6512
- }
6513
- function realListDir(dir) {
6514
- try {
6515
- return readdirSync3(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
6516
- } catch {
6517
- return [];
6518
- }
6519
- }
6520
- function winVersionRoots(env, listDir) {
6521
- const roots = [];
6522
- const appData = env.APPDATA;
6523
- if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
6524
- const localAppData = env.LOCALAPPDATA;
6525
- if (localAppData) {
6526
- const packages = winPath.join(localAppData, "Packages");
6527
- for (const name of listDir(packages)) {
6528
- if (!name.startsWith("Claude")) continue;
6529
- roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
6530
- }
6531
- }
6532
- return roots;
6533
- }
6534
- function findWindowsClaude(opts = {}) {
6535
- const env = opts.env ?? process.env;
6536
- const listDir = opts.listDir ?? realListDir;
6537
- const fileExists = opts.fileExists ?? existsSync5;
6538
- let best = null;
6539
- for (const root of winVersionRoots(env, listDir)) {
6540
- for (const version of listDir(root)) {
6541
- const candidate = winPath.join(root, version, "claude.exe");
6542
- if (!fileExists(candidate)) continue;
6543
- if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
6544
- }
6545
- }
6546
- return best?.path ?? null;
6547
- }
6548
- var warnedNoWindowsClaude = false;
6549
- var cachedWindowsBin = null;
6550
- var windowsScanDone = false;
6551
- function resolveClaudeBin(opts = {}) {
6552
- const env = opts.env ?? process.env;
6553
- const platform = opts.platform ?? process.platform;
6554
- const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
6555
- if (explicit) return explicit;
6556
- if (platform !== "win32") return CLAUDE_FALLBACK;
6557
- if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
6558
- windowsScanDone = true;
6559
- const found = findWindowsClaude({ ...opts, env });
6560
- cachedWindowsBin = found;
6561
- if (found) return found;
6562
- if (!warnedNoWindowsClaude) {
6563
- warnedNoWindowsClaude = true;
6564
- console.warn(
6565
- "[overmind] claude.exe \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u043D\u0438 \u0432 %APPDATA%\\Claude\\claude-code, \u043D\u0438 \u0432 MSIX-\u043A\u043E\u043F\u0438\u0438 \u043F\u043E\u0434 %LOCALAPPDATA%\\Packages\\Claude* \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u0433\u043E\u043B\u043E\u0435 \xABclaude\xBB (\u043F\u043E\u0438\u0441\u043A \u043F\u043E PATH+PATHEXT). \u0423\u043F\u0430\u0434\u0451\u0442 ENOENT (bash-\u043E\u0431\u0451\u0440\u0442\u043A\u0430 \u0431\u0435\u0437 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0432 PATH \u043D\u0435 \u0432\u0438\u0434\u043D\u0430 spawn'\u0443) \u2014 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 SWARM_CLAUDE_BIN \u043F\u0443\u0442\u0451\u043C \u043A \u043D\u0430\u0441\u0442\u043E\u044F\u0449\u0435\u043C\u0443 claude.exe."
6566
- );
6567
- }
6568
- return CLAUDE_FALLBACK;
7814
+ return `\u041C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u0442\u044F\u043D\u0435\u0442 \u0440\u043E\u0439: ${cost.pauses} \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043E\u043A \u0437\u0430 ${humanMinutes(windowMs)}, \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E ${humanMinutes(cost.lostMs)}`;
7815
+ }
7816
+ function buildReliefCostCandidates(cost, t, sustainMs, build) {
7817
+ if (!reliefAlertDue(cost, t)) return [];
7818
+ return [{ key: "relief_cost", minDurationMs: sustainMs, build }];
6569
7819
  }
6570
7820
 
7821
+ // src/launcher.ts
7822
+ import { spawn } from "child_process";
7823
+ import { closeSync as closeSync2, existsSync as existsSync8, mkdirSync as mkdirSync6, openSync as openSync2, readdirSync as readdirSync7 } from "fs";
7824
+ import { createRequire } from "module";
7825
+ import { homedir as homedir4 } from "os";
7826
+ import { dirname as dirname2, join as join12 } from "path";
7827
+ import { fileURLToPath } from "url";
7828
+
6571
7829
  // src/droneHome.ts
6572
- import { lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync4, rmSync, statSync as statSync3, symlinkSync } from "fs";
6573
- import { join as join7, relative, sep } from "path";
7830
+ import { lstatSync, mkdirSync as mkdirSync5, readdirSync as readdirSync6, rmSync, statSync as statSync6, symlinkSync as symlinkSync2 } from "fs";
7831
+ import { join as join11, relative, sep } from "path";
6574
7832
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
6575
7833
  function droneHomeDeny(env = process.env) {
6576
7834
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -6588,34 +7846,34 @@ function buildDroneHome(opts) {
6588
7846
  const { realHome, droneHome } = opts;
6589
7847
  const deny = opts.deny ?? droneHomeDeny();
6590
7848
  const platform = opts.platform ?? process.platform;
6591
- mkdirSync3(droneHome, { recursive: true, mode: 448 });
6592
- const wanted = new Set(mirroredEntries(readdirSync4(realHome), deny, platform));
7849
+ mkdirSync5(droneHome, { recursive: true, mode: 448 });
7850
+ const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
6593
7851
  const rel = relative(realHome, droneHome);
6594
7852
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
6595
- for (const existing of readdirSync4(droneHome)) {
7853
+ for (const existing of readdirSync6(droneHome)) {
6596
7854
  if (wanted.has(existing)) continue;
6597
- const p = join7(droneHome, existing);
7855
+ const p = join11(droneHome, existing);
6598
7856
  try {
6599
7857
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
6600
7858
  } catch {
6601
7859
  }
6602
7860
  }
6603
7861
  for (const name of wanted) {
6604
- const link = join7(droneHome, name);
7862
+ const link = join11(droneHome, name);
6605
7863
  try {
6606
7864
  lstatSync(link);
6607
7865
  continue;
6608
7866
  } catch {
6609
7867
  }
6610
- const target = join7(realHome, name);
7868
+ const target = join11(realHome, name);
6611
7869
  let isDir;
6612
7870
  try {
6613
- isDir = statSync3(target).isDirectory();
7871
+ isDir = statSync6(target).isDirectory();
6614
7872
  } catch {
6615
7873
  continue;
6616
7874
  }
6617
7875
  try {
6618
- symlinkSync(target, link, isDir ? "junction" : "file");
7876
+ symlinkSync2(target, link, isDir ? "junction" : "file");
6619
7877
  } catch (e) {
6620
7878
  if (e.code !== "EEXIST") throw e;
6621
7879
  }
@@ -6628,10 +7886,10 @@ var REAL_PATH_ENV_PINS = [
6628
7886
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
6629
7887
  ];
6630
7888
  function realPathEnvPins(realHome) {
6631
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join7(realHome, ...segments)]));
7889
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join11(realHome, ...segments)]));
6632
7890
  }
6633
- function droneDenyRules(realHome, swarmHome) {
6634
- const paths = [
7891
+ function dronePrivatePaths(realHome, swarmHome) {
7892
+ return [
6635
7893
  // НЕ `${swarmHome}/**`. Worktree дрона живёт ВНУТРИ swarmHome
6636
7894
  // (`<swarmHome>/worktrees.noindex/<repo>/<slug>`), и правило на весь каталог закрывало
6637
7895
  // дрону его собственный рабочий каталог: Read/Write/Edit отвечали «File is in a directory
@@ -6652,11 +7910,6 @@ function droneDenyRules(realHome, swarmHome) {
6652
7910
  `${realHome}/.claude/.credentials.json`,
6653
7911
  `${realHome}/.npmrc`
6654
7912
  ];
6655
- const abs = (p) => `//${p.replace(/^\/+/, "")}`;
6656
- return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
6657
- }
6658
- function droneSettingsJson(realHome, swarmHome) {
6659
- return JSON.stringify({ permissions: { deny: droneDenyRules(realHome, swarmHome) } });
6660
7913
  }
6661
7914
 
6662
7915
  // src/testForks.ts
@@ -6688,6 +7941,7 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
6688
7941
  }
6689
7942
 
6690
7943
  // src/launcher.ts
7944
+ init_executors();
6691
7945
  var requireFromHere = createRequire(import.meta.url);
6692
7946
  function resolveTsxCli() {
6693
7947
  try {
@@ -6698,29 +7952,12 @@ function resolveTsxCli() {
6698
7952
  );
6699
7953
  }
6700
7954
  }
6701
- var ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
6702
- function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
6703
- const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
6704
- const base3 = [
6705
- "--dangerously-skip-permissions",
6706
- ...ISOLATION_FLAGS,
6707
- "--mcp-config",
6708
- cfgPath,
6709
- ...settings ? ["--settings", settings] : [],
6710
- ...pluginFlags,
6711
- "--output-format",
6712
- "json",
6713
- "-p",
6714
- prompt
6715
- ];
6716
- const withModel = model ? ["--model", model, ...base3] : base3;
6717
- return continueSession ? ["--continue", ...withModel] : withModel;
6718
- }
7955
+ var MCP_SERVER_NAME = "overmind";
6719
7956
  function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
6720
7957
  const env = { ...base3 };
6721
7958
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
6722
7959
  if (isolate && !base3.TURBO_CACHE_DIR) {
6723
- env.TURBO_CACHE_DIR = join8(worktree, ".turbo", "cache");
7960
+ env.TURBO_CACHE_DIR = join12(worktree, ".turbo", "cache");
6724
7961
  }
6725
7962
  Object.assign(env, droneTestEnv(droneCap, base3));
6726
7963
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -6748,49 +7985,20 @@ var DAEMON_STATE_ENV = [
6748
7985
  "TASKLESS_TOKEN"
6749
7986
  ];
6750
7987
  function bundledRoleDir(kind) {
6751
- return join8(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
7988
+ return join12(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
6752
7989
  }
6753
7990
  function roleDir(worktree, kind) {
6754
- return join8(worktree, ".claude", "roles", kind);
7991
+ return join12(worktree, ".claude", "roles", kind);
6755
7992
  }
6756
7993
  function roleDirs(worktree, kind) {
6757
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync6(dir));
6758
- }
6759
- function transcriptDir(worktree, projectsDir) {
6760
- return join8(projectsDir, worktree.replace(/[/.]/g, "-"));
6761
- }
6762
- function sessionTranscriptExists(worktree, projectsDir = join8(homedir2(), ".claude", "projects")) {
6763
- try {
6764
- return readdirSync5(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
6765
- } catch {
6766
- return false;
6767
- }
6768
- }
6769
- function transcriptMtimeMs(worktree, projectsDir = join8(homedir2(), ".claude", "projects")) {
6770
- const dir = transcriptDir(worktree, projectsDir);
6771
- let files;
6772
- try {
6773
- files = readdirSync5(dir);
6774
- } catch {
6775
- return null;
6776
- }
6777
- let latest = null;
6778
- for (const f of files) {
6779
- if (!f.endsWith(".jsonl")) continue;
6780
- try {
6781
- const mtime2 = statSync4(join8(dir, f)).mtimeMs;
6782
- if (latest === null || mtime2 > latest) latest = mtime2;
6783
- } catch {
6784
- }
6785
- }
6786
- return latest;
7994
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync8(dir));
6787
7995
  }
6788
7996
  function nextRunLogPath(runLogsDir, runId) {
6789
- const dir = join8(runLogsDir, String(runId));
6790
- mkdirSync4(dir, { recursive: true });
7997
+ const dir = join12(runLogsDir, String(runId));
7998
+ mkdirSync6(dir, { recursive: true });
6791
7999
  let files = [];
6792
8000
  try {
6793
- files = readdirSync5(dir);
8001
+ files = readdirSync7(dir);
6794
8002
  } catch {
6795
8003
  }
6796
8004
  let next = 0;
@@ -6798,7 +8006,7 @@ function nextRunLogPath(runLogsDir, runId) {
6798
8006
  const m = /^(\d+)\.json/.exec(f);
6799
8007
  if (m) next = Math.max(next, Number(m[1]) + 1);
6800
8008
  }
6801
- return join8(dir, `${next}.json`);
8009
+ return join12(dir, `${next}.json`);
6802
8010
  }
6803
8011
  function openNextRunLog(runLogsDir, runId) {
6804
8012
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -6833,57 +8041,63 @@ function makeLauncher(cfg) {
6833
8041
  );
6834
8042
  }
6835
8043
  const channelEnv = cfg.channel && channelToken ? { SWARM_DRONE_CHANNEL_URL: cfg.channel.url, SWARM_DRONE_CHANNEL_TOKEN: channelToken } : { SWARM_DB: cfg.dbPath };
6836
- const mcpConfig = {
6837
- mcpServers: {
6838
- overmind: {
6839
- command: process.execPath,
6840
- args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["--conditions=development", resolveTsxCli(), cfg.overmindEntry, "--drone", "--run", String(run.id)],
6841
- // SWARM_DRONE_CHANNEL_URL/TOKEN (TASWM-295) ИЛИ SWARM_DB (откат) — см. channelEnv выше.
6842
- // Канал: дрон говорит только с демоном, файл базы ему не отдаётся вовсе. Токен подписан
6843
- // на СТАБИЛЬНЫЙ ключ прогона, поэтому переживает рестарт демона.
6844
- // SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — доверенный
6845
- // источник «чья это сессия» (read_task и отчёт о сдаче доверяют ему, а не строке,
6846
- // найденной по номеру прогона).
6847
- // SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID (TASWM-263) и SWARM_RUN_NAME
6848
- // (TASWM-265): роль, связи и имя прогона той же спавн-фиксацией. Нужны РЕЖИМУ ОТКАТА —
6849
- // ensureRunHydrated заводит ими стаб-строку в sqlite, если демон в client-режиме её там
6850
- // не заводил. На канале прогон отдаёт демон целиком, и стаб не нужен.
6851
- // SWARM_WORKSPACE_ID (TASWM-288): ВОРКСПЕЙС прогона — той же спавн-фиксацией: задачные
6852
- // вызовы (read_task, отчёт, тулы Героя) дрон делает НЕ через канал, а прямо в Taskless,
6853
- // и воркспейс им нужен верно сразу. Без него read_task/отчёт дрона
6854
- // ушли бы с глобальным пином демона — ровно то, что чинит эта задача. Пусто
6855
- // (прогон до фичи / sqlite-бэкенд) → ключ не кладём, дрон берёт глобальный пин.
6856
- // SWARM_HOME (TASWM-289): дрон едет с ТЕНЕВЫМ HOME, в котором `.taskless-swarm`
6857
- // нет вовсе, — а дрон-MCP из этого же дерева процессов читает oauth.json
6858
- // (connectTaskless в runDroneServer), пульс овермайнда и workspace.json. Наследовать
6859
- // ему нечего, поэтому реальный SWARM_HOME кладём явно. Дрон этот конфиг прочитать
6860
- // может, но нового ему это не даёт: его собственный worktree лежит ВНУТРИ
6861
- // SWARM_HOME, путь он и так знает (см. «чего этот модуль не делает» в droneHome.ts).
6862
- env: {
6863
- ...channelEnv,
6864
- ...cfg.droneIsolation ? { SWARM_HOME: cfg.droneIsolation.swarmHome } : {},
6865
- SWARM_TASK_SLUG: run.task_slug,
6866
- SWARM_RUN_KIND: run.kind,
6867
- SWARM_RUN_NAME: run.name,
6868
- ...run.workspace_id ? { SWARM_WORKSPACE_ID: run.workspace_id } : {},
6869
- ...run.reviews_run_id != null ? { SWARM_REVIEWS_RUN_ID: String(run.reviews_run_id) } : {},
6870
- ...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
6871
- }
6872
- }
8044
+ const mcp = {
8045
+ name: MCP_SERVER_NAME,
8046
+ command: process.execPath,
8047
+ args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["--conditions=development", resolveTsxCli(), cfg.overmindEntry, "--drone", "--run", String(run.id)],
8048
+ // SWARM_DRONE_CHANNEL_URL/TOKEN (TASWM-295) ИЛИ SWARM_DB (откат) — см. channelEnv выше.
8049
+ // Канал: дрон говорит только с демоном, файл базы ему не отдаётся вовсе. Токен подписан
8050
+ // на СТАБИЛЬНЫЙ ключ прогона, поэтому переживает рестарт демона.
8051
+ // SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — доверенный
8052
+ // источник «чья это сессия» (read_task и отчёт о сдаче доверяют ему, а не строке,
8053
+ // найденной по номеру прогона).
8054
+ // SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID (TASWM-263) и SWARM_RUN_NAME
8055
+ // (TASWM-265): роль, связи и имя прогона той же спавн-фиксацией. Нужны РЕЖИМУ ОТКАТА —
8056
+ // ensureRunHydrated заводит ими стаб-строку в sqlite, если демон в client-режиме её там
8057
+ // не заводил. На канале прогон отдаёт демон целиком, и стаб не нужен.
8058
+ // SWARM_WORKSPACE_ID (TASWM-288): ВОРКСПЕЙС прогона — той же спавн-фиксацией: задачные
8059
+ // вызовы (read_task, отчёт, тулы Героя) дрон делает НЕ через канал, а прямо в Taskless,
8060
+ // и воркспейс им нужен верно сразу. Без него read_task/отчёт дрона
8061
+ // ушли бы с глобальным пином демона — ровно то, что чинит эта задача. Пусто
8062
+ // (прогон до фичи / sqlite-бэкенд) → ключ не кладём, дрон берёт глобальный пин.
8063
+ // SWARM_HOME (TASWM-289): дрон едет с ТЕНЕВЫМ HOME, в котором `.taskless-swarm`
8064
+ // нет вовсе, — а дрон-MCP из этого же дерева процессов читает oauth.json
8065
+ // (connectTaskless в runDroneServer), пульс овермайнда и workspace.json. Наследовать
8066
+ // ему нечего, поэтому реальный SWARM_HOME кладём явно. Дрон этот конфиг прочитать
8067
+ // может, но нового ему это не даёт: его собственный worktree лежит ВНУТРИ
8068
+ // SWARM_HOME, путь он и так знает (см. «чего этот модуль не делает» в droneHome.ts).
8069
+ env: {
8070
+ ...channelEnv,
8071
+ ...cfg.droneIsolation ? { SWARM_HOME: cfg.droneIsolation.swarmHome } : {},
8072
+ SWARM_TASK_SLUG: run.task_slug,
8073
+ SWARM_RUN_KIND: run.kind,
8074
+ SWARM_RUN_NAME: run.name,
8075
+ ...run.workspace_id ? { SWARM_WORKSPACE_ID: run.workspace_id } : {},
8076
+ ...run.reviews_run_id != null ? { SWARM_REVIEWS_RUN_ID: String(run.reviews_run_id) } : {},
8077
+ ...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
6873
8078
  }
6874
8079
  };
6875
- const cfgPath = join8(worktree, ".swarm-mcp.json");
6876
- writeFileSync7(cfgPath, JSON.stringify(mcpConfig, null, 2));
6877
- const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
6878
- const pluginDirs = roleDirs(worktree, run.kind);
6879
- const realHome = cfg.droneIsolation?.realHome ?? homedir2();
8080
+ const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8081
+ const realHome = cfg.droneIsolation?.realHome ?? homedir4();
6880
8082
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
6881
- const settings = cfg.droneIsolation ? droneSettingsJson(realHome, cfg.droneIsolation.swarmHome) : void 0;
6882
- if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync6(join8(cfg.droneIsolation.swarmHome, "token"))) {
8083
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join12(cfg.droneIsolation.swarmHome, "token"))) {
6883
8084
  console.error(
6884
- `[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 ${join8(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.`
8085
+ `[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 ${join12(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.`
6885
8086
  );
6886
8087
  }
8088
+ const executor = cfg.executor ?? resolveExecutor();
8089
+ const plan = executor.plan({
8090
+ worktree,
8091
+ prompt,
8092
+ continueSession: !!continueSession,
8093
+ model,
8094
+ mcp,
8095
+ skillDirs: roleDirs(worktree, run.kind),
8096
+ // Закрытые дрону пути имеют смысл только при теневом HOME: без него дрон и так
8097
+ // сидит в HOME демона, и запирать половину дверей — самообман.
8098
+ denyPaths: cfg.droneIsolation ? dronePrivatePaths(realHome, cfg.droneIsolation.swarmHome) : [],
8099
+ realHome
8100
+ });
6887
8101
  let stdio = "ignore";
6888
8102
  let outFd;
6889
8103
  let errFd;
@@ -6893,15 +8107,16 @@ function makeLauncher(cfg) {
6893
8107
  errFd = fds.err ?? void 0;
6894
8108
  stdio = ["ignore", outFd, errFd ?? "ignore"];
6895
8109
  }
6896
- const child = spawn(cfg.claudeBin ?? resolveClaudeBin(), claudeArgs(cfgPath, prompt, continueSession, model, pluginDirs, settings), {
8110
+ const child = spawn(cfg.bin ?? plan.bin, plan.args, {
6897
8111
  cwd: worktree,
6898
8112
  detached: true,
6899
8113
  stdio,
6900
8114
  // Пер-worktree TURBO_CACHE_DIR (TASWM-232): дрон не отыгрывает кеш соседнего
6901
8115
  // worktree, проверки исполняются реально. Теневой HOME + чистка env от состояния
6902
8116
  // демона (TASWM-289). Доля тестовых форков от числа дронов на хосте (TASWM-246).
6903
- // Остальной env демона наследуется, как раньше.
6904
- env: droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null)
8117
+ // Остальной env демона наследуется, как раньше. Добавки адаптера — последними:
8118
+ // они про его собственное состояние, и перебивать их ройным нечем.
8119
+ env: { ...droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null), ...plan.env }
6905
8120
  });
6906
8121
  if (outFd !== void 0) closeSync2(outFd);
6907
8122
  if (errFd !== void 0) closeSync2(errFd);
@@ -6915,92 +8130,59 @@ function makeLauncher(cfg) {
6915
8130
  init_procCleanup();
6916
8131
 
6917
8132
  // src/cost.ts
6918
- import { readFileSync as readFileSync9, readdirSync as readdirSync6, renameSync as renameSync5 } from "fs";
6919
- import { join as join9 } from "path";
6920
-
6921
- // src/pricing.ts
6922
- var MODEL_PRICING = {
6923
- // Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
6924
- // Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
6925
- // ветвление по времени внутри расчёта стоимости.
6926
- "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
6927
- "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
6928
- // Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
6929
- "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
6930
- "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
6931
- };
6932
- var CACHE_READ_FACTOR = 0.1;
6933
- var CACHE_WRITE_5M_FACTOR = 1.25;
6934
- function costFromUsage(usage, model) {
6935
- const pricing = model ? MODEL_PRICING[model] : void 0;
6936
- if (!pricing) {
6937
- return { costUsd: 0, warning: `\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u0430\u0439\u0441\u0438\u043D\u0433\u0430: "${model ?? ""}" \u2014 cost \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D` };
6938
- }
6939
- const input = usage.input_tokens ?? 0;
6940
- const output = usage.output_tokens ?? 0;
6941
- const cacheWrite = usage.cache_creation_input_tokens ?? 0;
6942
- const cacheRead = usage.cache_read_input_tokens ?? 0;
6943
- const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
6944
- return { costUsd };
6945
- }
6946
-
6947
- // src/claudeResult.ts
6948
- function parseClaudeResultJson(raw) {
6949
- const trimmed = raw.trim();
6950
- if (!trimmed) return null;
6951
- try {
6952
- const parsed = JSON.parse(trimmed);
6953
- return typeof parsed === "object" && parsed !== null ? parsed : null;
6954
- } catch {
6955
- return null;
6956
- }
6957
- }
6958
- function launchCostUsd(result, model) {
6959
- if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
6960
- return { costUsd: result.total_cost_usd };
6961
- }
6962
- if (!result.usage) return { costUsd: 0 };
6963
- return costFromUsage(result.usage, model);
6964
- }
6965
-
6966
- // src/cost.ts
8133
+ init_executors();
8134
+ import { readFileSync as readFileSync10, readdirSync as readdirSync8, renameSync as renameSync7 } from "fs";
8135
+ import { join as join13 } from "path";
6967
8136
  var UNPROCESSED_LOG_RE = /^\d+\.json$/;
6968
8137
  function unprocessedLogFiles(dir) {
6969
8138
  try {
6970
- return readdirSync6(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
8139
+ return readdirSync8(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
6971
8140
  } catch {
6972
8141
  return [];
6973
8142
  }
6974
8143
  }
6975
8144
  function accountRunCost(deps, run) {
6976
- const dir = join9(deps.runLogsDir, String(run.id));
8145
+ const dir = join13(deps.runLogsDir, String(run.id));
6977
8146
  const files = unprocessedLogFiles(dir);
6978
8147
  if (files.length === 0) return;
6979
8148
  const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8149
+ const executor = deps.executor ?? resolveExecutor();
6980
8150
  let added = 0;
8151
+ let tokensIn = 0;
8152
+ let tokensOut = 0;
8153
+ let durationMs = 0;
6981
8154
  const processed = [];
6982
8155
  for (const f of files) {
6983
- const full = join9(dir, f);
8156
+ const full = join13(dir, f);
6984
8157
  let raw = null;
6985
8158
  try {
6986
- raw = readFileSync9(full, "utf8");
8159
+ raw = readFileSync10(full, "utf8");
6987
8160
  } catch {
6988
8161
  continue;
6989
8162
  }
6990
- const result = parseClaudeResultJson(raw);
8163
+ const result = executor.parseResult(raw, model);
6991
8164
  if (!result) {
6992
8165
  console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: \u043D\u0435 \u0440\u0430\u0441\u043F\u0430\u0440\u0441\u0438\u043B\u0441\u044F ${full} (\u043F\u0440\u043E\u0433\u043E\u043D \u0443\u043C\u0435\u0440 \u0434\u043E \u0437\u0430\u043F\u0438\u0441\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430?)`);
6993
8166
  } else {
6994
- const est = launchCostUsd(result, model);
6995
- if (est.warning) console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${est.warning}`);
6996
- added += est.costUsd;
8167
+ if (result.warning) console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${result.warning}`);
8168
+ added += result.costUsd;
8169
+ tokensIn += result.tokensIn ?? 0;
8170
+ tokensOut += result.tokensOut ?? 0;
8171
+ durationMs += result.durationMs ?? 0;
6997
8172
  }
6998
8173
  processed.push(full);
6999
8174
  }
7000
- if (added > 0) deps.db.updateRun(run.id, { cost_usd: run.cost_usd + added });
8175
+ if (added > 0 || tokensIn > 0 || tokensOut > 0 || durationMs > 0) {
8176
+ deps.db.updateRun(run.id, {
8177
+ cost_usd: run.cost_usd + added,
8178
+ tokens_in: run.tokens_in + tokensIn,
8179
+ tokens_out: run.tokens_out + tokensOut,
8180
+ duration_ms: run.duration_ms + durationMs
8181
+ });
8182
+ }
7001
8183
  for (const full of processed) {
7002
8184
  try {
7003
- renameSync5(full, `${full}.done`);
8185
+ renameSync7(full, `${full}.done`);
7004
8186
  } catch {
7005
8187
  }
7006
8188
  }
@@ -7010,14 +8192,15 @@ function makeCostAccountant(deps) {
7010
8192
  }
7011
8193
 
7012
8194
  // src/runOutcome.ts
7013
- import { readdirSync as readdirSync7, readFileSync as readFileSync10 } from "fs";
7014
- import { join as join10 } from "path";
8195
+ init_executors();
8196
+ import { readdirSync as readdirSync9, readFileSync as readFileSync11 } from "fs";
8197
+ import { join as join14 } from "path";
7015
8198
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
7016
- function classifyExit(runLogsDir, runId) {
7017
- const dir = join10(runLogsDir, String(runId));
8199
+ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
8200
+ const dir = join14(runLogsDir, String(runId));
7018
8201
  let files;
7019
8202
  try {
7020
- files = readdirSync7(dir);
8203
+ files = readdirSync9(dir);
7021
8204
  } catch {
7022
8205
  return "unknown";
7023
8206
  }
@@ -7031,22 +8214,24 @@ function classifyExit(runLogsDir, runId) {
7031
8214
  if (!latest) return "unknown";
7032
8215
  let raw;
7033
8216
  try {
7034
- raw = readFileSync10(join10(dir, latest.name), "utf8");
8217
+ raw = readFileSync11(join14(dir, latest.name), "utf8");
7035
8218
  } catch {
7036
8219
  return "unknown";
7037
8220
  }
7038
- return parseClaudeResultJson(raw) ? "graceful" : "crash";
8221
+ return executor.parseResult(raw, null) ? "graceful" : "crash";
7039
8222
  }
7040
- function makeExitClassifier(runLogsDir) {
7041
- return (run) => classifyExit(runLogsDir, run.id);
8223
+ function makeExitClassifier(runLogsDir, executor) {
8224
+ const exec = executor ?? resolveExecutor();
8225
+ return (run) => classifyExit(runLogsDir, run.id, exec);
7042
8226
  }
7043
8227
 
7044
8228
  // src/orchestrator.ts
8229
+ init_src();
7045
8230
  init_git();
7046
8231
 
7047
8232
  // src/deps.ts
7048
- import { existsSync as existsSync8, readdirSync as readdirSync9, readFileSync as readFileSync11 } from "fs";
7049
- import { join as join12 } from "path";
8233
+ import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync12 } from "fs";
8234
+ import { join as join16 } from "path";
7050
8235
  import { execa } from "execa";
7051
8236
  var PREPARE_TIMEOUT_MS = 15 * 6e4;
7052
8237
  var MAX_PLAN_COMMANDS = 12;
@@ -7062,21 +8247,21 @@ async function runCommand(cmd, worktree) {
7062
8247
  }
7063
8248
  function safeReadText(p) {
7064
8249
  try {
7065
- return readFileSync11(p, "utf8");
8250
+ return readFileSync12(p, "utf8");
7066
8251
  } catch {
7067
8252
  return null;
7068
8253
  }
7069
8254
  }
7070
8255
  function safeListDirs(p) {
7071
8256
  try {
7072
- return readdirSync9(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
8257
+ return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
7073
8258
  } catch {
7074
8259
  return [];
7075
8260
  }
7076
8261
  }
7077
8262
  function safeListFiles(p) {
7078
8263
  try {
7079
- return readdirSync9(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
8264
+ return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
7080
8265
  } catch {
7081
8266
  return [];
7082
8267
  }
@@ -7102,21 +8287,21 @@ function overridePlan(override) {
7102
8287
  };
7103
8288
  }
7104
8289
  function detectPackageManager(worktree, io = {}) {
7105
- const exists = io.exists ?? existsSync8;
8290
+ const exists = io.exists ?? existsSync10;
7106
8291
  const readText = io.readText ?? safeReadText;
7107
- const hasPkgJson = exists(join12(worktree, "package.json"));
8292
+ const hasPkgJson = exists(join16(worktree, "package.json"));
7108
8293
  if (!hasPkgJson) {
7109
- if (exists(join12(worktree, "deno.json")) || exists(join12(worktree, "deno.jsonc")) || exists(join12(worktree, "deno.lock"))) return "deno";
8294
+ if (exists(join16(worktree, "deno.json")) || exists(join16(worktree, "deno.jsonc")) || exists(join16(worktree, "deno.lock"))) return "deno";
7110
8295
  return null;
7111
8296
  }
7112
- const pkg = readJson(readText, join12(worktree, "package.json"));
8297
+ const pkg = readJson(readText, join16(worktree, "package.json"));
7113
8298
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
7114
8299
  if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
7115
- if (exists(join12(worktree, "bun.lock")) || exists(join12(worktree, "bun.lockb"))) return "bun";
7116
- if (exists(join12(worktree, "pnpm-lock.yaml"))) return "pnpm";
7117
- if (exists(join12(worktree, "yarn.lock"))) return "yarn";
7118
- if (exists(join12(worktree, "package-lock.json"))) return "npm";
7119
- if (exists(join12(worktree, "deno.lock"))) return "deno";
8300
+ if (exists(join16(worktree, "bun.lock")) || exists(join16(worktree, "bun.lockb"))) return "bun";
8301
+ if (exists(join16(worktree, "pnpm-lock.yaml"))) return "pnpm";
8302
+ if (exists(join16(worktree, "yarn.lock"))) return "yarn";
8303
+ if (exists(join16(worktree, "package-lock.json"))) return "npm";
8304
+ if (exists(join16(worktree, "deno.lock"))) return "deno";
7120
8305
  return "npm";
7121
8306
  }
7122
8307
  function jsInstallCommand(pm) {
@@ -7169,7 +8354,7 @@ var ECOSYSTEM_MARKERS = [
7169
8354
  // конфигурация — дело сборки, не подготовки
7170
8355
  ];
7171
8356
  function detectPlan(worktree, io) {
7172
- const exists = io.exists ?? existsSync8;
8357
+ const exists = io.exists ?? existsSync10;
7173
8358
  const listFiles = io.listFiles ?? safeListFiles;
7174
8359
  const pm = detectPackageManager(worktree, io);
7175
8360
  if (pm) {
@@ -7177,7 +8362,7 @@ function detectPlan(worktree, io) {
7177
8362
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
7178
8363
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
7179
8364
  // заново, но он дешёвый: тянет из того же общего кэша.
7180
- { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join12(worktree, "node_modules"), source: "detect" }
8365
+ { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join16(worktree, "node_modules"), source: "detect" }
7181
8366
  ];
7182
8367
  for (const target of workspaceBuildTargets(worktree, io)) {
7183
8368
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
@@ -7185,12 +8370,12 @@ function detectPlan(worktree, io) {
7185
8370
  return { source: "detect", ecosystem: `node/${pm}`, commands };
7186
8371
  }
7187
8372
  for (const marker of ECOSYSTEM_MARKERS) {
7188
- const hit = marker.files.some((f) => exists(join12(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
8373
+ const hit = marker.files.some((f) => exists(join16(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
7189
8374
  if (!hit) continue;
7190
8375
  let ecosystem = marker.ecosystem;
7191
8376
  let command = marker.command;
7192
8377
  for (const refine of marker.refine ?? []) {
7193
- if (!exists(join12(worktree, refine.file))) continue;
8378
+ if (!exists(join16(worktree, refine.file))) continue;
7194
8379
  ecosystem = refine.ecosystem;
7195
8380
  command = refine.command;
7196
8381
  break;
@@ -7247,12 +8432,12 @@ var CI_INSTALL_PATTERNS = [
7247
8432
  function ciWorkflowPlan(worktree, io) {
7248
8433
  const readText = io.readText ?? safeReadText;
7249
8434
  const listFiles = io.listFiles ?? safeListFiles;
7250
- const dir = join12(worktree, ".github", "workflows");
8435
+ const dir = join16(worktree, ".github", "workflows");
7251
8436
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
7252
8437
  const seen = /* @__PURE__ */ new Set();
7253
8438
  const commands = [];
7254
8439
  for (const name of files.sort()) {
7255
- const yaml = readText(join12(dir, name));
8440
+ const yaml = readText(join16(dir, name));
7256
8441
  if (yaml === null) continue;
7257
8442
  for (const raw of workflowRunLines(yaml)) {
7258
8443
  const line = raw.replace(/\s+/g, " ").trim();
@@ -7267,7 +8452,7 @@ function ciWorkflowPlan(worktree, io) {
7267
8452
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
7268
8453
  }
7269
8454
  function workspaceGlobs(worktree, readText) {
7270
- const yaml = readText(join12(worktree, "pnpm-workspace.yaml"));
8455
+ const yaml = readText(join16(worktree, "pnpm-workspace.yaml"));
7271
8456
  if (yaml !== null) {
7272
8457
  const globs = [];
7273
8458
  let inPackages = false;
@@ -7287,7 +8472,7 @@ function workspaceGlobs(worktree, readText) {
7287
8472
  }
7288
8473
  if (globs.length) return globs;
7289
8474
  }
7290
- const pkg = readJson(readText, join12(worktree, "package.json"));
8475
+ const pkg = readJson(readText, join16(worktree, "package.json"));
7291
8476
  const ws = pkg?.workspaces;
7292
8477
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
7293
8478
  return list.filter((g) => typeof g === "string");
@@ -7298,14 +8483,14 @@ function workspaceDirs(worktree, globs, io) {
7298
8483
  if (glob.startsWith("!")) continue;
7299
8484
  const star = glob.indexOf("*");
7300
8485
  if (star === -1) {
7301
- const dir = join12(worktree, glob);
7302
- if (io.exists(join12(dir, "package.json"))) dirs.add(dir);
8486
+ const dir = join16(worktree, glob);
8487
+ if (io.exists(join16(dir, "package.json"))) dirs.add(dir);
7303
8488
  continue;
7304
8489
  }
7305
- const base3 = join12(worktree, glob.slice(0, star).replace(/\/$/, ""));
8490
+ const base3 = join16(worktree, glob.slice(0, star).replace(/\/$/, ""));
7306
8491
  for (const name of io.listDirs(base3)) {
7307
- const dir = join12(base3, name);
7308
- if (io.exists(join12(dir, "package.json"))) dirs.add(dir);
8492
+ const dir = join16(base3, name);
8493
+ if (io.exists(join16(dir, "package.json"))) dirs.add(dir);
7309
8494
  }
7310
8495
  }
7311
8496
  return [...dirs];
@@ -7326,18 +8511,18 @@ function entryPaths(pkg) {
7326
8511
  return out;
7327
8512
  }
7328
8513
  function workspaceBuildTargets(worktree, io = {}) {
7329
- const exists = io.exists ?? existsSync8;
8514
+ const exists = io.exists ?? existsSync10;
7330
8515
  const readText = io.readText ?? safeReadText;
7331
8516
  const listDirs = io.listDirs ?? safeListDirs;
7332
8517
  const globs = workspaceGlobs(worktree, readText);
7333
8518
  if (!globs.length) return [];
7334
8519
  const pkgs = [];
7335
8520
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
7336
- const pkg = readJson(readText, join12(dir, "package.json"));
8521
+ const pkg = readJson(readText, join16(dir, "package.json"));
7337
8522
  if (!pkg || typeof pkg.name !== "string") continue;
7338
8523
  const scripts = pkg.scripts ?? {};
7339
8524
  const entries = entryPaths(pkg);
7340
- const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join12(dir, e)));
8525
+ const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join16(dir, e)));
7341
8526
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
7342
8527
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
7343
8528
  }
@@ -7365,7 +8550,7 @@ function planPrepare(worktree, io = {}) {
7365
8550
  return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
7366
8551
  }
7367
8552
  async function ensureWorktreeDeps(worktree, io = {}) {
7368
- const exists = io.exists ?? existsSync8;
8553
+ const exists = io.exists ?? existsSync10;
7369
8554
  const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
7370
8555
  const approve = io.approve ?? (() => true);
7371
8556
  const plan = planPrepare(worktree, io);
@@ -7456,7 +8641,9 @@ var BaseBranchMissingError = class extends Error {
7456
8641
  branch;
7457
8642
  repo;
7458
8643
  };
7459
- var AreaBusyError = class extends Error {
8644
+ var SpawnDeferredError = class extends Error {
8645
+ };
8646
+ var AreaBusyError = class extends SpawnDeferredError {
7460
8647
  constructor(area) {
7461
8648
  super(`\u0437\u043E\u043D\u0430 ${area} \u0437\u0430\u043D\u044F\u0442\u0430 \u0434\u0440\u0443\u0433\u0438\u043C \u0434\u0440\u043E\u043D\u043E\u043C \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0441\u0442\u0430\u0451\u0442\u0441\u044F \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438`);
7462
8649
  this.area = area;
@@ -7464,10 +8651,26 @@ var AreaBusyError = class extends Error {
7464
8651
  }
7465
8652
  area;
7466
8653
  };
8654
+ var FileOverlapError = class extends SpawnDeferredError {
8655
+ constructor(message, blockedByRunId) {
8656
+ super(message);
8657
+ this.blockedByRunId = blockedByRunId;
8658
+ this.name = "FileOverlapError";
8659
+ }
8660
+ blockedByRunId;
8661
+ };
8662
+ function healTaskSlug(db, run, task) {
8663
+ const slug = task?.slug;
8664
+ if (!slug || slug === run.task_slug || !isTaskSlug(slug) || isTaskSlug(run.task_slug)) return run;
8665
+ db.addEvent(run.id, "step", `\u0441\u043B\u0430\u0433 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D: ${run.task_slug} \u2192 ${slug} (\u043F\u0440\u043E\u0434 \u043E\u0442\u0434\u0430\u043B \u043F\u0440\u043E\u0433\u043E\u043D \u0431\u0435\u0437 \u0441\u043B\u0430\u0433\u0430 \u2014 TASWM-329)`);
8666
+ return db.updateRun(run.id, { task_slug: slug }) ?? run;
8667
+ }
7467
8668
  async function spawnDrone(deps, run) {
7468
8669
  const repo = normalizeRepo(run.repo || deps.repo || "");
7469
8670
  if (!repo) throw new Error(`\u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} (${run.task_slug}) \u043D\u0435\u0442 repo \u0438 \u043D\u0435\u0442 \u0434\u0435\u0444\u043E\u043B\u0442\u0430 SWARM_REPO`);
7470
- if (run.area && deps.db.runningAreas(repo).includes(run.area)) {
8671
+ const conflict = deps.conflicts ? await deps.conflicts(run, repo) : { kind: "unknown" };
8672
+ if (conflict.kind === "wait") throw new FileOverlapError(conflict.message, conflict.blockedBy.id);
8673
+ if (conflict.kind === "unknown" && run.area && deps.db.runningAreas(repo).includes(run.area)) {
7471
8674
  throw new AreaBusyError(run.area);
7472
8675
  }
7473
8676
  const branch = run.task_slug;
@@ -7518,13 +8721,13 @@ function isGithubPrUrl(url) {
7518
8721
  return GH_PR_RE.test(url.trim());
7519
8722
  }
7520
8723
  function softStatus() {
7521
- return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [] };
8724
+ return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [], headBranch: null };
7522
8725
  }
7523
8726
  async function prStatus(prUrl, run = realRunner) {
7524
8727
  if (!isGithubPrUrl(prUrl)) return softStatus();
7525
8728
  let out;
7526
8729
  try {
7527
- out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files"]);
8730
+ out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files,headRefName"]);
7528
8731
  } catch {
7529
8732
  return softStatus();
7530
8733
  }
@@ -7545,9 +8748,20 @@ async function prStatus(prUrl, run = realRunner) {
7545
8748
  // не смержен, открыт и GitHub уверенно говорит CONFLICTING
7546
8749
  conflicting: !merged && state === "OPEN" && mergeable === "CONFLICTING",
7547
8750
  isDraft: j.isDraft === true,
7548
- files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : []
8751
+ files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : [],
8752
+ headBranch: typeof j.headRefName === "string" && j.headRefName ? j.headRefName : null
7549
8753
  };
7550
8754
  }
8755
+ async function prHeadBranch(prUrl, run = realRunner) {
8756
+ if (!isGithubPrUrl(prUrl)) return null;
8757
+ try {
8758
+ const out = await run(["pr", "view", prUrl, "--json", "headRefName"]);
8759
+ const j = JSON.parse(out);
8760
+ return typeof j.headRefName === "string" && j.headRefName ? j.headRefName : null;
8761
+ } catch {
8762
+ return null;
8763
+ }
8764
+ }
7551
8765
  async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
7552
8766
  if (!branch || !repoDir) return [];
7553
8767
  try {
@@ -7755,6 +8969,13 @@ function reviewTransitionPatch(run, prUrl) {
7755
8969
  }
7756
8970
  return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
7757
8971
  }
8972
+ function branchFromPrPatch(currentBranch, headBranch) {
8973
+ if (!headBranch || headBranch === currentBranch) return null;
8974
+ return {
8975
+ patch: { branch: headBranch },
8976
+ event: currentBranch ? `\u0432\u0435\u0442\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0432\u0437\u044F\u0442\u0430 \u0443 PR: ${currentBranch} \u2192 ${headBranch} (\u043F\u043E\u043B\u0435 \u0431\u044B\u043B\u043E \u0441\u043E\u0431\u0440\u0430\u043D\u043E \u0438\u0437 \u0441\u043B\u0430\u0433\u0430 \u2014 TASWM-356)` : `\u0432\u0435\u0442\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0432\u0437\u044F\u0442\u0430 \u0443 PR: ${headBranch} (TASWM-356)`
8977
+ };
8978
+ }
7758
8979
  function applyReviewTransition(db, run, prUrl) {
7759
8980
  const { patch, ...t } = reviewTransitionPatch(run, prUrl);
7760
8981
  db.updateRun(run.id, patch);
@@ -7763,14 +8984,14 @@ function applyReviewTransition(db, run, prUrl) {
7763
8984
 
7764
8985
  // src/droneServer.ts
7765
8986
  init_src();
7766
- function freezeInstruction(slug, reason = "\u0435\u0433\u043E \u043F\u0443\u043B\u044C\u0441 (heartbeat) \u0443\u0441\u0442\u0430\u0440\u0435\u043B") {
8987
+ function freezeInstruction(branch, reason = "\u0435\u0433\u043E \u043F\u0443\u043B\u044C\u0441 (heartbeat) \u0443\u0441\u0442\u0430\u0440\u0435\u043B") {
7767
8988
  return [
7768
8989
  `\u26D4 OVERMIND \u041D\u0415\u0414\u041E\u0421\u0422\u0423\u041F\u0415\u041D \u2014 ${reason}. \u041D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0430\u0434 \u0442\u043E\u0431\u043E\u0439 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0442: \u043D\u0435\u043A\u043E\u043C\u0443`,
7769
8990
  "\u043F\u0440\u0438\u043D\u044F\u0442\u044C \u0442\u0432\u043E\u0439 \u0432\u043E\u043F\u0440\u043E\u0441, \u0437\u0430\u043C\u0435\u0442\u0438\u0442\u044C \u0441\u043C\u0435\u0440\u0442\u044C, \u0441\u043C\u0435\u0440\u0436\u0438\u0442\u044C PR \u0438\u043B\u0438 \u0440\u0430\u0437\u0440\u0443\u043B\u0438\u0442\u044C \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442. \u0420\u0430\u0431\u043E\u0442\u0430\u0442\u044C \u0432\u0441\u043B\u0435\u043F\u0443\u044E \u043D\u0435\u043B\u044C\u0437\u044F.",
7770
8991
  "\u0417\u0410\u041C\u0420\u0418 \u0438 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0441\u044C, \u0447\u0442\u043E\u0431\u044B \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u043F\u0440\u043E\u043F\u0430\u043B\u0430:",
7771
8992
  "1. \u0417\u0430\u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0412\u0421\u042E \u0442\u0435\u043A\u0443\u0449\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043A\u0430\u043A \u0435\u0441\u0442\u044C: `git add -A && git commit` (\u0442\u0440\u0435\u0439\u043B\u0435\u0440 \u043F\u0440\u043E \u0434\u0440\u043E\u043D\u0430 \u2014 \u043A\u0430\u043A \u043E\u0431\u044B\u0447\u043D\u043E).",
7772
8993
  " \u041D\u043E\u0432\u044B\u0439 \u043A\u043E\u0434 \u041D\u0415 \u043F\u0438\u0448\u0438, \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u0430\u043B\u044C\u0448\u0435 \u041D\u0415 \u0434\u0435\u043B\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u043E\u0445\u0440\u0430\u043D\u0438 \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043D\u043E\u0435, \u0431\u0435\u0437 \u043C\u0443\u0441\u043E\u0440\u043D\u044B\u0445 \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432.",
7773
- `2. \u0417\u0430\u043F\u0443\u0448\u044C \u0432\u0435\u0442\u043A\u0443: \`git push -u origin ${slug}\` \u2014 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0440\u0451\u0442 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u043C.`,
8994
+ `2. \u0417\u0430\u043F\u0443\u0448\u044C \u0432\u0435\u0442\u043A\u0443: \`git push -u origin ${branch}\` \u2014 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0440\u0451\u0442 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u043C.`,
7774
8995
  "3. \u0417\u0430\u0432\u0435\u0440\u0448\u0438 \u0440\u0430\u0431\u043E\u0442\u0443 \u2014 \u0431\u043E\u043B\u044C\u0448\u0435 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u0432 \u041D\u0415 \u0437\u043E\u0432\u0438 (\u043D\u0438 report_step, \u043D\u0438 ask, \u043D\u0438 done). \u041F\u0440\u043E\u0441\u0442\u043E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438 \u0441\u0435\u0441\u0441\u0438\u044E.",
7775
8996
  "\u041F\u0440\u043E\u0433\u043E\u043D \u043F\u043E\u043C\u0435\u0447\u0435\u043D \xABpaused\xBB. \u041A\u043E\u0433\u0434\u0430 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F \u043D\u043E\u0432\u044B\u0439 overmind, \u043E\u043D \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442 \u0442\u0435\u0431\u044F \u0441 \u044D\u0442\u043E\u0433\u043E \u043C\u0435\u0441\u0442\u0430 (--continue) \u2014",
7776
8997
  "\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0448\u044C \u043E\u0442\u0442\u0443\u0434\u0430. \u041D\u0435 \u043F\u0435\u0440\u0435\u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0439 \u0438 \u043D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0437\u0430\u0434\u0430\u0447\u0443 \u0441\u0435\u0439\u0447\u0430\u0441."
@@ -7803,10 +9024,12 @@ async function buildDroneServer(store, opts = {}) {
7803
9024
  const slugForInstruction = (fallback) => opts.expectedTaskSlug ?? fallback ?? "<slug>";
7804
9025
  const freeze = async (withdraw = false) => {
7805
9026
  let slug;
9027
+ let branch;
7806
9028
  try {
7807
9029
  if (withdraw) await store.withdrawMyQuestion("overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u0437\u0430\u043C\u0435\u0440, \u0432\u043E\u043F\u0440\u043E\u0441 \u0441\u043D\u044F\u0442");
7808
9030
  const run = await store.myRun();
7809
9031
  slug = run?.task_slug;
9032
+ branch = !opts.expectedTaskSlug || opts.expectedTaskSlug === run?.task_slug ? run?.branch : null;
7810
9033
  if (run && run.status !== "paused") {
7811
9034
  await store.updateRun(null, { status: "paused" });
7812
9035
  await store.addEvent(null, "paused", "overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u0437\u0430\u043C\u0435\u0440, \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 origin, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430");
@@ -7814,7 +9037,7 @@ async function buildDroneServer(store, opts = {}) {
7814
9037
  } catch (e) {
7815
9038
  console.error("[overmind-drone] \u043F\u043E\u043C\u0435\u0442\u0438\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D paused \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C (\u043D\u0430\u0434\u0437\u043E\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D):", e);
7816
9039
  }
7817
- return { content: [{ type: "text", text: freezeInstruction(slugForInstruction(slug), opts.deadReason) }] };
9040
+ return { content: [{ type: "text", text: freezeInstruction(branch || slugForInstruction(slug), opts.deadReason) }] };
7818
9041
  };
7819
9042
  const guard = (fn) => async (a) => {
7820
9043
  try {
@@ -7838,7 +9061,7 @@ async function buildDroneServer(store, opts = {}) {
7838
9061
  );
7839
9062
  server.tool(
7840
9063
  "read_task",
7841
- "\u0421\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0442\u0432\u043E\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u041F\u0420\u042F\u041C\u041E \u0421\u0415\u0419\u0427\u0410\u0421: \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 + \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (\u0430\u0432\u0442\u043E\u0440+\u0432\u0440\u0435\u043C\u044F+\u0442\u0435\u043A\u0441\u0442) + \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0445 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432 (\u0441\u043F\u0435\u043A\u0438 \u043E\u0431\u044B\u0447\u043D\u043E \u043B\u0435\u0436\u0430\u0442 \u0438\u043C\u0435\u043D\u043D\u043E \u0442\u0430\u043C). \u0411\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432. \u0417\u043E\u0432\u0438 \u043F\u0435\u0440\u0435\u0434 done() \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0433 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0443\u0436\u0435 \u043F\u043E\u0441\u043B\u0435 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430, \u0430 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0443\u0432\u0438\u0434\u0438\u0442 (\u043E\u043D \u0441\u043E\u0431\u0440\u0430\u043D \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435).",
9064
+ "\u0421\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0442\u0432\u043E\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u041F\u0420\u042F\u041C\u041E \u0421\u0415\u0419\u0427\u0410\u0421: \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 + \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 + \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (\u0430\u0432\u0442\u043E\u0440+\u0432\u0440\u0435\u043C\u044F+\u0442\u0435\u043A\u0441\u0442) + \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0445 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432 (\u0441\u043F\u0435\u043A\u0438 \u043E\u0431\u044B\u0447\u043D\u043E \u043B\u0435\u0436\u0430\u0442 \u0438\u043C\u0435\u043D\u043D\u043E \u0442\u0430\u043C) + \u0447\u0435\u043C \u043A\u043E\u043D\u0447\u0438\u043B\u0438\u0441\u044C \u0441\u043E\u0441\u0435\u0434\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0442\u043E\u0439 \u0436\u0435 \u0437\u043E\u043D\u0435 \u043A\u043E\u0434\u0430 \u0438 \u043A\u0442\u043E \u0438\u0437 \u0440\u043E\u044F \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0440\u044F\u0434\u043E\u043C. \u0411\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432. \u0417\u043E\u0432\u0438 \u043F\u0435\u0440\u0435\u0434 done() \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0433 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0443\u0436\u0435 \u043F\u043E\u0441\u043B\u0435 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430, \u0430 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0443\u0432\u0438\u0434\u0438\u0442 (\u043E\u043D \u0441\u043E\u0431\u0440\u0430\u043D \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435).",
7842
9065
  {},
7843
9066
  async () => {
7844
9067
  if (!opts.taskless) {
@@ -8051,9 +9274,12 @@ ${report}${shots.warning}`,
8051
9274
  await store.addEvent(null, "step", `\u043E\u0442\u0447\u0451\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 ${taskSlug} \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${e instanceof Error ? e.message : String(e)}`);
8052
9275
  }
8053
9276
  };
9277
+ const headBranch = opts.prHeadBranch ? await opts.prHeadBranch(pr_url).catch(() => null) : null;
9278
+ const branchFix = branchFromPrPatch(run?.branch, headBranch);
9279
+ if (branchFix) await store.addEvent(null, "step", branchFix.event);
8054
9280
  if (run) {
8055
9281
  const t = reviewTransitionPatch(run, pr_url);
8056
- await store.updateRun(null, t.patch);
9282
+ await store.updateRun(null, { ...t.patch, ...branchFix?.patch ?? {} });
8057
9283
  await store.addEvent(null, "pr_opened", pr_url);
8058
9284
  await postReport(reportSlug(run.task_slug), t.cycle ?? run.review_cycle ?? 0);
8059
9285
  if (t.isFixIteration) {
@@ -8066,7 +9292,7 @@ ${report}${shots.warning}`,
8066
9292
  }
8067
9293
  return { content: [{ type: "text", text: `\u043F\u0440\u0438\u043D\u044F\u0442\u043E, \u0443\u0448\u043B\u043E \u043D\u0430 \u0440\u0435\u0432\u044C\u044E${shotsNote(shots)}` }] };
8068
9294
  }
8069
- await store.updateRun(null, { status: "review", pr_url });
9295
+ await store.updateRun(null, { status: "review", pr_url, ...branchFix?.patch ?? {} });
8070
9296
  await store.addEvent(null, "pr_opened", pr_url);
8071
9297
  const fallbackSlug = reportSlug(void 0);
8072
9298
  if (fallbackSlug) await postReport(fallbackSlug, void 0);
@@ -8231,7 +9457,7 @@ async function runDroneServer(runId) {
8231
9457
  );
8232
9458
  const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
8233
9459
  const expectedKind = toRunKind(process.env.SWARM_RUN_KIND);
8234
- const common = { taskless, runKind: expectedKind, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
9460
+ const common = { taskless, runKind: expectedKind, prHeadBranch, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
8235
9461
  const channelUrl = process.env.SWARM_DRONE_CHANNEL_URL;
8236
9462
  const channelToken = process.env.SWARM_DRONE_CHANNEL_TOKEN;
8237
9463
  if (channelUrl && channelToken) {
@@ -8324,12 +9550,15 @@ var AnomalyTracker = class {
8324
9550
  for (const [k, v] of Object.entries(restored.since)) this.since.set(k, v);
8325
9551
  for (const [k, v] of Object.entries(restored.lastFiredAt)) this.lastFiredAt.set(k, v);
8326
9552
  for (const [k, v] of Object.entries(restored.fires)) this.fires.set(k, v);
9553
+ for (const k of restored.resolved ?? []) this.resolved.add(k);
8327
9554
  }
8328
9555
  }
8329
9556
  renotifyCooldownMs;
8330
9557
  since = /* @__PURE__ */ new Map();
8331
9558
  lastFiredAt = /* @__PURE__ */ new Map();
8332
9559
  fires = /* @__PURE__ */ new Map();
9560
+ /** TAS-485: очередь «уже звонили, а условие ушло» — к гашению в инбоксе. */
9561
+ resolved = /* @__PURE__ */ new Set();
8333
9562
  maxRepeats;
8334
9563
  io;
8335
9564
  /** Кандидаты, готовые к сигналу (отлежали minDurationMs, не в кулдауне) — НИЧЕГО не коммитит. */
@@ -8350,6 +9579,7 @@ var AnomalyTracker = class {
8350
9579
  for (const map of [this.since, this.lastFiredAt, this.fires]) {
8351
9580
  for (const k of [...map.keys()]) {
8352
9581
  if (!seen.has(k)) {
9582
+ if (map === this.lastFiredAt) this.resolved.add(k);
8353
9583
  map.delete(k);
8354
9584
  pruned = true;
8355
9585
  }
@@ -8362,8 +9592,25 @@ var AnomalyTracker = class {
8362
9592
  commit(key, now) {
8363
9593
  this.lastFiredAt.set(key, now);
8364
9594
  this.fires.set(key, (this.fires.get(key) ?? 0) + 1);
9595
+ this.resolved.delete(key);
8365
9596
  this.persist();
8366
9597
  }
9598
+ /**
9599
+ * TAS-485: ключи, чьё условие ушло ПОСЛЕ того, как о нём звонили, — их записи
9600
+ * в инбоксе пора погасить. Список только ЧИТАЕТСЯ: вычёркивает доставленное
9601
+ * вызывающий (`forgetResolved`) и только после успеха — та же дисциплина, что
9602
+ * у eligible/commit. Проглоти мы сбой доставки как успех, тревога о том, чего
9603
+ * нет, осталась бы висеть навсегда и молча.
9604
+ */
9605
+ pendingResolved() {
9606
+ return [...this.resolved];
9607
+ }
9608
+ /** Гашение по этим ключам доставлено — забываем их. */
9609
+ forgetResolved(keys) {
9610
+ let changed = false;
9611
+ for (const k of keys) changed = this.resolved.delete(k) || changed;
9612
+ if (changed) this.persist();
9613
+ }
8367
9614
  /**
8368
9615
  * Следующий сигнал по этому ключу — ПОСЛЕДНИЙ разрешённый в эпизоде: вызывающий меняет
8369
9616
  * формулировку (LAST_REMINDER_SUFFIX), чтобы затихание не выглядело как «рой забыл».
@@ -8379,7 +9626,8 @@ var AnomalyTracker = class {
8379
9626
  this.io.save({
8380
9627
  since: Object.fromEntries(this.since),
8381
9628
  lastFiredAt: Object.fromEntries(this.lastFiredAt),
8382
- fires: Object.fromEntries(this.fires)
9629
+ fires: Object.fromEntries(this.fires),
9630
+ resolved: [...this.resolved]
8383
9631
  });
8384
9632
  } catch (e) {
8385
9633
  console.error("[anomalies] \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u043D\u0430 \u0434\u0438\u0441\u043A:", e);
@@ -8433,8 +9681,9 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8433
9681
  const changedAt = sqliteMs(run.updated_at);
8434
9682
  return changedAt >= q.from && (q.to == null || changedAt <= q.to);
8435
9683
  };
9684
+ const closedBeforeStart = (run) => opts.daemonStartedAt != null && sqliteMs(run.updated_at) < opts.daemonStartedAt;
8436
9685
  const recentFailed = allRuns.filter(
8437
- (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r)
9686
+ (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r)
8438
9687
  );
8439
9688
  if (recentFailed.length >= thresholds.failedSpikeCount) {
8440
9689
  candidates.push({
@@ -8471,6 +9720,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8471
9720
  if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
8472
9721
  if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
8473
9722
  if (killedByOperator(run)) continue;
9723
+ if (closedBeforeStart(run)) continue;
8474
9724
  const repeat = isRepeatDown(run);
8475
9725
  candidates.push({
8476
9726
  key: `run_stalled:${run.id}`,
@@ -8491,6 +9741,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8491
9741
  if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
8492
9742
  if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
8493
9743
  if (killedByOperator(run)) continue;
9744
+ if (closedBeforeStart(run)) continue;
8494
9745
  const repeat = isRepeatDown(run);
8495
9746
  candidates.push({
8496
9747
  key: `run_failed:${run.id}`,
@@ -8910,10 +10161,31 @@ function makeExclusiveReader(opts) {
8910
10161
  }
8911
10162
 
8912
10163
  // src/envGate.ts
8913
- import { existsSync as existsSync10 } from "fs";
8914
- import { join as join18, delimiter } from "path";
10164
+ import { existsSync as existsSync13 } from "fs";
10165
+ import { join as join23 } from "path";
8915
10166
  import { createConnection } from "net";
8916
10167
  import { execa as execa2 } from "execa";
10168
+
10169
+ // src/platform.ts
10170
+ import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
10171
+ import { delimiter, join as join22 } from "path";
10172
+ function resolveCommandPath(name, env = process.env, platform = process.platform) {
10173
+ if (name.includes("/") || name.includes("\\")) return existsSync12(name) ? name : null;
10174
+ const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
10175
+ const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
10176
+ for (const dir of dirs) {
10177
+ for (const ext of exts) {
10178
+ const candidate = join22(dir, name + ext);
10179
+ if (existsSync12(candidate)) return candidate;
10180
+ }
10181
+ }
10182
+ return null;
10183
+ }
10184
+ function hasCommandInPath(name, env = process.env, platform = process.platform) {
10185
+ return resolveCommandPath(name, env, platform) !== null;
10186
+ }
10187
+
10188
+ // src/envGate.ts
8917
10189
  var PROBE_TIMEOUT_MS = 2500;
8918
10190
  var PORT_TIMEOUT_MS = 1e3;
8919
10191
  function parseRequirements(lines) {
@@ -8962,8 +10234,8 @@ var COMPOSE_MARKERS = [
8962
10234
  "compose.yaml"
8963
10235
  ];
8964
10236
  function detectRequirements(repo, io = {}) {
8965
- const exists = io.exists ?? existsSync10;
8966
- const marker = COMPOSE_MARKERS.find((f) => exists(join18(repo, f)));
10237
+ const exists = io.exists ?? existsSync13;
10238
+ const marker = COMPOSE_MARKERS.find((f) => exists(join23(repo, f)));
8967
10239
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
8968
10240
  }
8969
10241
  function resolveRequirements(repo, declared, io = {}) {
@@ -8982,17 +10254,14 @@ async function dockerAlive() {
8982
10254
  }
8983
10255
  }
8984
10256
  async function hasCommand(name, env) {
8985
- if (name.includes("/")) return existsSync10(name);
8986
- const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
8987
- const exts = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
8988
- return dirs.some((dir) => exts.some((ext) => existsSync10(join18(dir, name + ext))));
10257
+ return hasCommandInPath(name, env);
8989
10258
  }
8990
10259
  async function portOpen(host, port) {
8991
- return new Promise((resolve3) => {
10260
+ return new Promise((resolve4) => {
8992
10261
  const socket = createConnection({ host, port });
8993
10262
  const finish = (ok) => {
8994
10263
  socket.destroy();
8995
- resolve3(ok);
10264
+ resolve4(ok);
8996
10265
  };
8997
10266
  socket.setTimeout(PORT_TIMEOUT_MS);
8998
10267
  socket.once("connect", () => finish(true));
@@ -9102,8 +10371,8 @@ import { basename as basename3 } from "path";
9102
10371
  import { serve as serve2 } from "@hono/node-server";
9103
10372
 
9104
10373
  // src/diff.ts
9105
- import { statSync as statSync6 } from "fs";
9106
- import { join as join19 } from "path";
10374
+ import { statSync as statSync8 } from "fs";
10375
+ import { join as join24 } from "path";
9107
10376
  import { $ as $4 } from "execa";
9108
10377
  var MAX_FILES = 200;
9109
10378
  var MAX_LINES_PER_FILE = 1500;
@@ -9246,7 +10515,7 @@ async function resolveBase(wt, baseBranch) {
9246
10515
  }
9247
10516
  function mtime(path) {
9248
10517
  try {
9249
- return statSync6(path).mtimeMs;
10518
+ return statSync8(path).mtimeMs;
9250
10519
  } catch {
9251
10520
  return 0;
9252
10521
  }
@@ -9271,7 +10540,7 @@ async function collectRunDiff(worktree, opts = {}) {
9271
10540
  files.push(pf);
9272
10541
  }
9273
10542
  }
9274
- files.sort((a, b) => mtime(join19(worktree, b.path)) - mtime(join19(worktree, a.path)));
10543
+ files.sort((a, b) => mtime(join24(worktree, b.path)) - mtime(join24(worktree, a.path)));
9275
10544
  const truncatedFiles = files.length > MAX_FILES;
9276
10545
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
9277
10546
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -9286,11 +10555,25 @@ async function collectRunDiff(worktree, opts = {}) {
9286
10555
  ...truncatedFiles ? { truncated: true } : {}
9287
10556
  };
9288
10557
  }
10558
+ async function changedPaths(worktree, opts = {}) {
10559
+ if (!worktree) return [];
10560
+ try {
10561
+ const isWt = (await $4({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
10562
+ if (isWt !== "true") return [];
10563
+ const from = await resolveBase(worktree, opts.baseBranch) ?? "HEAD";
10564
+ const tracked = (await $4({ reject: false })`git -C ${worktree} diff --name-only --find-renames ${from} --`).stdout;
10565
+ const untracked = (await $4({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
10566
+ const all = [...tracked.split("\n"), ...untracked.split("\0")].map((s) => s.trim()).filter(Boolean);
10567
+ return [...new Set(all)].sort();
10568
+ } catch {
10569
+ return [];
10570
+ }
10571
+ }
9289
10572
 
9290
10573
  // package.json
9291
10574
  var package_default = {
9292
10575
  name: "@taskless-app/swarm",
9293
- version: "0.4.10",
10576
+ version: "0.6.0",
9294
10577
  type: "module",
9295
10578
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
9296
10579
  keywords: [
@@ -9342,13 +10625,16 @@ var package_default = {
9342
10625
  }
9343
10626
  };
9344
10627
 
10628
+ // src/version.ts
10629
+ var PACKAGE_VERSION = package_default.version;
10630
+ var MIN_NODE_MAJOR = Number(/(\d+)/.exec(package_default.engines?.node ?? "")?.[1] ?? 0);
10631
+
9345
10632
  // src/web.ts
9346
10633
  init_storm();
9347
10634
  init_resources();
9348
10635
  init_config();
9349
10636
  init_droneChannel();
9350
10637
  init_oauth();
9351
- var PACKAGE_VERSION = package_default.version;
9352
10638
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
9353
10639
  var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
9354
10640
  var TASKLESS_MARK = `<svg viewBox="0 0 60 54" width="19" height="19" fill="none" aria-hidden="true" style="display:block"><g transform="matrix(1.0512,0,0,1.0512,0,-0.021)"><path fill="currentColor" fill-rule="nonzero" d="M46.92,0 L10.24,0 C4.59,0 0,4.59 0,10.24 L0,10.4 C0,16.05 4.59,20.64 10.24,20.64 L16.84,20.64 C19.56,20.64 22.12,19.59 24.05,17.67 L36.06,5.77 C37.25,4.6 38.82,3.95 40.49,3.95 L46.93,3.95 C50.4,3.95 53.22,6.77 53.22,10.24 L53.22,10.4 C53.22,13.87 50.4,16.69 46.93,16.69 L39.82,16.69 C37.05,16.69 34.36,17.83 32.44,19.83 L21.13,31.58 C19.28,33.5 18.27,36.02 18.27,38.68 L18.27,40.94 C18.27,46.59 22.86,51.18 28.51,51.18 C34.16,51.18 38.75,46.59 38.75,40.94 L38.75,20.74 C39.1,20.68 39.46,20.64 39.82,20.64 L46.93,20.64 C52.58,20.64 57.17,16.05 57.17,10.4 L57.17,10.24 C57.17,4.59 52.58,0 46.93,0 L46.92,0 Z M21.26,14.87 C20.07,16.04 18.5,16.69 16.83,16.69 L10.23,16.69 C6.76,16.69 3.94,13.87 3.94,10.4 L3.94,10.24 C3.94,6.77 6.76,3.95 10.23,3.95 L32.27,3.95 L21.25,14.87 L21.26,14.87 Z M34.79,40.94 C34.79,44.41 31.97,47.23 28.5,47.23 C25.03,47.23 22.21,44.41 22.21,40.94 L22.21,38.68 C22.21,37.05 22.83,35.5 23.97,34.32 L34.79,23.07 L34.79,40.94 L34.79,40.94 Z"/></g></svg>`;
@@ -9404,6 +10690,10 @@ function roySnapshot(db, now = Date.now(), overmindLog = [], paused = false, dae
9404
10690
  stopReason: r.stop_reason,
9405
10691
  reason,
9406
10692
  updated: r.updated_at,
10693
+ startedAt: r.created_at,
10694
+ // TASWM-351: возраст прогона на карточке
10695
+ costUsd: r.cost_usd || 0,
10696
+ // TASWM-351: то же поле, что уходит в прод (swarm_run_upsert{costUsd})
9407
10697
  step,
9408
10698
  hasDecision: withQ.has(r.id),
9409
10699
  rescued: evs.some((e) => e.kind === "rescued"),
@@ -10656,7 +11946,8 @@ function daemonStatus(deps) {
10656
11946
  restart: deps.restart ? { phase: deps.restart.phase, intent: deps.restart.intent, sessions: live } : null,
10657
11947
  counts,
10658
11948
  live,
10659
- version: PACKAGE_VERSION
11949
+ version: PACKAGE_VERSION,
11950
+ repos: deps.repoMap?.() ?? []
10660
11951
  };
10661
11952
  }
10662
11953
  function eventsStream(db, narratorLog, control, daemonVersion, restart, liveSessionIds2, selfHost) {
@@ -10904,8 +12195,8 @@ function startWeb(deps, port = WEB_PORT) {
10904
12195
  return { port, server };
10905
12196
  }
10906
12197
  function closeWebServer(server) {
10907
- return new Promise((resolve3) => {
10908
- server.close(() => resolve3());
12198
+ return new Promise((resolve4) => {
12199
+ server.close(() => resolve4());
10909
12200
  server.closeAllConnections?.();
10910
12201
  });
10911
12202
  }
@@ -10949,24 +12240,57 @@ async function checkDaemonVersion(start, now = () => /* @__PURE__ */ new Date())
10949
12240
  }
10950
12241
  }
10951
12242
 
12243
+ // src/release.ts
12244
+ init_claudeBin();
12245
+ var SWARM_PACKAGE = "@taskless-app/swarm";
12246
+ var LATEST_TIMEOUT_MS = 4e3;
12247
+ async function fetchLatestVersion(opts = {}) {
12248
+ const fetchFn = opts.fetchFn ?? fetch;
12249
+ const timeoutMs = opts.timeoutMs ?? LATEST_TIMEOUT_MS;
12250
+ const pkg = opts.pkg ?? SWARM_PACKAGE;
12251
+ const ctrl = new AbortController();
12252
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
12253
+ try {
12254
+ const res = await fetchFn(`https://registry.npmjs.org/${pkg}/latest`, {
12255
+ signal: ctrl.signal,
12256
+ headers: { accept: "application/vnd.npm.install-v1+json, application/json" }
12257
+ });
12258
+ if (!res.ok) return null;
12259
+ const body = await res.json();
12260
+ return typeof body.version === "string" && body.version ? body.version : null;
12261
+ } catch {
12262
+ return null;
12263
+ } finally {
12264
+ clearTimeout(timer);
12265
+ }
12266
+ }
12267
+ function isOutdated(current, latest) {
12268
+ if (!latest || !current) return false;
12269
+ return compareVersionDirs(latest, current) > 0;
12270
+ }
12271
+ function outdatedLine(current, latest, updateCommand = "swarm update") {
12272
+ if (!isOutdated(current, latest)) return null;
12273
+ return `\u26A0 \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u043E\u044F \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0430: \u0443 \u0442\u0435\u0431\u044F v${current}, \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest} \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: ${updateCommand}`;
12274
+ }
12275
+
10952
12276
  // src/hostCommands.ts
10953
12277
  init_src();
10954
- import { readFileSync as readFileSync16, renameSync as renameSync6, writeFileSync as writeFileSync12 } from "fs";
10955
- import { join as join20 } from "path";
12278
+ import { readFileSync as readFileSync18, renameSync as renameSync8, writeFileSync as writeFileSync13 } from "fs";
12279
+ import { join as join25 } from "path";
10956
12280
  function hostCommandStatePath(home) {
10957
- return join20(home, "host-command.json");
12281
+ return join25(home, "host-command.json");
10958
12282
  }
10959
12283
  function readState(path) {
10960
12284
  try {
10961
- return JSON.parse(readFileSync16(path, "utf8"));
12285
+ return JSON.parse(readFileSync18(path, "utf8"));
10962
12286
  } catch {
10963
12287
  return {};
10964
12288
  }
10965
12289
  }
10966
12290
  function writeState(path, state) {
10967
12291
  try {
10968
- writeFileSync12(`${path}.tmp`, JSON.stringify(state));
10969
- renameSync6(`${path}.tmp`, path);
12292
+ writeFileSync13(`${path}.tmp`, JSON.stringify(state));
12293
+ renameSync8(`${path}.tmp`, path);
10970
12294
  } catch {
10971
12295
  }
10972
12296
  }
@@ -11064,6 +12388,28 @@ async function executeOne(cmd, opts) {
11064
12388
  await ack(cmd.id, "done", { result: `\u043F\u0440\u043E\u0433\u043E\u043D ${slug} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D` });
11065
12389
  return false;
11066
12390
  }
12391
+ // TASWM-351: «перезапустить с того же места». Отказ побудки (дрон уже работает,
12392
+ // продолжать нечем) уезжает как `rejected` с ЕЁ формулировкой — повторять такую
12393
+ // команду бессмысленно, и это ровно та разница между rejected и failed, ради
12394
+ // которой они разделены (см. HOST_COMMAND_STATUSES).
12395
+ case "wake_run": {
12396
+ const slug = cmd.runSlug?.trim();
12397
+ if (!slug) {
12398
+ await ack(cmd.id, "rejected", { result: "\u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043D\u043E, \u043A\u0430\u043A\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0442\u044C" });
12399
+ return false;
12400
+ }
12401
+ const woken = await effects.wakeRun(slug);
12402
+ if (!woken) {
12403
+ await ack(cmd.id, "rejected", { result: `\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043F\u043E ${slug} \u0443 \u043C\u0435\u043D\u044F \u043D\u0435\u0442` });
12404
+ return false;
12405
+ }
12406
+ if (!woken.ok) {
12407
+ await ack(cmd.id, "rejected", { result: woken.why });
12408
+ return false;
12409
+ }
12410
+ await ack(cmd.id, "done", { result: woken.why });
12411
+ return false;
12412
+ }
11067
12413
  case "restart":
11068
12414
  return await doRestart(cmd, opts, "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B\u0441\u044F \u043F\u043E \u043A\u043E\u043C\u0430\u043D\u0434\u0435 \u0438\u0437 Taskless");
11069
12415
  case "update": {
@@ -11101,8 +12447,8 @@ async function doRestart(cmd, opts, note) {
11101
12447
 
11102
12448
  // src/selfUpdate.ts
11103
12449
  init_git();
11104
- import { existsSync as existsSync11 } from "fs";
11105
- import { join as join21 } from "path";
12450
+ import { existsSync as existsSync14 } from "fs";
12451
+ import { join as join26 } from "path";
11106
12452
  import { $ as $6 } from "execa";
11107
12453
  var DAEMON_PACKAGE = "@taskless-app/swarm";
11108
12454
  var defaultRun = async (cmd, args, cwd) => {
@@ -11115,7 +12461,7 @@ function tail(text, max = 400) {
11115
12461
  }
11116
12462
  async function runSelfUpdate(io) {
11117
12463
  const run = io.run ?? defaultRun;
11118
- const exists = io.exists ?? existsSync11;
12464
+ const exists = io.exists ?? existsSync14;
11119
12465
  const repo = io.repo;
11120
12466
  await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
11121
12467
  const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
@@ -11144,7 +12490,7 @@ async function runSelfUpdate(io) {
11144
12490
  };
11145
12491
  }
11146
12492
  await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
11147
- if (!exists(join21(repo, "pnpm-lock.yaml"))) {
12493
+ if (!exists(join26(repo, "pnpm-lock.yaml"))) {
11148
12494
  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" };
11149
12495
  }
11150
12496
  const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
@@ -11178,13 +12524,14 @@ function makeOvermindNarrator() {
11178
12524
  }
11179
12525
 
11180
12526
  // src/probe.ts
12527
+ init_executors();
11181
12528
  import { spawn as spawn2 } from "child_process";
11182
- var LIMIT_RE = /(weekly limit|usage limit|hit your .*limit)/i;
11183
12529
  var RESET_RE = /resets[^\n]*/i;
11184
- function anthropicHealthy(opts = {}) {
11185
- const bin = opts.claudeBin ?? resolveClaudeBin();
12530
+ function executorHealthy(opts = {}) {
12531
+ const executor = opts.executor ?? resolveExecutor();
12532
+ const bin = opts.bin ?? executor.resolveBin();
11186
12533
  const timeoutMs = opts.timeoutMs ?? 2e4;
11187
- return new Promise((resolve3) => {
12534
+ return new Promise((resolve4) => {
11188
12535
  let done = false;
11189
12536
  let out = "";
11190
12537
  const finish = (r) => {
@@ -11195,9 +12542,9 @@ function anthropicHealthy(opts = {}) {
11195
12542
  child.kill("SIGKILL");
11196
12543
  } catch {
11197
12544
  }
11198
- resolve3(r);
12545
+ resolve4(r);
11199
12546
  };
11200
- const child = spawn2(bin, [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"], { stdio: ["ignore", "pipe", "pipe"] });
12547
+ const child = spawn2(bin, [...executor.probe.args], { stdio: ["ignore", "pipe", "pipe"] });
11201
12548
  child.stdout?.on("data", (d) => {
11202
12549
  out += d.toString();
11203
12550
  });
@@ -11208,7 +12555,9 @@ function anthropicHealthy(opts = {}) {
11208
12555
  child.on("error", () => finish({ ok: false, reason: "exec", resetHint: null }));
11209
12556
  child.on("exit", (code) => {
11210
12557
  if (code === 0) return finish({ ok: true, reason: "ok", resetHint: null });
11211
- if (LIMIT_RE.test(out)) return finish({ ok: false, reason: "limit", resetHint: out.match(RESET_RE)?.[0]?.trim() ?? null });
12558
+ if (executor.probe.limitPattern.test(out)) {
12559
+ return finish({ ok: false, reason: "limit", resetHint: out.match(RESET_RE)?.[0]?.trim() ?? null });
12560
+ }
11212
12561
  finish({ ok: false, reason: "provider", resetHint: null });
11213
12562
  });
11214
12563
  });
@@ -11234,18 +12583,21 @@ async function fetchExternalStorm(url, opts = {}) {
11234
12583
  }
11235
12584
 
11236
12585
  // src/anomalyState.ts
11237
- import { existsSync as existsSync12, readFileSync as readFileSync17, renameSync as renameSync7, writeFileSync as writeFileSync13 } from "fs";
11238
- import { join as join22 } from "path";
12586
+ import { existsSync as existsSync15, readFileSync as readFileSync19, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
12587
+ import { join as join27 } from "path";
11239
12588
  var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
11240
- var EMPTY = { since: {}, lastFiredAt: {}, fires: {}, stopQuiet: null };
12589
+ var MAX_RESOLVED = 200;
12590
+ function emptyState() {
12591
+ return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
12592
+ }
11241
12593
  function anomalyStatePath(home) {
11242
- return join22(home, "anomaly-state.json");
12594
+ return join27(home, "anomaly-state.json");
11243
12595
  }
11244
12596
  function loadAnomalyState(home, now = Date.now(), io = {}) {
11245
- const exists = io.exists ?? existsSync12;
11246
- const readFile2 = io.readFile ?? ((p) => readFileSync17(p, "utf8"));
12597
+ const exists = io.exists ?? existsSync15;
12598
+ const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
11247
12599
  const path = anomalyStatePath(home);
11248
- if (!exists(path)) return { ...EMPTY };
12600
+ if (!exists(path)) return emptyState();
11249
12601
  try {
11250
12602
  const raw = JSON.parse(readFile2(path));
11251
12603
  const since = numbers(raw.since);
@@ -11260,17 +12612,21 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
11260
12612
  since: pick(since, fresh),
11261
12613
  lastFiredAt: pick(lastFiredAt, fresh),
11262
12614
  fires: pick(fires, fresh),
12615
+ // TAS-485: очередь гашения НЕ фильтруем тем же TTL, что ключи. Она про
12616
+ // уведомление, которое УЖЕ висит у человека в инбоксе: пролежав неделю
12617
+ // недоставленным, гашение не становится лишним — оно становится нужнее.
12618
+ resolved: strings(raw.resolved).slice(0, MAX_RESOLVED),
11263
12619
  stopQuiet: parseQuiet(raw.stopQuiet)
11264
12620
  };
11265
12621
  } catch {
11266
- return { ...EMPTY };
12622
+ return emptyState();
11267
12623
  }
11268
12624
  }
11269
12625
  function saveAnomalyState(home, state, io = {}) {
11270
12626
  const path = anomalyStatePath(home);
11271
12627
  const writeFile2 = io.writeFile ?? ((p, data) => {
11272
- writeFileSync13(`${p}.tmp`, data);
11273
- renameSync7(`${p}.tmp`, p);
12628
+ writeFileSync14(`${p}.tmp`, data);
12629
+ renameSync9(`${p}.tmp`, p);
11274
12630
  });
11275
12631
  writeFile2(path, JSON.stringify(state, null, 2));
11276
12632
  }
@@ -11286,9 +12642,14 @@ var AnomalyStateStore = class {
11286
12642
  /** Инъекция в AnomalyTracker (см. AnomalyTrackerOpts.io). */
11287
12643
  trackerIo() {
11288
12644
  return {
11289
- load: () => ({ since: this.state.since, lastFiredAt: this.state.lastFiredAt, fires: this.state.fires }),
12645
+ load: () => ({
12646
+ since: this.state.since,
12647
+ lastFiredAt: this.state.lastFiredAt,
12648
+ fires: this.state.fires,
12649
+ resolved: this.state.resolved
12650
+ }),
11290
12651
  save: (snapshot) => {
11291
- this.state = { ...snapshot, stopQuiet: this.state.stopQuiet };
12652
+ this.state = { ...snapshot, resolved: (snapshot.resolved ?? []).slice(0, MAX_RESOLVED), stopQuiet: this.state.stopQuiet };
11292
12653
  this.flush();
11293
12654
  }
11294
12655
  };
@@ -11330,6 +12691,10 @@ function numbers(raw) {
11330
12691
  }
11331
12692
  return out;
11332
12693
  }
12694
+ function strings(raw) {
12695
+ if (!Array.isArray(raw)) return [];
12696
+ return raw.filter((v) => typeof v === "string" && v.length > 0);
12697
+ }
11333
12698
  function pick(src, keys) {
11334
12699
  const out = {};
11335
12700
  for (const k of keys) if (src[k] != null) out[k] = src[k];
@@ -11362,7 +12727,7 @@ function isRepeatEvent(prev, cur) {
11362
12727
  }
11363
12728
  function swarmLastActivityMs(db) {
11364
12729
  let latest = null;
11365
- const bump = (iso) => {
12730
+ const bump2 = (iso) => {
11366
12731
  const ms = sqliteMs(iso);
11367
12732
  if (latest == null || ms > latest) latest = ms;
11368
12733
  };
@@ -11370,12 +12735,12 @@ function swarmLastActivityMs(db) {
11370
12735
  if (isTerminalStatus(run.status)) continue;
11371
12736
  const evs = db.events(run.id);
11372
12737
  if (!evs.length) {
11373
- bump(run.updated_at);
12738
+ bump2(run.updated_at);
11374
12739
  continue;
11375
12740
  }
11376
12741
  let i = evs.length - 1;
11377
12742
  while (i > 0 && isRepeatEvent(evs[i - 1], evs[i])) i--;
11378
- bump(evs[i].at);
12743
+ bump2(evs[i].at);
11379
12744
  }
11380
12745
  return latest;
11381
12746
  }
@@ -11501,7 +12866,193 @@ function decideWatchdogAction(db, now, state, thresholds, droneCap, wt, exclusiv
11501
12866
  // src/overmind.ts
11502
12867
  init_storm();
11503
12868
  init_src();
12869
+
12870
+ // src/zones.ts
12871
+ init_src();
12872
+
12873
+ // src/gitHistory.ts
12874
+ import { $ as $7 } from "execa";
12875
+ var REC = "";
12876
+ var FIELD = "";
12877
+ var LOG_FORMAT = `${REC}%H${FIELD}%s`;
12878
+ var SLUG_IN_SUBJECT = /\b([A-Z][A-Z0-9]{0,7}-\d+)\b/;
12879
+ function slugFromSubject(subject) {
12880
+ return SLUG_IN_SUBJECT.exec(subject)?.[1] ?? null;
12881
+ }
12882
+ function unquotePath(raw) {
12883
+ const p = raw.trim();
12884
+ return p.length >= 2 && p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
12885
+ }
12886
+ function parseGitLog(out) {
12887
+ const commits = [];
12888
+ for (const chunk of out.split(REC)) {
12889
+ if (!chunk.trim()) continue;
12890
+ const [head, ...rest] = chunk.split("\n");
12891
+ const [sha, subject] = (head ?? "").split(FIELD);
12892
+ if (!sha) continue;
12893
+ const files = rest.map(unquotePath).filter(Boolean);
12894
+ commits.push({ sha, subject: subject ?? "", files });
12895
+ }
12896
+ return commits;
12897
+ }
12898
+ var NOISE_RE = /^(journal\/|changelog\/|docs\/|design\/|scratch\/|\.claude\/|.*\.lock$|pnpm-lock\.yaml$)/;
12899
+ function isNoisePath(path) {
12900
+ return NOISE_RE.test(path);
12901
+ }
12902
+ var GROUP_DEFAULTS = { maxFilesPerCommit: 120, ignore: isNoisePath };
12903
+ function groupTaskHistory(commits, opts = {}) {
12904
+ const o = { ...GROUP_DEFAULTS, ...opts };
12905
+ const bySlug = /* @__PURE__ */ new Map();
12906
+ for (const c of commits) {
12907
+ const slug = slugFromSubject(c.subject);
12908
+ if (!slug) continue;
12909
+ if (c.files.length > o.maxFilesPerCommit) continue;
12910
+ const files = c.files.filter((f) => !o.ignore(f));
12911
+ if (!files.length) continue;
12912
+ let acc = bySlug.get(slug);
12913
+ if (!acc) bySlug.set(slug, acc = { files: /* @__PURE__ */ new Set(), texts: [] });
12914
+ for (const f of files) acc.files.add(f);
12915
+ acc.texts.push(c.subject);
12916
+ }
12917
+ return [...bySlug.entries()].map(([slug, acc]) => ({
12918
+ slug,
12919
+ files: [...acc.files],
12920
+ text: acc.texts.join(" \xB7 ")
12921
+ }));
12922
+ }
12923
+ async function readCommitLog(repo, opts = {}) {
12924
+ const max = opts.maxCommits ?? 1500;
12925
+ try {
12926
+ const { stdout } = await $7({ cwd: repo, reject: false })`git log --no-merges --name-only --pretty=format:${LOG_FORMAT} -n ${String(max)} ${opts.ref ?? "HEAD"}`;
12927
+ return parseGitLog(stdout ?? "");
12928
+ } catch {
12929
+ return [];
12930
+ }
12931
+ }
12932
+ async function readTaskHistory(repo, opts = {}) {
12933
+ return groupTaskHistory(await readCommitLog(repo, opts), opts);
12934
+ }
12935
+ async function headSha(repo) {
12936
+ try {
12937
+ const { stdout, exitCode } = await $7({ cwd: repo, reject: false })`git rev-parse HEAD`;
12938
+ return exitCode === 0 ? stdout.trim() : null;
12939
+ } catch {
12940
+ return null;
12941
+ }
12942
+ }
12943
+
12944
+ // src/zones.ts
12945
+ function runText(run) {
12946
+ return [run.task_title ?? "", run.task_desc ?? ""].join(" ").trim();
12947
+ }
12948
+ function predictForRun(model, run) {
12949
+ return predictFiles(model, { zone: run.area, text: runText(run) });
12950
+ }
12951
+ function short(files, n = 3) {
12952
+ return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
12953
+ }
12954
+ var SPINE_PREDICTION_WEIGHT = 0.7;
12955
+ function decideConflict({ model, candidate, active }) {
12956
+ if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
12957
+ if (!hasZoneModel(model)) return { kind: "unknown" };
12958
+ const predicted = predictForRun(model, candidate);
12959
+ if (!predicted.length) return { kind: "unknown" };
12960
+ if (!active.length) return { kind: "clear" };
12961
+ const spine = new Set(model.spine);
12962
+ const candidateSpine = predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
12963
+ if (candidateSpine.length) {
12964
+ const blocker = active[0];
12965
+ return {
12966
+ kind: "wait",
12967
+ blockedBy: blocker.run,
12968
+ 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})`
12969
+ };
12970
+ }
12971
+ for (const w of active) {
12972
+ if (!w.actual) continue;
12973
+ const theirSpine = w.files.filter((f) => spine.has(f));
12974
+ if (theirSpine.length && zoneForFiles(model, w.files)?.zone === SPINE_ZONE) {
12975
+ return {
12976
+ kind: "wait",
12977
+ blockedBy: w.run,
12978
+ 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`
12979
+ };
12980
+ }
12981
+ }
12982
+ for (const w of active) {
12983
+ const risk = overlapRisk(predicted, w.files);
12984
+ if (!risk.overlap) continue;
12985
+ const what = risk.sharedFiles.length ? `\u043E\u0434\u043D\u0438 \u0444\u0430\u0439\u043B\u044B (${short(risk.sharedFiles)})` : `\u043E\u0434\u0438\u043D \u043A\u0430\u0442\u0430\u043B\u043E\u0433 (${short(risk.sharedDirs)})`;
12986
+ return {
12987
+ kind: "wait",
12988
+ blockedBy: w.run,
12989
+ message: `\u044D\u0442\u0438 \u0434\u0432\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0442\u0440\u043E\u0433\u0430\u044E\u0442 ${what} \u2014 \u0441\u0442\u0430\u0432\u0438\u043C \u043F\u043E\u0441\u043B\u0435\u0434\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u043D\u043E: \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)")
12990
+ };
12991
+ }
12992
+ return { kind: "clear" };
12993
+ }
12994
+ function suggestAreaForRun(model, run) {
12995
+ if (!hasZoneModel(model)) return null;
12996
+ const text = runText(run);
12997
+ if (!text) return null;
12998
+ const top = suggestZonesFromText(model, text)[0];
12999
+ return top && top.confidence >= 0.3 ? { zone: top.zone, confidence: top.confidence, why: top.why } : null;
13000
+ }
13001
+ var DEFAULT_TTL_MS = 10 * 6e4;
13002
+ var ZoneModels = class {
13003
+ constructor(opts = {}) {
13004
+ this.opts = opts;
13005
+ }
13006
+ opts;
13007
+ cache = /* @__PURE__ */ new Map();
13008
+ /** Модель репо. Истории нет / это не git — EMPTY_ZONE_MODEL (раздача на старом локе). */
13009
+ async forRepo(repo, now = Date.now()) {
13010
+ if (!repo) return EMPTY_ZONE_MODEL;
13011
+ const head = await headSha(repo);
13012
+ const hit = this.cache.get(repo);
13013
+ if (hit && hit.head === head && now - hit.builtAt < (this.opts.ttlMs ?? DEFAULT_TTL_MS)) return hit.model;
13014
+ const model = await this.build(repo);
13015
+ this.cache.set(repo, { head, builtAt: now, model });
13016
+ return model;
13017
+ }
13018
+ /** Сбросить кэш (тесты, `swarm zones --refresh`). */
13019
+ clear() {
13020
+ this.cache.clear();
13021
+ }
13022
+ async build(repo) {
13023
+ const history = await readTaskHistory(repo, { ...this.opts.maxCommits ? { maxCommits: this.opts.maxCommits } : {} });
13024
+ return buildZoneModel(withManualAreas(history, this.opts.manualAreas?.() ?? /* @__PURE__ */ new Map()));
13025
+ }
13026
+ };
13027
+ var ActiveFilesCache = class {
13028
+ constructor(read, ttlMs = 3e4) {
13029
+ this.read = read;
13030
+ this.ttlMs = ttlMs;
13031
+ }
13032
+ read;
13033
+ ttlMs;
13034
+ cache = /* @__PURE__ */ new Map();
13035
+ async filesOf(run, now = Date.now()) {
13036
+ const hit = this.cache.get(run.id);
13037
+ if (hit && now - hit.at < this.ttlMs) return hit.files;
13038
+ const files = await this.read(run);
13039
+ this.cache.set(run.id, { at: now, files });
13040
+ return files;
13041
+ }
13042
+ /** Забыть всех, кого нет среди живых (дрон закончил — его файлы больше не заняты). */
13043
+ prune(liveIds) {
13044
+ const live = new Set(liveIds);
13045
+ for (const id of [...this.cache.keys()]) if (!live.has(id)) this.cache.delete(id);
13046
+ }
13047
+ };
13048
+ function withManualAreas(history, areas) {
13049
+ return history.map((e) => ({ ...e, area: normalizeZoneName(areas.get(e.slug) ?? null) }));
13050
+ }
13051
+
13052
+ // src/overmind.ts
13053
+ var ANOMALY_RESOLVE_BATCH = 50;
11504
13054
  var THIS_FILE = fileURLToPath2(import.meta.url);
13055
+ var executorHasSession = (p) => EXECUTOR.session.exists(p);
11505
13056
  var OVERMIND_ENTRY = THIS_FILE;
11506
13057
  var STALL_MS = Number(process.env.SWARM_STALL_MS) || 10 * 60 * 1e3;
11507
13058
  var STALL_FLOOR_MS = Number(process.env.SWARM_STALL_FLOOR_MS) || 30 * 60 * 1e3;
@@ -11515,6 +13066,10 @@ var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
11515
13066
  var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
11516
13067
  var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
11517
13068
  var REVIEW_ESCALATION_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F";
13069
+ var REVIEW_UNRECOVERABLE_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u044C";
13070
+ function reviewUnrecoverableMark(branch) {
13071
+ return `${REVIEW_UNRECOVERABLE_MARK} (\u0432\u0435\u0442\u043A\u0430 ${branch})`;
13072
+ }
11518
13073
  var REVIEW_CYCLE_LIMIT = Number(process.env.SWARM_REVIEW_CYCLE_LIMIT) || 3;
11519
13074
  var REVIEW_CYCLE_ESCALATION_MARK = "\u0440\u0435-\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0446\u0438\u043A\u043B\u0438\u043B\u043E\u0441\u044C";
11520
13075
  var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
@@ -11541,6 +13096,7 @@ var CHANGES_WAKE_ESCALATION_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\
11541
13096
  var NEEDS_HUMAN_MARKS = [
11542
13097
  RESOLVE_ESCALATION_MARK,
11543
13098
  REVIEW_ESCALATION_MARK,
13099
+ REVIEW_UNRECOVERABLE_MARK,
11544
13100
  REVIEW_CYCLE_ESCALATION_MARK,
11545
13101
  CI_WAKE_ESCALATION_MARK,
11546
13102
  CHANGES_WAKE_ESCALATION_MARK
@@ -11869,8 +13425,12 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
11869
13425
  // авторские подписи внутри него уже приведены к одной строке (renderAttachmentsBlock).
11870
13426
  ...attachmentsBlock ? ["", attachmentsBlock] : [],
11871
13427
  "",
11872
- "\u0422\u044B \u2014 \u0434\u0440\u043E\u043D \u0440\u043E\u044F. \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C \u0432 \u044D\u0442\u043E\u043C git-worktree, \u0432\u0435\u0442\u043A\u0430 \u0443\u0436\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430 \u043F\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438.",
11873
- "\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; read_task() \u2014 \u0441\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 (\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435+\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u044B+\u0434\u043E\u043A\u0438) \u0431\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432, \u0435\u0441\u043B\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043C\u043E\u0433\u043B\u0438 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435 \u0441\u0442\u0430\u0440\u0442\u0430; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR; note(text) \u2014 \u0437\u0430\u043C\u0435\u0442\u043A\u0430 \xAB\u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438\xBB \u0411\u0415\u0417 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0438: \u043D\u0430\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u043D\u0430 \u043F\u0440\u043E\u0431\u0435\u043B \u0432 \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435, \u043F\u0440\u043E\u0442\u0443\u0445\u0448\u0443\u044E \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u044E, \u0431\u0430\u0433 \u0440\u044F\u0434\u043E\u043C \u0441\u043E \u0441\u0432\u043E\u0438\u043C \u0434\u0438\u0444\u0444\u043E\u043C \u2014 \u0441\u043A\u0430\u0436\u0438 \u0438 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u0434\u0430\u043B\u044C\u0448\u0435, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0443\u0432\u0438\u0434\u0438\u0442 \u0438 \u0441\u0430\u043C \u0440\u0435\u0448\u0438\u0442, \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u044C \u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043D\u0435 \u043F\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0439 \u0442\u0430\u043A\u043E\u0435 \u0432 \u0434\u043E\u0440\u043E\u0433\u043E\u0439 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 ask \u0438 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u0438 \u0432 \u043E\u0442\u0447\u0451\u0442\u0435 \u043E \u0441\u0434\u0430\u0447\u0435).",
13428
+ // TASWM-356: ветка worktree — НЕ обязательно слаг. У продолжения чужого PR она своя
13429
+ // (её завёл прошлый дрон), и `git push -u origin <slug>` оттуда либо откажет, либо заведёт
13430
+ // вторую ветку мимо PR. Пишем ту, что реально записана на прогоне; пусто (первый спавн —
13431
+ // ветку заводит spawnDrone из слага) — прежняя формулировка.
13432
+ `\u0422\u044B \u2014 \u0434\u0440\u043E\u043D \u0440\u043E\u044F. \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C \u0432 \u044D\u0442\u043E\u043C git-worktree, \u0432\u0435\u0442\u043A\u0430 \`${run.branch || run.task_slug}\`${run.branch && run.branch !== run.task_slug ? " (\u043E\u043D\u0430 \u041D\u0415 \u0440\u0430\u0432\u043D\u0430 \u0441\u043B\u0430\u0433\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u044D\u0442\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u0435 \u0443\u0436\u0435 \u043E\u0442\u043A\u0440\u044B\u0442\u043E\u0433\u043E PR, \u043F\u0443\u0448 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u043D\u0435\u0451)" : " \u0441\u043E\u0437\u0434\u0430\u043D\u0430 \u043F\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438"}.`,
13433
+ "\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; read_task() \u2014 \u0441\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 (\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435+\u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438+\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u044B+\u0434\u043E\u043A\u0438+\u0438\u0441\u0445\u043E\u0434\u044B \u0441\u043E\u0441\u0435\u0434\u043D\u0438\u0445 \u0437\u0430\u0434\u0430\u0447 \u043F\u043E \u0437\u043E\u043D\u0435 \u043A\u043E\u0434\u0430) \u0431\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432, \u0435\u0441\u043B\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043C\u043E\u0433\u043B\u0438 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435 \u0441\u0442\u0430\u0440\u0442\u0430; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR; note(text) \u2014 \u0437\u0430\u043C\u0435\u0442\u043A\u0430 \xAB\u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438\xBB \u0411\u0415\u0417 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0438: \u043D\u0430\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u043D\u0430 \u043F\u0440\u043E\u0431\u0435\u043B \u0432 \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435, \u043F\u0440\u043E\u0442\u0443\u0445\u0448\u0443\u044E \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u044E, \u0431\u0430\u0433 \u0440\u044F\u0434\u043E\u043C \u0441\u043E \u0441\u0432\u043E\u0438\u043C \u0434\u0438\u0444\u0444\u043E\u043C \u2014 \u0441\u043A\u0430\u0436\u0438 \u0438 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u0434\u0430\u043B\u044C\u0448\u0435, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0443\u0432\u0438\u0434\u0438\u0442 \u0438 \u0441\u0430\u043C \u0440\u0435\u0448\u0438\u0442, \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u044C \u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043D\u0435 \u043F\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0439 \u0442\u0430\u043A\u043E\u0435 \u0432 \u0434\u043E\u0440\u043E\u0433\u043E\u0439 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 ask \u0438 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u0438 \u0432 \u043E\u0442\u0447\u0451\u0442\u0435 \u043E \u0441\u0434\u0430\u0447\u0435).",
11874
13434
  "\u041F\u0440\u0430\u0432\u0438\u043B\u0430: \u0442\u0440\u043E\u0433\u0430\u0435\u0448\u044C UI \u2014 \u0421\u041D\u0410\u0427\u0410\u041B\u0410 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438, \u0435\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0441\u043A\u0438\u043B\u043B \u0441 UI-\u043F\u0440\u0430\u0432\u0438\u043B\u0430\u043C\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430, \u0438 \u0441\u043B\u0435\u0434\u0443\u0439 \u0435\u0433\u043E \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435 (\u043D\u0435\u0442 \u043D\u0443\u0436\u043D\u043E\u0433\u043E \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0430 \u2014 \u0437\u0430\u0432\u0435\u0434\u0438 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443, \u043D\u0435 \u0445\u0430\u0440\u0434\u043A\u043E\u0434\u044C \u043E\u0434\u043D\u043E\u0440\u0430\u0437\u043E\u0432\u043E\u0435); \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0441\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0438; \u043F\u043E\u0441\u043B\u0435 UI-\u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u0441\u043A\u0440\u0438\u043D\u044B mobile+desktop \u041A \u0417\u0410\u0414\u0410\u0427\u0415 (\u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 screenshots \u0443 done); \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0447\u0435\u0440\u0435\u0437 gh \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url).",
11875
13435
  // TASWM-293: живой случай SCE-5 — ревьюер заблокировал PR за отсутствие CI (у проекта
11876
13436
  // его нет и не предполагается), автора разбудили «закрывать замечания», замечаний по коду
@@ -11887,7 +13447,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
11887
13447
  `\u0412\u0435\u0434\u0438 \u0434\u043D\u0435\u0432\u043D\u0438\u043A \u0437\u0430\u0434\u0430\u0447\u0438: \u0437\u0430\u0432\u0435\u0434\u0438 \`journal/${run.task_slug}.md\` \u0438 \u043F\u043E \u0445\u043E\u0434\u0443 \u0440\u0430\u0431\u043E\u0442\u044B (\u043D\u0435 \u043E\u0434\u043D\u0438\u043C \u043C\u0430\u0445\u043E\u043C \u0432 \u043A\u043E\u043D\u0446\u0435) \u0434\u043E\u043F\u0438\u0441\u044B\u0432\u0430\u0439 \u0442\u0443\u0434\u0430 \u0448\u0430\u0433\u0438 \u0438 \u043F\u0440\u0438\u043D\u044F\u0442\u044B\u0435 \u0440\u0435\u0448\u0435\u043D\u0438\u044F \u2014 \u044D\u0442\u043E \u0445\u0440\u043E\u043D\u0438\u043A\u0430 \u0434\u043B\u044F \u0440\u0435\u0442\u0440\u043E\u0441\u043F\u0435\u043A\u0442\u0438\u0432\u044B, \u0441\u0432\u043E\u0439 \u0444\u0430\u0439\u043B \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0434\u0440\u0430\u0442\u044C\u0441\u044F \u0437\u0430 \u043E\u0431\u0449\u0438\u0439 \u0441 \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u044B\u043C\u0438 \u0434\u0440\u043E\u043D\u0430\u043C\u0438. \u041A\u043E\u043C\u043C\u0438\u0442\u044C \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u043C\u0438 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F\u043C\u0438 PR, \u043E\u0431\u044B\u0447\u043D\u044B\u043C \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u043C. \u041D\u0435 \u0434\u0443\u0431\u043B\u0438\u0440\u0443\u0439 \u0441 summary \u0432 done(): \u0434\u043D\u0435\u0432\u043D\u0438\u043A \u2014 \u0441\u044B\u0440\u0430\u044F \u0445\u0440\u043E\u043D\u0438\u043A\u0430 (\u0447\u0442\u043E \u043F\u0440\u043E\u0431\u043E\u0432\u0430\u043B, \u0447\u0442\u043E \u043E\u0442\u0431\u0440\u043E\u0441\u0438\u043B \u0438 \u043F\u043E\u0447\u0435\u043C\u0443), summary \u2014 \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u0439 \u0438\u0442\u043E\u0433 \u0434\u043B\u044F \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430; \u0435\u0441\u043B\u0438 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0443\u0436\u0435 \u0440\u0430\u0441\u043F\u0438\u0441\u0430\u043D\u043E \u0432 \u0434\u043D\u0435\u0432\u043D\u0438\u043A\u0435, \u0432 summary \u043F\u0440\u043E\u0441\u0442\u043E \u0441\u043E\u0448\u043B\u0438\u0441\u044C \u043D\u0430 \u043D\u0435\u0433\u043E.`,
11888
13448
  "",
11889
13449
  "\u0421\u041E\u0425\u0420\u0410\u041D\u042F\u0419 \u0420\u0410\u0411\u041E\u0422\u0423 \u041F\u041E \u0425\u041E\u0414\u0423 \u2014 \u0438\u043D\u0430\u0447\u0435 \u043E\u043D\u0430 \u043F\u0440\u043E\u043F\u0430\u0434\u0451\u0442, \u0435\u0441\u043B\u0438 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0443\u0431\u044C\u044E\u0442:",
11890
- `\u2022 \u041F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0437\u043D\u0430\u0447\u0438\u043C\u043E\u0433\u043E \u0448\u0430\u0433\u0430 \u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0438 \u0441\u0440\u0430\u0437\u0443 \u043F\u0443\u0448\u044C: git push -u origin ${run.task_slug} (\u0432\u0435\u0442\u043A\u0430 = slug). \u041D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0438\u0440\u0430\u0435\u0442 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0434\u0440\u043E\u043D\u043E\u043C \u2014 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0438 \u043F\u0440\u0430\u0432\u043A\u0438 \u0432 worktree \u043D\u0435 \u043F\u0435\u0440\u0435\u0436\u0438\u0432\u0443\u0442 \u0441\u043C\u0435\u0440\u0442\u044C \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430.`,
13450
+ `\u2022 \u041F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0437\u043D\u0430\u0447\u0438\u043C\u043E\u0433\u043E \u0448\u0430\u0433\u0430 \u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0438 \u0441\u0440\u0430\u0437\u0443 \u043F\u0443\u0448\u044C: git push -u origin ${run.branch || run.task_slug}. \u041D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0438\u0440\u0430\u0435\u0442 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0434\u0440\u043E\u043D\u043E\u043C \u2014 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0438 \u043F\u0440\u0430\u0432\u043A\u0438 \u0432 worktree \u043D\u0435 \u043F\u0435\u0440\u0435\u0436\u0438\u0432\u0443\u0442 \u0441\u043C\u0435\u0440\u0442\u044C \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430.`,
11891
13451
  "\u2022 \u041D\u0435 \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439 \u0434\u043E\u043B\u0433\u0438\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0432 \u0444\u043E\u043D\u0435 \xAB\u0447\u0442\u043E\u0431\u044B \u043F\u043E\u0434\u043E\u0436\u0434\u0430\u0442\u044C\xBB: \u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u0442\u0441\u044F, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u0451\u0448\u044C \u0437\u0432\u0430\u0442\u044C \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B, \u0438 \u0444\u043E\u043D\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043D\u0435 \u0441\u043F\u0430\u0441\u0451\u0442. \u0414\u043E\u043B\u0433\u043E\u0435 \u0434\u0435\u0440\u0436\u0438 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u043C \u0432\u044B\u0437\u043E\u0432\u043E\u043C; \u0434\u043B\u0438\u043D\u043D\u0443\u044E \u0441\u0435\u0440\u0438\u044E \u0434\u0440\u043E\u0431\u0438 \u043D\u0430 \u0447\u0430\u0441\u0442\u0438 \u0438 \u043A\u043E\u043C\u043C\u0438\u0442\u044C+\u043F\u0443\u0448\u044C \u043C\u0435\u0436\u0434\u0443 \u043D\u0438\u043C\u0438.",
11892
13452
  '\u2022 \u041F\u0435\u0440\u0435\u0434 \u0434\u043E\u043B\u0433\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 (\u0442\u0435\u0441\u0442\u044B, \u0441\u0431\u043E\u0440\u043A\u0430 \u2014 \u043C\u0438\u043D\u0443\u0442\u044B \u0431\u0435\u0437 \u0432\u044B\u0432\u043E\u0434\u0430) \u043A\u0438\u043D\u044C report_step("\u0433\u043E\u043D\u044E \u0442\u0435\u0441\u0442\u044B\u2026") \u2014 \u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E (\u0440\u043E\u0439 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E \u043E\u0442\u0441\u043B\u0435\u0436\u0438\u0432\u0430\u0435\u0442 \u0436\u0438\u0432\u043E\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043A\u043E\u043C\u0430\u043D\u0434\u044B), \u043D\u043E \u043F\u043E\u043B\u0435\u0437\u043D\u0435\u0435 \u0434\u043B\u044F \u0432\u0438\u0434\u0438\u043C\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430 \u0432 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u0435 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430.',
11893
13453
  "",
@@ -11912,10 +13472,10 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
11912
13472
  }
11913
13473
  return parts.join("\n");
11914
13474
  }
11915
- var HERO_CHARTER_PATH = join23(dirname5(THIS_FILE), "..", "docs", "HERO.md");
13475
+ var HERO_CHARTER_PATH = join28(dirname5(THIS_FILE), "..", "docs", "HERO.md");
11916
13476
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
11917
13477
  const name = run.name || `#${run.id}`;
11918
- const charter = readFileSync18(HERO_CHARTER_PATH, "utf8");
13478
+ const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
11919
13479
  const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
11920
13480
 
11921
13481
  ${task.description}` : `\u042D\u041F\u0418\u041A ${run.task_slug} (\u0442\u0435\u043B\u043E \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E slug \u0438 \u0437\u0434\u0440\u0430\u0432\u043E\u043C\u0443 \u0441\u043C\u044B\u0441\u043B\u0443).`;
@@ -12166,8 +13726,8 @@ async function wakeRun(deps, runId) {
12166
13726
  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` };
12167
13727
  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` };
12168
13728
  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` };
12169
- const worktreeExists = deps.worktreeExists ?? existsSync13;
12170
- const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
13729
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
13730
+ const hasTranscript = deps.hasTranscript ?? executorHasSession;
12171
13731
  const wt = run.worktree_path;
12172
13732
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
12173
13733
  const isService = run.reviews_run_id != null || run.resolves_run_id != null;
@@ -12251,9 +13811,9 @@ function repoHasCi(deps, run) {
12251
13811
  function repoHasCiWorkflows(worktreePath, repo) {
12252
13812
  for (const base3 of [worktreePath, repo]) {
12253
13813
  if (!base3) continue;
12254
- const dir = join23(base3, ".github", "workflows");
13814
+ const dir = join28(base3, ".github", "workflows");
12255
13815
  try {
12256
- if (readdirSync10(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
13816
+ if (readdirSync12(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
12257
13817
  } catch {
12258
13818
  }
12259
13819
  }
@@ -12400,8 +13960,8 @@ async function wakeReviewerOnCiReady(deps, run, over) {
12400
13960
  return true;
12401
13961
  }
12402
13962
  function worktreeAndTranscriptAlive(deps, worktree) {
12403
- const exists = deps.worktreeExists ?? existsSync13;
12404
- const transcript = deps.hasTranscript ?? sessionTranscriptExists;
13963
+ const exists = deps.worktreeExists ?? existsSync16;
13964
+ const transcript = deps.hasTranscript ?? executorHasSession;
12405
13965
  return exists(worktree) && transcript(worktree);
12406
13966
  }
12407
13967
  async function spawnFreshReviewerAfterStall(deps, run, why) {
@@ -12504,6 +14064,22 @@ async function envGateAllows(deps, run) {
12504
14064
  function effectiveRepo(deps, run) {
12505
14065
  return normalizeRepo(run.repo || deps.repoFallback || "");
12506
14066
  }
14067
+ async function zoneConflictFor(db, models, activeFiles, run, repo, repoFallback) {
14068
+ const model = await models.forRepo(repo);
14069
+ if (!hasZoneModel(model)) return { kind: "unknown" };
14070
+ const live = db.listRuns().filter((r) => (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status));
14071
+ activeFiles.prune(live.map((r) => r.id));
14072
+ const active = [];
14073
+ for (const r of live) {
14074
+ if (r.id === run.id) continue;
14075
+ if (normalizeRepo(r.repo || repoFallback || "") !== repo) continue;
14076
+ const files = await activeFiles.filesOf(r);
14077
+ active.push(
14078
+ files.length ? { run: r, files, actual: true } : { run: r, files: predictForRun(model, r).map((p) => p.path), actual: false }
14079
+ );
14080
+ }
14081
+ return decideConflict({ model, candidate: run, active });
14082
+ }
12507
14083
  function activeDronesOnRepo(deps, repo, exceptRunId) {
12508
14084
  return deps.db.listRuns().filter(
12509
14085
  (r) => r.id !== exceptRunId && (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status) && effectiveRepo(deps, r) === repo
@@ -12543,6 +14119,15 @@ async function exclusiveGateAllows(deps, run) {
12543
14119
  }
12544
14120
  return true;
12545
14121
  }
14122
+ function pruneGateLogs(deps) {
14123
+ if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size && !deps.overlapLogged?.size) return;
14124
+ const open = /* @__PURE__ */ new Set();
14125
+ for (const run of deps.db.listRuns()) if (!isTerminalStatus(run.status)) open.add(run.id);
14126
+ for (const map of [deps.envGateLogged, deps.exclusiveLogged, deps.overlapLogged]) {
14127
+ if (!map) continue;
14128
+ for (const id of [...map.keys()]) if (!open.has(id)) map.delete(id);
14129
+ }
14130
+ }
12546
14131
  async function exclusivityBlockedRunIds(deps) {
12547
14132
  const blocked = /* @__PURE__ */ new Set();
12548
14133
  if (!deps.exclusiveProject) return blocked;
@@ -12559,6 +14144,18 @@ async function exclusivityBlockedRunIds(deps) {
12559
14144
  }
12560
14145
  return blocked;
12561
14146
  }
14147
+ async function fillSuggestedArea(deps, run) {
14148
+ if (run.area || !deps.suggestArea) return;
14149
+ const guess = await deps.suggestArea(run).catch(() => null);
14150
+ if (!guess) return;
14151
+ deps.db.updateRun(run.id, { area: guess.zone });
14152
+ run.area = guess.zone;
14153
+ deps.db.addEvent(
14154
+ run.id,
14155
+ "step",
14156
+ `\u0437\u043E\u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0440\u0435\u043F\u043E: ${guess.zone} (${Math.round(guess.confidence * 100)}% \u2014 ${guess.why}) \u2014 TASWM-348`
14157
+ );
14158
+ }
12562
14159
  async function trySpawnQueuedRun(deps, run) {
12563
14160
  if (deps.control?.paused) return "skipped";
12564
14161
  const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
@@ -12574,11 +14171,21 @@ async function trySpawnQueuedRun(deps, run) {
12574
14171
  if (!await exclusiveGateAllows(deps, run)) return "skipped";
12575
14172
  if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
12576
14173
  if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
14174
+ await fillSuggestedArea(deps, run);
12577
14175
  try {
12578
14176
  await deps.spawn(run);
12579
14177
  narrate(deps, `\u0411\u0435\u0440\u0443 ${run.task_slug} \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0441\u043F\u0430\u0432\u043D\u044E ${droneLabel(run)}.`);
12580
14178
  return "spawned";
12581
14179
  } catch (e) {
14180
+ if (e instanceof FileOverlapError) {
14181
+ const logged = deps.overlapLogged ??= /* @__PURE__ */ new Map();
14182
+ if (logged.get(run.id) !== e.message) {
14183
+ logged.set(run.id, e.message);
14184
+ deps.db.addEvent(run.id, "step", `${e.message} \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0441\u0442\u0430\u0451\u0442\u0441\u044F \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 (TASWM-348)`);
14185
+ narrate(deps, `${droneLabel(run)} \u2014 ${e.message}. \u041D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u043E.`);
14186
+ }
14187
+ return "skipped";
14188
+ }
12582
14189
  if (e instanceof AreaBusyError) return "skipped";
12583
14190
  deps.db.updateRun(run.id, { status: "failed" });
12584
14191
  deps.db.addEvent(run.id, "failed", String(e));
@@ -12605,8 +14212,8 @@ async function resumeWatchdogParked(deps, droneCap) {
12605
14212
  }
12606
14213
  if (!candidate) return;
12607
14214
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
12608
- const worktreeExists = deps.worktreeExists ?? existsSync13;
12609
- const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
14215
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
14216
+ const hasTranscript = deps.hasTranscript ?? executorHasSession;
12610
14217
  const wt = candidate.worktree_path;
12611
14218
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
12612
14219
  const mode = canContinue ? "continue" : "fresh";
@@ -12774,8 +14381,8 @@ async function manageRelief(deps, now, droneCap, stormActive) {
12774
14381
  if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
12775
14382
  if (!deps.respawn) return;
12776
14383
  const wt = resumeCandidate.worktree_path;
12777
- const worktreeExists = deps.worktreeExists ?? existsSync13;
12778
- const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
14384
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
14385
+ const hasTranscript = deps.hasTranscript ?? executorHasSession;
12779
14386
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
12780
14387
  const mode = canContinue ? "continue" : "fresh";
12781
14388
  if (mode === "continue" && wt && deps.reclaimWorktree) {
@@ -12898,15 +14505,28 @@ ${journal}`);
12898
14505
  deps.db.addEvent(run.id, "step", `\u0434\u0435\u043C\u043E\u043D-\u043E\u0442\u0447\u0451\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 ${run.task_slug} \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${e instanceof Error ? e.message : String(e)}`);
12899
14506
  });
12900
14507
  }
14508
+ function syncBranchFromPr(db, run, headBranch) {
14509
+ const fix = branchFromPrPatch(run.branch, headBranch);
14510
+ if (!fix) return run;
14511
+ const updated = db.updateRun(run.id, fix.patch) ?? run;
14512
+ db.addEvent(run.id, "step", fix.event);
14513
+ return updated;
14514
+ }
14515
+ async function worktreeBranchFor(run, prHead) {
14516
+ if (run.branch) return run.branch;
14517
+ const fromPr = run.pr_url && prHead ? await prHead(run.pr_url).catch(() => null) : null;
14518
+ return fromPr || run.task_slug;
14519
+ }
12901
14520
  async function adoptDroneToReview(deps, run, prUrl) {
12902
14521
  const t = applyReviewTransition(deps.db, run, prUrl);
14522
+ const adopted = syncBranchFromPr(deps.db, run, deps.prHeadBranch ? await deps.prHeadBranch(prUrl).catch(() => null) : null);
12903
14523
  deps.db.addEvent(run.id, "pr_opened", prUrl);
12904
14524
  deps.db.addEvent(
12905
14525
  run.id,
12906
14526
  "step",
12907
14527
  t.isFixIteration ? `\u0434\u0440\u043E\u043D \u043E\u0442\u043A\u0440\u044B\u043B PR \u043D\u0430 \u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0438 \u0438 \u0443\u043C\u0435\u0440 \u0434\u043E done() \u2014 \u0443\u0441\u044B\u043D\u043E\u0432\u043B\u0451\u043D \u043D\u0430 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E #${t.cycle} (\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB \u0441\u0431\u0440\u043E\u0448\u0435\u043D, pr_url \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0438\u0437 gh): ${prUrl}` : `\u0434\u0440\u043E\u043D \u043E\u0442\u043A\u0440\u044B\u043B PR \u043F\u0435\u0440\u0435\u0434 \u0441\u043C\u0435\u0440\u0442\u044C\u044E \u2014 \u0443\u0441\u044B\u043D\u043E\u0432\u043B\u0451\u043D \u043D\u0430 \u0440\u0435\u0432\u044C\u044E (pr_url \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0438\u0437 gh): ${prUrl}`
12908
14528
  );
12909
- await postAdoptionReport(deps, run, prUrl, t.cycle ?? run.review_cycle ?? 0);
14529
+ await postAdoptionReport(deps, adopted, prUrl, t.cycle ?? run.review_cycle ?? 0);
12910
14530
  }
12911
14531
  async function postScrapedVerdictReport(deps, runId, taskSlug, verdict, droneName, workspaceId, cycle) {
12912
14532
  if (!deps.reportComment) return;
@@ -12936,8 +14556,8 @@ function buildEscalationCandidates(db) {
12936
14556
  }
12937
14557
  async function tickOnce(deps) {
12938
14558
  const now = deps.now ? deps.now() : Date.now();
12939
- const worktreeExists = deps.worktreeExists ?? existsSync13;
12940
- const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
14559
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
14560
+ const hasTranscript = deps.hasTranscript ?? executorHasSession;
12941
14561
  if (deps.rehydrate) {
12942
14562
  const rh = deps.rehydrate;
12943
14563
  const sinceLast = rh.lastAt == null ? Infinity : now - rh.lastAt;
@@ -12988,6 +14608,18 @@ async function tickOnce(deps) {
12988
14608
  await dv.run();
12989
14609
  }
12990
14610
  }
14611
+ if (deps.repoMap) {
14612
+ const rm3 = deps.repoMap;
14613
+ const sinceLast = rm3.lastAt == null ? Infinity : now - rm3.lastAt;
14614
+ if (sinceLast >= rm3.intervalMs) {
14615
+ rm3.lastAt = now;
14616
+ try {
14617
+ await rm3.run();
14618
+ } catch (e) {
14619
+ console.error("[overmind] \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u043A\u0430\u0440\u0442\u044B \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:", e);
14620
+ }
14621
+ }
14622
+ }
12991
14623
  if (deps.rehydrate && deps.kill) {
12992
14624
  const kill = deps.kill;
12993
14625
  for (const run of deps.db.listRuns()) {
@@ -13013,6 +14645,19 @@ async function tickOnce(deps) {
13013
14645
  }
13014
14646
  }
13015
14647
  }
14648
+ if (deps.logRotate) {
14649
+ const lr = deps.logRotate;
14650
+ const sinceLast = lr.lastAt == null ? Infinity : now - lr.lastAt;
14651
+ if (sinceLast >= lr.intervalMs) {
14652
+ lr.lastAt = now;
14653
+ try {
14654
+ lr.run();
14655
+ } catch (e) {
14656
+ console.error("[overmind] \u0440\u043E\u0442\u0430\u0446\u0438\u044F \u043B\u043E\u0433\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e);
14657
+ }
14658
+ }
14659
+ }
14660
+ pruneGateLogs(deps);
13016
14661
  let stormActive = false;
13017
14662
  if (deps.storm) {
13018
14663
  for (const run of deps.db.listRuns("running")) {
@@ -13038,7 +14683,7 @@ async function tickOnce(deps) {
13038
14683
  if (!stormActive) {
13039
14684
  await resumeWatchdogParked(deps, droneCap);
13040
14685
  }
13041
- for (const run of deps.db.listRuns("review")) {
14686
+ for (let run of deps.db.listRuns("review")) {
13042
14687
  try {
13043
14688
  if (run.resolves_run_id != null) {
13044
14689
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
@@ -13057,7 +14702,9 @@ async function tickOnce(deps) {
13057
14702
  continue;
13058
14703
  }
13059
14704
  if (!run.pr_url) continue;
13060
- const st = await deps.prStatus(run.pr_url);
14705
+ const prUrl = run.pr_url;
14706
+ const st = await deps.prStatus(prUrl);
14707
+ run = syncBranchFromPr(deps.db, run, st.headBranch);
13061
14708
  if (st.merged) {
13062
14709
  await finalizeMergedReview(deps, run);
13063
14710
  narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${run.task_slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
@@ -13126,11 +14773,13 @@ async function tickOnce(deps) {
13126
14773
  continue;
13127
14774
  }
13128
14775
  const attempts = deps.db.reviewerAttemptCountFor(run.id);
14776
+ const runEvents = deps.db.events(run.id);
14777
+ const unrecoverableMark = reviewUnrecoverableMark(run.branch || run.task_slug);
13129
14778
  let verdictFromComment = null;
13130
14779
  let lastAttempt = null;
13131
14780
  if (attempts > 0 && deps.verdictComment) {
13132
14781
  lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
13133
- verdictFromComment = await deps.verdictComment(run.pr_url, lastAttempt?.created_at ?? run.created_at).catch(() => null);
14782
+ verdictFromComment = await deps.verdictComment(prUrl, lastAttempt?.created_at ?? run.created_at).catch(() => null);
13134
14783
  }
13135
14784
  if (verdictFromComment) {
13136
14785
  const mark = verdictCommentMark(verdictFromComment.createdAt);
@@ -13145,15 +14794,16 @@ async function tickOnce(deps) {
13145
14794
  await wakeAuthorOnChanges(deps, run, now, droneCap);
13146
14795
  }
13147
14796
  }
14797
+ } else if (runEvents.some((e) => (e.text ?? "").includes(unrecoverableMark))) {
13148
14798
  } else if (attempts >= REVIEW_SPAWN_LIMIT) {
13149
- if (!deps.db.events(run.id).some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
14799
+ if (!runEvents.some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
13150
14800
  deps.db.addEvent(run.id, "step", `${REVIEW_ESCALATION_MARK}: ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u0440\u0435\u0432\u044C\u044E \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
13151
14801
  narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F \u0437\u0430 ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`);
13152
14802
  }
13153
14803
  } else {
13154
14804
  const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
13155
14805
  if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk) {
13156
- const freshSt = await deps.prStatus(run.pr_url);
14806
+ const freshSt = await deps.prStatus(prUrl);
13157
14807
  if (freshSt.merged) {
13158
14808
  await finalizeMergedReview(deps, run);
13159
14809
  narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E, \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
@@ -13166,11 +14816,21 @@ async function tickOnce(deps) {
13166
14816
  } else if (freshSt.conflicting || freshSt.isDraft) {
13167
14817
  deps.db.addEvent(run.id, "step", "PR \u0441\u0442\u0430\u043B \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u044E\u0449\u0438\u043C/\u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u043E\u043C, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D \u0434\u043E \u0441\u043B\u0435\u0434. \u0442\u0438\u043A\u0430");
13168
14818
  } else {
14819
+ run = syncBranchFromPr(deps.db, run, freshSt.headBranch);
13169
14820
  try {
13170
14821
  await deps.spawnReviewer(run, st.files ?? []);
13171
14822
  narrate(deps, `PR ${droneLabel(run)} \u0433\u043E\u0442\u043E\u0432 \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430.`);
13172
14823
  } catch (e) {
13173
- deps.db.addEvent(run.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u043E\u0441\u044C: ${String(e)}`);
14824
+ if (isUnrecoverableRefError(e)) {
14825
+ deps.db.addEvent(
14826
+ run.id,
14827
+ "step",
14828
+ `${unrecoverableMark}: ${String(e)} \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`
14829
+ );
14830
+ narrate(deps, `\u0420\u0435\u0432\u044C\u044E\u0435\u0440\u0430 ${droneLabel(run)} \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u044C: \u0432\u0435\u0442\u043A\u0438 PR \u043D\u0435\u0442 \u043D\u0430 origin \u2014 \u0437\u043E\u0432\u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0441\u0440\u0430\u0437\u0443, \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u043D\u0435 \u0436\u0433\u0443.`);
14831
+ } else {
14832
+ deps.db.addEvent(run.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u043E\u0441\u044C: ${String(e)}`);
14833
+ }
13174
14834
  }
13175
14835
  }
13176
14836
  }
@@ -13437,7 +15097,9 @@ async function tickOnce(deps) {
13437
15097
  // resource-gate-stall (TASWM-212): та же причина, что видит лог manageRelief — один
13438
15098
  // deps.gateReason() на тик (кэш hostReading() внутри resources.ts, лишнего спавна нет).
13439
15099
  ...deps.gateReason ? { gateReason: deps.gateReason() } : {},
13440
- ...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {}
15100
+ ...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {},
15101
+ // Рестарт не переигрывает падения (TAS-485) — см. closedBeforeStart в anomalies.ts.
15102
+ ...az.startedAt != null ? { daemonStartedAt: az.startedAt } : {}
13441
15103
  }),
13442
15104
  ...buildEscalationCandidates(deps.db),
13443
15105
  // ЦЕНА РАЗГРУЗКИ (TASWM-316): «машина не тянет» с числами и рекомендацией — та же
@@ -13467,6 +15129,20 @@ async function tickOnce(deps) {
13467
15129
  }
13468
15130
  narrate(deps, `\u26A0\uFE0F ${signal.title}`);
13469
15131
  }
15132
+ const stale = az.tracker.pendingResolved();
15133
+ if (stale.length > 0) {
15134
+ if (!az.resolve) {
15135
+ az.tracker.forgetResolved(stale);
15136
+ } else {
15137
+ const batch = stale.slice(0, ANOMALY_RESOLVE_BATCH);
15138
+ try {
15139
+ await az.resolve(batch);
15140
+ az.tracker.forgetResolved(batch);
15141
+ } catch (e) {
15142
+ console.error("[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0433\u0430\u0448\u0435\u043D\u0438\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0432\u0448\u0438\u0445\u0441\u044F \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u0439 \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u043E, \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:", e);
15143
+ }
15144
+ }
15145
+ }
13470
15146
  }
13471
15147
  if (deps.watchdog) {
13472
15148
  try {
@@ -13532,7 +15208,7 @@ async function cancelRun(deps, slug, reason) {
13532
15208
  }
13533
15209
  async function resolveConflict(deps, reviewRun) {
13534
15210
  const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
13535
- const branch = reviewRun.branch || reviewRun.task_slug;
15211
+ const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
13536
15212
  const resolver = deps.db.createRun({
13537
15213
  task_slug: reviewRun.task_slug,
13538
15214
  repo,
@@ -13542,7 +15218,7 @@ async function resolveConflict(deps, reviewRun) {
13542
15218
  workspace_id: reviewRun.workspace_id
13543
15219
  // та же задача, тот же воркспейс (TASWM-288)
13544
15220
  });
13545
- const wt = join23(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
15221
+ const wt = join28(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
13546
15222
  try {
13547
15223
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
13548
15224
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -13572,14 +15248,14 @@ async function resolveConflict(deps, reviewRun) {
13572
15248
  await deps.git.removeWorktree(repo, wt).catch(() => {
13573
15249
  });
13574
15250
  deps.db.updateRun(resolver.id, { status: "failed" });
13575
- const marker = isTransientResourceError(e) ? "\u041E\u0421 \u043D\u0435 \u0434\u0430\u0451\u0442 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C git-\u043F\u0440\u043E\u0446\u0435\u0441\u0441\u044B (\u0440\u0435\u0441\u0443\u0440\u0441, \u043D\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442)" : "merge-main-in \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F";
15251
+ const marker = isUnrecoverableRefError(e) ? "\u0432\u0435\u0442\u043A\u0438 PR \u043D\u0435\u0442 \u043D\u0430 origin \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A" : isTransientResourceError(e) ? "\u041E\u0421 \u043D\u0435 \u0434\u0430\u0451\u0442 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C git-\u043F\u0440\u043E\u0446\u0435\u0441\u0441\u044B (\u0440\u0435\u0441\u0443\u0440\u0441, \u043D\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442)" : "merge-main-in \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F";
13576
15252
  deps.db.addEvent(resolver.id, "failed", `${marker}: ${String(e)}`);
13577
15253
  throw e;
13578
15254
  }
13579
15255
  }
13580
15256
  async function spawnReviewer(deps, reviewRun, files) {
13581
15257
  const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
13582
- const branch = reviewRun.branch || reviewRun.task_slug;
15258
+ const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
13583
15259
  const priorReview = reviewRun.review_cycle > 0 ? lastChangesSummary(deps.db, reviewRun.id) : null;
13584
15260
  const verifyMode = deps.verifyMode ? await deps.verifyMode(repo).catch(() => void 0) : void 0;
13585
15261
  const checklist = buildReviewChecklist(reviewRun, files, priorReview, verifyMode);
@@ -13594,7 +15270,7 @@ async function spawnReviewer(deps, reviewRun, files) {
13594
15270
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
13595
15271
  workspace_id: reviewRun.workspace_id
13596
15272
  });
13597
- const wt = join23(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
15273
+ const wt = join28(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
13598
15274
  try {
13599
15275
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
13600
15276
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -13629,7 +15305,7 @@ async function respawnRun(deps, run, mode, nudge) {
13629
15305
  ]);
13630
15306
  const base3 = effectiveBaseBranch(run.base_branch, projectBase);
13631
15307
  const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
13632
- if (!existsSync13(wt)) {
15308
+ if (!existsSync16(wt)) {
13633
15309
  const branch = runToSpawn.branch || runToSpawn.task_slug;
13634
15310
  const source = await addWorktree(repo, wt, branch, {
13635
15311
  ...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
@@ -13654,11 +15330,11 @@ async function respawnRun(deps, run, mode, nudge) {
13654
15330
  const prompt = notice ? `${freshPrompt}
13655
15331
 
13656
15332
  ${notice}` : freshPrompt;
13657
- const pid = deps.launch({ worktree: wt, run, prompt });
15333
+ const pid = deps.launch({ worktree: wt, run: runToSpawn, prompt });
13658
15334
  deps.db.updateRun(run.id, { status: "running", pid });
13659
15335
  }
13660
15336
  async function syncCrashedOnStartup(deps) {
13661
- const worktreeExists = deps.worktreeExists ?? existsSync13;
15337
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
13662
15338
  let orphaned = 0;
13663
15339
  let merged = 0;
13664
15340
  let requeued = 0;
@@ -13721,39 +15397,37 @@ async function makeStore(repo, worktreesDir, repoFlag) {
13721
15397
  console.warn(`[overmind] \u043D\u0435 \u0441\u043C\u043E\u0433 \u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u044C \u043F\u0443\u0442\u044C \u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044E (${e instanceof Error ? e.message : e}) \u2014 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0437\u0430\u043F\u0443\u0441\u043A \u0441\u043D\u043E\u0432\u0430 \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u0442 --repo`);
13722
15398
  }
13723
15399
  }
15400
+ const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW, homedir5(), repoPaths);
15401
+ const repoMap = emptyRepoMapState(scanRootsList);
13724
15402
  if (SWARM_BACKEND !== "client") {
13725
15403
  const { SwarmDb: SwarmDb2 } = await loadSqlite();
13726
- return { store: new SwarmDb2(DB_PATH, repo), reconciled: true, host };
15404
+ return { store: new SwarmDb2(DB_PATH, repo), reconciled: true, host, repoMap };
13727
15405
  }
13728
15406
  const call = resilientSwarmMcp({
13729
15407
  onDown: (ms) => console.warn(`[overmind] \u043F\u0440\u043E\u0434-MCP \u043C\u043E\u043B\u0447\u0438\u0442 ${Math.round(ms / 1e3)}\u0441 \u2014 \u0440\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432\u0441\u043B\u0435\u043F\u0443\u044E, \u0441\u0434\u0430\u0447\u0438 \u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u044B \u043D\u0435 \u0434\u043E\u0435\u0437\u0436\u0430\u044E\u0442.`),
13730
15408
  onUp: () => console.warn("[overmind] \u043F\u0440\u043E\u0434-MCP \u0441\u043D\u043E\u0432\u0430 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438.")
13731
15409
  });
13732
15410
  const explicitRegistry = await buildRepoRegistry(repoPaths);
13733
- const byName = new Map(explicitRegistry.map((e) => [e.repoFullName, e.path]));
13734
- const scanRootsList = defaultScanRoots(homedir3(), repoPaths);
13735
- const scanStartedAt = Date.now();
13736
- const scanned = scanRoots(scanRootsList, { maxDepth: SWARM_SCAN_DEPTH });
13737
- let addedByScan = 0;
13738
- for (const [full, path] of scanned) {
13739
- if (byName.has(full)) continue;
13740
- byName.set(full, path);
13741
- addedByScan++;
13742
- }
13743
- if (addedByScan > 0) {
13744
- console.log(
13745
- `[overmind] \u043D\u0430\u0448\u0451\u043B \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 ${addedByScan} \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439(\u0435\u0432) \u0441\u043A\u0430\u043D\u043E\u043C \u0437\u0430 ${Date.now() - scanStartedAt}\u043C\u0441: ${[...scanned.keys()].slice(0, 8).join(", ")}${scanned.size > 8 ? ` \u0438 \u0435\u0449\u0451 ${scanned.size - 8}` : ""}`
13746
- );
13747
- }
13748
- const repoRegistry = [...byName].map(([repoFullName, path]) => ({ repoFullName, path }));
13749
- if (repoPaths.length > 0 && repoRegistry.length < repoPaths.length) {
15411
+ if (repoPaths.length > 0 && explicitRegistry.length < repoPaths.length) {
13750
15412
  console.warn(
13751
- `[overmind] \u0440\u0435\u043F\u043E-\u0440\u0435\u0435\u0441\u0442\u0440: ${repoRegistry.length}/${repoPaths.length} \u043F\u0443\u0442\u0435\u0439 \u0440\u0435\u0437\u043E\u043B\u0432\u043D\u0443\u043B\u0438\u0441\u044C \u0432 repoFullName (\u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u0435 \u2014 \u043D\u0435 git-\u0440\u0435\u043F\u043E \u0438\u043B\u0438 \u0431\u0435\u0437 remote origin)`
15413
+ `[overmind] \u0440\u0435\u043F\u043E-\u0440\u0435\u0435\u0441\u0442\u0440: ${explicitRegistry.length}/${repoPaths.length} \u043F\u0443\u0442\u0435\u0439 \u0440\u0435\u0437\u043E\u043B\u0432\u043D\u0443\u043B\u0438\u0441\u044C \u0432 repoFullName (\u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u0435 \u2014 \u043D\u0435 git-\u0440\u0435\u043F\u043E \u0438\u043B\u0438 \u0431\u0435\u0437 remote origin)`
13752
15414
  );
13753
15415
  }
15416
+ const rebuildRegistry = () => refreshRepoMap({
15417
+ call,
15418
+ configured: explicitRegistry,
15419
+ roots: scanRootsList,
15420
+ maxDepth: SWARM_SCAN_DEPTH,
15421
+ cachePath: REPO_MAP_PATH,
15422
+ state: repoMap,
15423
+ log: (m) => console.log(m)
15424
+ });
15425
+ const repoRegistry = await rebuildRegistry();
13754
15426
  if (repoRegistry.length === 0) {
13755
15427
  console.warn(
13756
- `[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(", ")}. \u0415\u0441\u043B\u0438 \u043A\u043B\u043E\u043D\u044B \u043B\u0435\u0436\u0430\u0442 \u0432 \u0434\u0440\u0443\u0433\u043E\u043C \u043C\u0435\u0441\u0442\u0435 \u2014 \u0443\u043A\u0430\u0436\u0438: swarm start --repo <\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u043F\u043E> (\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u0441\u044F).`
15428
+ `[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: почему пусто — три РАЗНЫЕ причины, и лечатся они по-разному. Одна
15429
+ // общая фраза заставляла бы человека гадать, что из этого его случай.
15430
+ (repoMap.unknown ? "\u041F\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 \u043F\u0440\u043E\u0432\u0435\u0440\u044C `swarm login` \u0438 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0441\u0442\u044C \u0441\u0435\u0440\u0432\u0435\u0440\u0430. " : repoMap.missing.length ? `\u041D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0438\u0437 ${repoMap.missing.length} \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0440\u043E\u044F \u0442\u0443\u0442 \u043D\u0435\u0442. ` : "\u0423 \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. ") + "\u0415\u0441\u043B\u0438 \u043A\u043B\u043E\u043D\u044B \u043B\u0435\u0436\u0430\u0442 \u0432 \u0434\u0440\u0443\u0433\u043E\u043C \u043C\u0435\u0441\u0442\u0435 \u2014 \u0443\u043A\u0430\u0436\u0438: swarm start --repo <\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u043F\u043E> (\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u0441\u044F)."
13757
15431
  );
13758
15432
  }
13759
15433
  const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo, repoRegistry, host });
@@ -13768,7 +15442,17 @@ async function makeStore(repo, worktreesDir, repoFlag) {
13768
15442
  `overmind backend=client (\u0442\u043E\u043D\u043A\u0438\u0439 \u043A\u043B\u0438\u0435\u043D\u0442, outbox=${SWARM_OUTBOX}); \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F (ps \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D) \u2014 running/blocked \u043E\u0441\u0442\u0430\u044E\u0442\u0441\u044F \u043D\u0435\u0440\u0435\u043A\u043E\u043D\u0441\u0438\u043B\u0438\u0440\u043E\u0432\u0430\u043D\u044B, startup-sync (TASWM-128) \u0431\u0443\u0434\u0435\u0442 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D`
13769
15443
  );
13770
15444
  }
13771
- return { store: client, reconciled: rec.reconciled, host };
15445
+ return {
15446
+ store: client,
15447
+ reconciled: rec.reconciled,
15448
+ host,
15449
+ repoMap,
15450
+ // Периодическая пересборка (шаг 0a4 тика): применяем новый реестр к ЖИВОМУ клиенту —
15451
+ // и список для claim/heartbeat, и обратную карту owner/repo → путь.
15452
+ refreshRepoMap: async () => {
15453
+ client.setRepoRegistry(await rebuildRegistry());
15454
+ }
15455
+ };
13772
15456
  }
13773
15457
  var crashGuardsInstalled = false;
13774
15458
  function installCrashGuards() {
@@ -13787,22 +15471,22 @@ async function migrateWorktreesAtStartup() {
13787
15471
  legacyDir: WORKTREES_LEGACY_DIR,
13788
15472
  targetDir: WORKTREES_DIR,
13789
15473
  envOverride: process.env.SWARM_WORKTREES || "",
13790
- exists: (d) => existsSync13(d),
15474
+ exists: (d) => existsSync16(d),
13791
15475
  isNonEmptyDir: (d) => {
13792
15476
  try {
13793
- return readdirSync10(d).length > 0;
15477
+ return readdirSync12(d).length > 0;
13794
15478
  } catch {
13795
15479
  return false;
13796
15480
  }
13797
15481
  },
13798
15482
  liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
13799
- rename: (from, to) => renameSync8(from, to),
15483
+ rename: (from, to) => renameSync10(from, to),
13800
15484
  repairMoved: (t) => repairMovedWorktrees(t),
13801
15485
  log: (m) => console.log(m)
13802
15486
  });
13803
15487
  } catch (e) {
13804
15488
  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);
13805
- return existsSync13(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
15489
+ return existsSync16(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
13806
15490
  }
13807
15491
  }
13808
15492
  async function reportWorkspaceAccess(tl, db) {
@@ -13820,13 +15504,22 @@ async function reportWorkspaceAccess(tl, db) {
13820
15504
  async function runDaemon(opts) {
13821
15505
  installCrashGuards();
13822
15506
  const daemonStart = await captureDaemonStart(THIS_FILE);
15507
+ const latestRelease = fetchLatestVersion();
13823
15508
  const worktreesDir = await migrateWorktreesAtStartup();
13824
15509
  if (!opts.repo) {
13825
15510
  const remembered = effectiveRepoPaths("", loadRememberedRepos(SWARM_HOME))[0];
13826
15511
  if (remembered) opts.repo = remembered;
13827
15512
  }
13828
- const { store: db, reconciled, host } = await makeStore(opts.repo, worktreesDir, opts.repoFlag);
15513
+ const {
15514
+ store: db,
15515
+ reconciled,
15516
+ host,
15517
+ repoMap: repoMapState,
15518
+ refreshRepoMap: rebuildRepoMap
15519
+ } = await makeStore(opts.repo, worktreesDir, opts.repoFlag);
13829
15520
  await preflightAuth();
15521
+ const outdated = outdatedLine(PACKAGE_VERSION, await latestRelease);
15522
+ if (outdated) console.warn(`[overmind] ${outdated}`);
13830
15523
  const tl = await connectTaskless();
13831
15524
  const tlFor = (workspaceId) => tl ? tl.forWorkspace(workspaceId) : null;
13832
15525
  if (tl) await reportWorkspaceAccess(tl, db);
@@ -13863,18 +15556,23 @@ async function runDaemon(opts) {
13863
15556
  overmindEntry: THIS_FILE,
13864
15557
  dbPath: DB_PATH,
13865
15558
  runLogsDir: RUN_LOGS_DIR,
15559
+ // Кем исполнять (TASWM-349) — настройка SWARM_EXECUTOR, один резолв на весь демон.
15560
+ executor: EXECUTOR,
13866
15561
  droneIsolation: DRONE_ISOLATION,
13867
15562
  channel: droneChannelLaunch,
13868
15563
  droneCap: () => lastDroneCap
13869
15564
  });
13870
15565
  let prepareReader;
15566
+ const zoneModels = new ZoneModels({ manualAreas: () => new Map(db.listRuns().map((r) => [r.task_slug, r.area])) });
15567
+ const activeFiles = new ActiveFilesCache((r) => changedPaths(r.worktree_path, { baseBranch: r.base_branch ?? null }));
13871
15568
  const baseSpawn = {
13872
15569
  db,
13873
15570
  repo: opts.repo,
13874
15571
  // фолбэк, если у прогона repo пуст
13875
15572
  worktreesDir,
13876
15573
  launch,
13877
- prepare: (repo) => prepareReader ? prepareReader(repo) : Promise.resolve(null)
15574
+ prepare: (repo) => prepareReader ? prepareReader(repo) : Promise.resolve(null),
15575
+ conflicts: (run, repo) => zoneConflictFor(db, zoneModels, activeFiles, run, repo, opts.repo)
13878
15576
  };
13879
15577
  let intake;
13880
15578
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_MAX_IN_FLIGHT > 0) {
@@ -13886,6 +15584,10 @@ async function runDaemon(opts) {
13886
15584
  const client = db;
13887
15585
  rehydrate = { run: () => client.hydrate(), ready: () => client.pendingCount() === 0, intervalMs: SWARM_REHYDRATE_MS };
13888
15586
  }
15587
+ let repoMap;
15588
+ if (rebuildRepoMap && SWARM_REPO_MAP_MS > 0) {
15589
+ repoMap = { run: rebuildRepoMap, intervalMs: SWARM_REPO_MAP_MS, lastAt: Date.now() };
15590
+ }
13889
15591
  let daemonVersion;
13890
15592
  if (daemonStart && SWARM_VERSION_CHECK_MS > 0) {
13891
15593
  let cached = null;
@@ -13904,7 +15606,7 @@ async function runDaemon(opts) {
13904
15606
  const client = db;
13905
15607
  heartbeat = {
13906
15608
  push: async () => {
13907
- const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null);
15609
+ const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastDroneCap);
13908
15610
  if (reply.commands.length) onHostCommands(reply.commands);
13909
15611
  if (reply.logRequests.length) {
13910
15612
  await serveLogRequests(
@@ -13974,7 +15676,14 @@ async function runDaemon(opts) {
13974
15676
  autoReviewEnabled: SWARM_AUTO_REVIEW,
13975
15677
  outboxPendingCount: () => client.pendingCount(),
13976
15678
  notify: (signal) => client.notifyAnomaly(signal),
13977
- stopQuiet: () => anomalyState.stopQuiet()
15679
+ // TAS-485: условие ушло → гасим свою же тревогу в инбоксе.
15680
+ resolve: (keys) => client.resolveAnomalies(keys),
15681
+ stopQuiet: () => anomalyState.stopQuiet(),
15682
+ // TAS-485: этот демон отвечает за то, что случилось ПРИ НЁМ. Прогон, закрытый до
15683
+ // старта, он не переоткрывает — иначе каждый перезапуск начинает волну заново
15684
+ // (id прогонов в зеркале свои у каждой жизни процесса, durable-кулдаун по ним не
15685
+ // совпадает — см. closedBeforeStart в anomalies.ts).
15686
+ startedAt: Date.now()
13978
15687
  };
13979
15688
  }
13980
15689
  const watchdog = {
@@ -14047,6 +15756,9 @@ async function runDaemon(opts) {
14047
15756
  worktreesDir,
14048
15757
  launch,
14049
15758
  git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
15759
+ // TASWM-356: ветка ревьюера — фактическая ветка PR. Штатно она уже в поле прогона
15760
+ // (реконсайл синхронит её из headBranch), этот ридер — для прогонов без неё.
15761
+ prHeadBranch,
14050
15762
  // TASWM-293: ТОТ ЖЕ живой ридер verifyMode, что у исполнителя (spawnDrone) —
14051
15763
  // иначе ревьюер судит проект с verifyMode=local по правилам ci и блокирует PR
14052
15764
  // за отсутствие CI, которого у проекта нет и не предполагается.
@@ -14117,8 +15829,9 @@ async function runDaemon(opts) {
14117
15829
  baseBranchReader ? baseBranchReader(repoOfRun) : Promise.resolve("")
14118
15830
  ]);
14119
15831
  const task = taskClient ? requireTaskBody(rawTask, run.task_slug, run.workspace_id) : null;
14120
- const base3 = effectiveBaseBranch(run.base_branch, projectBase);
14121
- const runToSpawn = base3 && run.base_branch !== base3 ? db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
15832
+ const healed = healTaskSlug(db, run, task);
15833
+ const base3 = effectiveBaseBranch(healed.base_branch, projectBase);
15834
+ const runToSpawn = base3 && healed.base_branch !== base3 ? db.updateRun(healed.id, { base_branch: base3 }) ?? healed : healed;
14122
15835
  const taskContext = task && taskClient ? await fetchTaskContext(taskClient, task) : void 0;
14123
15836
  await spawnDrone(
14124
15837
  {
@@ -14136,9 +15849,9 @@ async function runDaemon(opts) {
14136
15849
  runToSpawn
14137
15850
  );
14138
15851
  if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
14139
- if (taskClient) await taskClient.startTask(run.task_slug);
15852
+ if (taskClient) await taskClient.startTask(runToSpawn.task_slug);
14140
15853
  },
14141
- prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state })),
15854
+ prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state, headBranch: s.headBranch })),
14142
15855
  // Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
14143
15856
  // (фолбэк — дефолт-репо роя). Реальный gh; логика усыновления — в tickOnce (тестируется).
14144
15857
  openPrsForBranch: (branch, repoDir) => openPrsForBranch(branch, repoDir || opts.repo),
@@ -14162,6 +15875,8 @@ async function runDaemon(opts) {
14162
15875
  reportComment: (slug, body, label, ws) => tl.forWorkspace(ws).addComment(slug, body, label)
14163
15876
  } : {},
14164
15877
  prCommitTitles: (prUrl) => prCommitTitles(prUrl),
15878
+ prHeadBranch: (prUrl) => prHeadBranch(prUrl),
15879
+ // TASWM-356: ветка PR на усыновлении, узким запросом
14165
15880
  journalForBranch: (repo, branch, slug) => journalForBranch(repo || opts.repo, branch, slug),
14166
15881
  removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p),
14167
15882
  // TASWM-303: тот же фолбэк repo, что у removeWorktree/journalForBranch — run.repo пуст
@@ -14171,15 +15886,17 @@ async function runDaemon(opts) {
14171
15886
  repoHasCi: (run) => repoHasCiWorkflows(run.worktree_path, run.repo || opts.repo),
14172
15887
  isAlive: pidAlive,
14173
15888
  // Учёт cost/usage (TASWM-188): реальный аккаунтант поверх RUN_LOGS_DIR + db.
14174
- accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db }),
15889
+ accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR }),
14175
15890
  // Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
14176
15891
  // (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
14177
- exitOutcome: makeExitClassifier(RUN_LOGS_DIR),
15892
+ exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
14178
15893
  // Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
14179
15894
  livePpids,
14180
15895
  // Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
14181
15896
  // POST /swarm/kill → stopped в проде → overmind гасит живой pid на реконсайле).
14182
- kill: (pid) => killProcessGroup(pid, "SIGTERM"),
15897
+ // TASWM-349: свой способ остановки — за адаптером; не задан (оба сегодняшних) → общий
15898
+ // групповой SIGTERM.
15899
+ kill: (pid) => EXECUTOR.stop ? EXECUTOR.stop(pid, "SIGTERM") : killProcessGroup(pid, "SIGTERM"),
14183
15900
  // Реклейм worktree паузнутого/возвращаемого дрона (TASWM-228): тот же cwd-скан, что и
14184
15901
  // очистка перед сносом worktree, но БЕЗ сноса каталога — worktree остаётся под
14185
15902
  // --continue. Гасит тяжёлую работу дрона, ушедшую из его process group (Bash-тул
@@ -14218,7 +15935,7 @@ async function runDaemon(opts) {
14218
15935
  lastCommitMs: (p) => worktreeLastCommitMs(p),
14219
15936
  // mtime транскрипта claude-сессии как liveness (TASWM-136): точнее коммитов — обновляется
14220
15937
  // на каждое действие дрона (Edit/Read/tool-use), не только на коммит.
14221
- transcriptMtimeMs: (p) => transcriptMtimeMs(p),
15938
+ transcriptMtimeMs: (p) => EXECUTOR.session.mtimeMs(p),
14222
15939
  // Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
14223
15940
  resourcesOk: hostResourcesOk,
14224
15941
  // Ресурс-гейт v2 (TASWM-151): жёсткий порог активной разгрузки — те же сырые показания
@@ -14249,6 +15966,29 @@ async function runDaemon(opts) {
14249
15966
  ...exclusiveProject ? { exclusiveProject } : {},
14250
15967
  repoFallback: opts.repo,
14251
15968
  exclusiveLogged: /* @__PURE__ */ new Map(),
15969
+ // Зона задачи из истории репо (TASWM-348) — предлагаем сами, если постановщик
15970
+ // её не проставил. Дедуп событий отдельной мапой, как у гейтов выше.
15971
+ suggestArea: async (run) => {
15972
+ const repo = normalizeRepo(run.repo || opts.repo || "");
15973
+ return repo ? suggestAreaForRun(await zoneModels.forRepo(repo), run) : null;
15974
+ },
15975
+ overlapLogged: /* @__PURE__ */ new Map(),
15976
+ // Ротация своего лога (TASWM-330): демон пишет в него неделями, а до этого ротация была
15977
+ // только при старте. Итог ротации печатаем в сам лог — первой строкой нового поколения
15978
+ // видно, что история не потерялась, а уехала в `.1`.
15979
+ logRotate: {
15980
+ run: () => {
15981
+ if (!stdoutGoesTo(DAEMON_LOG_PATH)) return;
15982
+ const outcome = rotateDaemonLog(DAEMON_LOG_PATH, { maxBytes: DAEMON_LOG_MAX_BYTES, keep: DAEMON_LOG_KEEP });
15983
+ if (outcome === "rotated") {
15984
+ console.log(
15985
+ `[overmind] \u043B\u043E\u0433 \u0434\u043E\u0440\u043E\u0441 \u0434\u043E ${Math.round(DAEMON_LOG_MAX_BYTES / 1024 / 1024)} \u041C\u0411 \u2014 \u043F\u0440\u043E\u0448\u043B\u043E\u0435 \u043F\u043E\u043A\u043E\u043B\u0435\u043D\u0438\u0435 \u0432 ${DAEMON_LOG_PATH}.1 (\u0434\u0435\u0440\u0436\u0438\u043C ${DAEMON_LOG_KEEP}; \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u2014 SWARM_DAEMON_LOG_MAX_BYTES)`
15986
+ );
15987
+ }
15988
+ },
15989
+ intervalMs: DAEMON_LOG_CHECK_MS,
15990
+ lastAt: null
15991
+ },
14252
15992
  // merge-main-in: review-PR конфликтует с main → влить main в ветку PR, не переделывая
14253
15993
  // фичу. Реальные git-примитивы + launch; логика — в resolveConflict (тестируется).
14254
15994
  resolveConflict: (reviewRun) => resolveConflict(
@@ -14257,7 +15997,9 @@ async function runDaemon(opts) {
14257
15997
  repoFallback: opts.repo,
14258
15998
  worktreesDir,
14259
15999
  launch,
14260
- git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree }
16000
+ git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree },
16001
+ prHeadBranch
16002
+ // TASWM-356: ветка PR для прогонов без записанного поля branch
14261
16003
  },
14262
16004
  reviewRun
14263
16005
  ).then(() => {
@@ -14288,8 +16030,8 @@ async function runDaemon(opts) {
14288
16030
  mode,
14289
16031
  nudge
14290
16032
  ),
14291
- worktreeExists: (p) => existsSync13(p),
14292
- hasTranscript: (p) => sessionTranscriptExists(p),
16033
+ worktreeExists: (p) => existsSync16(p),
16034
+ hasTranscript: executorHasSession,
14293
16035
  // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
14294
16036
  // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
14295
16037
  // строится над failing/failingRunIds ИЗ checks — без повторного `gh pr checks`.
@@ -14300,7 +16042,7 @@ async function runDaemon(opts) {
14300
16042
  // Штормовой режим (TASWM-23): health-probe на отбой (реальный claude) + внешний
14301
16043
  // сигнал SWARM_STORM_URL (2-й слой, если задан). Логика/пороги — в manageStorm/storm.ts.
14302
16044
  storm: {
14303
- probe: () => anthropicHealthy({ timeoutMs: resolveStormConfig().probeTimeoutMs }),
16045
+ probe: () => executorHealthy({ executor: EXECUTOR, timeoutMs: resolveStormConfig().probeTimeoutMs }),
14304
16046
  // externalDown опционален (exactOptionalPropertyTypes): ключ есть только при
14305
16047
  // заданном SWARM_STORM_URL, иначе опускаем — не передаём undefined.
14306
16048
  ...SWARM_STORM_URL ? { externalDown: () => fetchExternalStorm(SWARM_STORM_URL) } : {}
@@ -14316,6 +16058,8 @@ async function runDaemon(opts) {
14316
16058
  ...heartbeat ? { heartbeat } : {},
14317
16059
  // Сверка кода демона с main (TASWM-251): ключ есть только когда entryFile в git-репо.
14318
16060
  ...daemonVersion ? { daemonVersion } : {},
16061
+ // Карта репозиториев машины (TASWM-336): ключ есть только в client-режиме с непустым TTL.
16062
+ ...repoMap ? { repoMap } : {},
14319
16063
  // Встроенный надзор (client-режим, TASWM-194): ключ есть только когда сконфигурен.
14320
16064
  ...anomalies ? { anomalies } : {},
14321
16065
  // Сторож прогресса роя (TASWM-270): в отличие от anomalies выше — заведён БЕЗУСЛОВНО
@@ -14444,6 +16188,17 @@ async function runDaemon(opts) {
14444
16188
  );
14445
16189
  return run ? { id: run.id } : null;
14446
16190
  },
16191
+ // TASWM-351: «перезапустить с того же места» из продукта. Тот же вход, что у
16192
+ // `swarm wake <id>` и у POST /wake локальной морды, — сознательно, чтобы у
16193
+ // побудки была ОДНА реализация: она умеет отличать «сессию можно продолжить»
16194
+ // от «продолжать нечем» и не будит служебные прогоны вслепую (см. wakeRun).
16195
+ // Слаг → прогон резолвим тем же activeRunForSlug, что и остановка: у команды
16196
+ // из веба нет и не будет локального id прогона (он живёт только в этом процессе).
16197
+ wakeRun: async (slug) => {
16198
+ const run = db.activeRunForSlug(slug);
16199
+ if (!run) return null;
16200
+ return wakeRun({ db, isAlive: pidAlive, respawn: deps.respawn, narrate: narrator.narrate }, run.id);
16201
+ },
14447
16202
  update: async (onStep) => {
14448
16203
  if (!daemonStart) {
14449
16204
  return { ok: false, step: "fetch", message: "\u043A\u043E\u0434 \u0434\u0435\u043C\u043E\u043D\u0430 \u043B\u0435\u0436\u0438\u0442 \u043D\u0435 \u0432 git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E" };
@@ -14529,6 +16284,10 @@ async function runDaemon(opts) {
14529
16284
  // Устаревший код демона (TASWM-251): тот же кэш, что читает heartbeat.push выше —
14530
16285
  // локальный дашборд :7420 видит ту же сверку, что уходит в прод, без отдельного git-похода.
14531
16286
  daemonVersion: () => daemonVersion?.get() ?? null,
16287
+ // Карта репозиториев машины (TASWM-336): то же состояние, что мутирует шаг тика, — его
16288
+ // печатает `swarm status`. Без него человек снова гадал бы, какие репозитории машина
16289
+ // считает своими и в каком дереве работает при двух копиях одного клона.
16290
+ repoMap: () => repoMapRows(repoMapState),
14532
16291
  // Оператор-пауза + «Оживить» (TASWM-214 Фаза 2): тот же shared-флаг, что читает тик;
14533
16292
  // narrate — операторские действия видны в ленте роя; revive — сброс залипшего шторма.
14534
16293
  control,
@@ -14651,6 +16410,7 @@ export {
14651
16410
  REVIEW_CYCLE_LIMIT,
14652
16411
  REVIEW_ESCALATION_MARK,
14653
16412
  REVIEW_SPAWN_LIMIT,
16413
+ REVIEW_UNRECOVERABLE_MARK,
14654
16414
  SELF_REFERENCE_MARK,
14655
16415
  STALL_FLOOR_MS,
14656
16416
  STALL_MS,
@@ -14673,6 +16433,7 @@ export {
14673
16433
  repoHasCiWorkflows,
14674
16434
  resolveConflict,
14675
16435
  respawnRun,
16436
+ reviewUnrecoverableMark,
14676
16437
  reviveStuck,
14677
16438
  runDaemon,
14678
16439
  spawnReviewer,