@taskless-app/swarm 0.5.0 → 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 +38 -0
  2. package/dist/cli.js +1427 -307
  3. package/dist/overmind.js +1912 -548
  4. package/package.json +1 -1
package/dist/overmind.js CHANGED
@@ -720,6 +720,14 @@ var init_hosts = __esm({
720
720
  }
721
721
  });
722
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
+
723
731
  // ../swarm-core/src/hostCommands.ts
724
732
  function isHostCommandKind(v) {
725
733
  return typeof v === "string" && HOST_COMMAND_KINDS.includes(v);
@@ -729,17 +737,20 @@ function isHostCommandExpired(expiresAt, nowMs) {
729
737
  const at = Date.parse(expiresAt);
730
738
  return Number.isFinite(at) && at <= nowMs;
731
739
  }
740
+ function isRunScopedCommand(kind) {
741
+ return RUN_SCOPED.has(kind);
742
+ }
732
743
  function hostCommandLabel(cmd) {
733
744
  const base3 = HOST_COMMAND_LABELS[cmd.kind];
734
745
  if (cmd.kind === "set_max_drones" && cmd.maxDrones != null) return `${base3} \u2192 ${cmd.maxDrones}`;
735
- if (cmd.kind === "stop_run" && cmd.runSlug) return `${base3} ${cmd.runSlug}`;
746
+ if (isRunScopedCommand(cmd.kind) && cmd.runSlug) return `${base3} ${cmd.runSlug}`;
736
747
  return base3;
737
748
  }
738
- 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;
739
750
  var init_hostCommands = __esm({
740
751
  "../swarm-core/src/hostCommands.ts"() {
741
752
  "use strict";
742
- 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"];
743
754
  HOST_COMMAND_STATUSES = ["pending", "delivered", "running", "done", "failed", "rejected", "expired"];
744
755
  TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "rejected", "expired"]);
745
756
  HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
@@ -758,8 +769,11 @@ var init_hostCommands = __esm({
758
769
  restart: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A",
759
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",
760
771
  set_max_drones: "\u0441\u043C\u0435\u043D\u0430 \u0447\u0438\u0441\u043B\u0430 \u0434\u0440\u043E\u043D\u043E\u0432",
761
- 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"
762
774
  };
775
+ RUN_SCOPED_COMMAND_KINDS = ["stop_run", "wake_run"];
776
+ RUN_SCOPED = new Set(RUN_SCOPED_COMMAND_KINDS);
763
777
  MAX_DRONES_LIMIT = 8;
764
778
  }
765
779
  });
@@ -774,6 +788,436 @@ var init_relief = __esm({
774
788
  }
775
789
  });
776
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
+
777
1221
  // ../swarm-core/src/index.ts
778
1222
  var init_src = __esm({
779
1223
  "../swarm-core/src/index.ts"() {
@@ -790,8 +1234,483 @@ var init_src = __esm({
790
1234
  init_names();
791
1235
  init_baseBranch();
792
1236
  init_hosts();
1237
+ init_hostPool();
793
1238
  init_hostCommands();
794
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
+ };
795
1714
  }
796
1715
  });
797
1716
 
@@ -811,6 +1730,7 @@ __export(config_exports, {
811
1730
  DRONE_HOME: () => DRONE_HOME,
812
1731
  DRONE_HOME_ISOLATION: () => DRONE_HOME_ISOLATION,
813
1732
  DRONE_ISOLATION: () => DRONE_ISOLATION,
1733
+ EXECUTOR: () => EXECUTOR,
814
1734
  HEARTBEAT_PATH: () => HEARTBEAT_PATH,
815
1735
  HEARTBEAT_STALE_MS: () => HEARTBEAT_STALE_MS,
816
1736
  REPO_MAP_PATH: () => REPO_MAP_PATH,
@@ -880,9 +1800,8 @@ __export(config_exports, {
880
1800
  resolveEffectiveWorktreesDir: () => resolveEffectiveWorktreesDir,
881
1801
  reviewCutoffAt: () => reviewCutoffAt
882
1802
  });
883
- import { homedir } from "os";
884
- import { basename, join as join2, resolve } from "path";
885
- 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";
886
1805
  function normalizeRepo(repo) {
887
1806
  const r = (repo ?? "").trim();
888
1807
  if (!r) return "";
@@ -893,7 +1812,7 @@ function normalizeRepo(repo) {
893
1812
  }
894
1813
  }
895
1814
  function droneWorktreePath(worktreesDir, repo, slug) {
896
- return join2(worktreesDir, basename(normalizeRepo(repo)), slug);
1815
+ return join5(worktreesDir, basename(normalizeRepo(repo)), slug);
897
1816
  }
898
1817
  function resolveEffectiveWorktreesDir(deps) {
899
1818
  if (deps.envOverride) return deps.envOverride;
@@ -935,7 +1854,7 @@ function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
935
1854
  }
936
1855
  function tokenFromFile() {
937
1856
  try {
938
- return readFileSync2(join2(SWARM_HOME, "token"), "utf8").trim();
1857
+ return readFileSync2(join5(SWARM_HOME, "token"), "utf8").trim();
939
1858
  } catch {
940
1859
  return "";
941
1860
  }
@@ -949,34 +1868,36 @@ function reviewCutoffAt() {
949
1868
  const at = stamp();
950
1869
  try {
951
1870
  const tmp = `${REVIEW_CUTOFF_PATH}.tmp`;
952
- writeFileSync2(tmp, JSON.stringify({ at }));
1871
+ writeFileSync3(tmp, JSON.stringify({ at }));
953
1872
  renameSync2(tmp, REVIEW_CUTOFF_PATH);
954
1873
  } catch {
955
1874
  }
956
1875
  return at;
957
1876
  }
958
- 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, 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;
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;
959
1878
  var init_config = __esm({
960
1879
  "src/config.ts"() {
961
1880
  "use strict";
962
1881
  init_src();
963
- SWARM_HOME = process.env.SWARM_HOME || join2(homedir(), ".taskless-swarm");
964
- mkdirSync(SWARM_HOME, { recursive: true });
965
- DB_PATH = process.env.SWARM_DB || join2(SWARM_HOME, "swarm.db");
966
- 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");
967
1888
  DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
968
1889
  DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
969
- WORKTREES_NOINDEX_DIR = join2(SWARM_HOME, "worktrees.noindex");
970
- WORKTREES_LEGACY_DIR = join2(SWARM_HOME, "worktrees");
1890
+ WORKTREES_NOINDEX_DIR = join5(SWARM_HOME, "worktrees.noindex");
1891
+ WORKTREES_LEGACY_DIR = join5(SWARM_HOME, "worktrees");
971
1892
  WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
972
- RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join2(SWARM_HOME, "run-logs");
973
- 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");
974
1895
  DAEMON_LOG_MAX_BYTES = envNum("SWARM_DAEMON_LOG_MAX_BYTES", 16 * 1024 * 1024);
975
1896
  DAEMON_LOG_KEEP = envNum("SWARM_DAEMON_LOG_KEEP", 2);
976
1897
  DAEMON_LOG_CHECK_MS = envNum("SWARM_DAEMON_LOG_CHECK_MS", 6e4);
977
1898
  DIST_BUILD = true;
978
1899
  SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
979
- SWARM_OUTBOX = process.env.SWARM_OUTBOX || join2(SWARM_HOME, "outbox.jsonl");
1900
+ SWARM_OUTBOX = process.env.SWARM_OUTBOX || join5(SWARM_HOME, "outbox.jsonl");
980
1901
  SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
981
1902
  SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
982
1903
  SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
@@ -1001,15 +1922,15 @@ var init_config = __esm({
1001
1922
  DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
1002
1923
  DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
1003
1924
  DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
1004
- 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");
1005
1926
  DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
1006
- HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join2(SWARM_HOME, "overmind.heartbeat");
1927
+ HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join5(SWARM_HOME, "overmind.heartbeat");
1007
1928
  HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
1008
1929
  SWARM_REPO = process.env.SWARM_REPO || "";
1009
1930
  SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
1010
1931
  SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
1011
1932
  SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
1012
- REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join2(SWARM_HOME, "repo-map.json");
1933
+ REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join5(SWARM_HOME, "repo-map.json");
1013
1934
  SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
1014
1935
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
1015
1936
  SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
@@ -1020,7 +1941,7 @@ var init_config = __esm({
1020
1941
  TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
1021
1942
  SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
1022
1943
  SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
1023
- 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");
1024
1945
  SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
1025
1946
  SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
1026
1947
  WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
@@ -1377,10 +2298,10 @@ var init_storm = __esm({
1377
2298
  });
1378
2299
 
1379
2300
  // src/heartbeat.ts
1380
- 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";
1381
2302
  function writeHeartbeat(path, now) {
1382
2303
  const tmp = `${path}.tmp`;
1383
- writeFileSync4(tmp, String(now));
2304
+ writeFileSync5(tmp, String(now));
1384
2305
  renameSync3(tmp, path);
1385
2306
  }
1386
2307
  function readHeartbeat(path) {
@@ -1721,16 +2642,21 @@ var init_procCleanup = __esm({
1721
2642
  });
1722
2643
 
1723
2644
  // src/git.ts
1724
- import { existsSync as existsSync8, readdirSync as readdirSync8, statSync as statSync6 } from "fs";
2645
+ import { existsSync as existsSync9, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
1725
2646
  import { appendFile, mkdir, readFile, rm } from "fs/promises";
1726
- import { isAbsolute as isAbsolute2, join as join12 } from "path";
2647
+ import { isAbsolute as isAbsolute2, join as join15 } from "path";
1727
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
+ }
1728
2654
  async function reclaimStaleWorktree(repo, worktree) {
1729
2655
  await detachWorktree(repo, worktree);
1730
2656
  }
1731
2657
  function isLinkedWorktree(dir) {
1732
2658
  try {
1733
- return statSync6(join12(dir, ".git")).isFile();
2659
+ return statSync7(join15(dir, ".git")).isFile();
1734
2660
  } catch {
1735
2661
  return false;
1736
2662
  }
@@ -1742,7 +2668,7 @@ async function detachWorktree(repo, worktree) {
1742
2668
  }
1743
2669
  }
1744
2670
  async function ensureWorktreeDirGone(worktree) {
1745
- if (!existsSync8(worktree)) return false;
2671
+ if (!existsSync9(worktree)) return false;
1746
2672
  if (!isLinkedWorktree(worktree)) {
1747
2673
  console.error(
1748
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.`
@@ -1757,7 +2683,7 @@ async function ensureWorktreeDirGone(worktree) {
1757
2683
  );
1758
2684
  return true;
1759
2685
  }
1760
- if (existsSync8(worktree)) {
2686
+ if (existsSync9(worktree)) {
1761
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.`);
1762
2688
  }
1763
2689
  return true;
@@ -1800,7 +2726,7 @@ async function addWorktree(repo, path, branch, opts = {}) {
1800
2726
  }
1801
2727
  const busyAt = await worktreeForBranch(repo, branch);
1802
2728
  if (busyAt) {
1803
- const exists = existsSync8(busyAt);
2729
+ const exists = existsSync9(busyAt);
1804
2730
  const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
1805
2731
  if (live) throw new WorktreeBusyError(branch, busyAt);
1806
2732
  if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
@@ -1836,9 +2762,9 @@ async function ensureGitExcluded(worktree, pattern) {
1836
2762
  const out = await $2({ reject: false })`git -C ${worktree} rev-parse --git-common-dir`;
1837
2763
  const raw = out.stdout.trim();
1838
2764
  if (out.exitCode !== 0 || !raw) return false;
1839
- const commonDir = isAbsolute2(raw) ? raw : join12(worktree, raw);
1840
- const infoDir = join12(commonDir, "info");
1841
- const excludeFile = join12(infoDir, "exclude");
2765
+ const commonDir = isAbsolute2(raw) ? raw : join15(worktree, raw);
2766
+ const infoDir = join15(commonDir, "info");
2767
+ const excludeFile = join15(infoDir, "exclude");
1842
2768
  let current = "";
1843
2769
  try {
1844
2770
  current = await readFile(excludeFile, "utf8");
@@ -1871,7 +2797,7 @@ async function worktreeDirty(path) {
1871
2797
  async function repairMovedWorktrees(targetDir) {
1872
2798
  const subdirs = (dir) => {
1873
2799
  try {
1874
- return readdirSync8(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join12(dir, d.name));
2800
+ return readdirSync10(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join15(dir, d.name));
1875
2801
  } catch {
1876
2802
  return [];
1877
2803
  }
@@ -1915,7 +2841,9 @@ async function journalForBranch(repo, branch, slug) {
1915
2841
  }
1916
2842
  async function prepareDetachedWorktree(repo, branch, worktree) {
1917
2843
  await $2`git -C ${repo} fetch --quiet origin`;
1918
- 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}`;
1919
2847
  }
1920
2848
  async function mergeRef(worktree, ref) {
1921
2849
  const before = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
@@ -1948,7 +2876,7 @@ async function worktreeLastCommitMs(worktree) {
1948
2876
  const sec = Number(out);
1949
2877
  return Number.isInteger(sec) && sec > 0 ? sec * 1e3 : null;
1950
2878
  }
1951
- var WorktreeBusyError, GITHUB_REMOTE_RE;
2879
+ var WorktreeBusyError, MissingRefError, MISSING_REF_RE, GITHUB_REMOTE_RE;
1952
2880
  var init_git = __esm({
1953
2881
  "src/git.ts"() {
1954
2882
  "use strict";
@@ -1963,6 +2891,17 @@ var init_git = __esm({
1963
2891
  branch;
1964
2892
  worktreePath;
1965
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;
1966
2905
  GITHUB_REMOTE_RE = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/i;
1967
2906
  }
1968
2907
  });
@@ -1983,7 +2922,7 @@ __export(droneChannel_exports, {
1983
2922
  verifyLocalCliToken: () => verifyLocalCliToken
1984
2923
  });
1985
2924
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
1986
- import { chmodSync, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
2925
+ import { chmodSync, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
1987
2926
  import { dirname as dirname3 } from "path";
1988
2927
  import { serve } from "@hono/node-server";
1989
2928
  function loadChannelSecret(path) {
@@ -1993,7 +2932,7 @@ function loadChannelSecret(path) {
1993
2932
  } catch {
1994
2933
  }
1995
2934
  const secret = randomBytes(32).toString("hex");
1996
- mkdirSync5(dirname3(path), { recursive: true });
2935
+ mkdirSync7(dirname3(path), { recursive: true });
1997
2936
  writeFileSync9(path, secret, { mode: 384 });
1998
2937
  try {
1999
2938
  chmodSync(path, 384);
@@ -2324,7 +3263,7 @@ var init_droneChannel = __esm({
2324
3263
 
2325
3264
  // src/attachments.ts
2326
3265
  import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
2327
- import { join as join14 } from "path";
3266
+ import { join as join17 } from "path";
2328
3267
  function attachmentsFromDoc(doc, origin) {
2329
3268
  const out = [];
2330
3269
  const walk2 = (node) => {
@@ -2415,7 +3354,7 @@ async function materializeAttachments(opts) {
2415
3354
  };
2416
3355
  const result = { files: [], skipped: [] };
2417
3356
  if (opts.refs.length === 0) return result;
2418
- const dir = join14(opts.worktree, ATTACHMENTS_DIR);
3357
+ const dir = join17(opts.worktree, ATTACHMENTS_DIR);
2419
3358
  await io.removeDir(dir);
2420
3359
  let dirReady = false;
2421
3360
  let total = 0;
@@ -2461,7 +3400,7 @@ async function materializeAttachments(opts) {
2461
3400
  await io.mkdir(dir);
2462
3401
  dirReady = true;
2463
3402
  }
2464
- await io.writeFile(join14(dir, name), file.bytes);
3403
+ await io.writeFile(join17(dir, name), file.bytes);
2465
3404
  } catch (e) {
2466
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)})`);
2467
3406
  continue;
@@ -2614,7 +3553,7 @@ var init_attachments = __esm({
2614
3553
 
2615
3554
  // src/shotUpload.ts
2616
3555
  import { readFile as fsReadFile } from "fs/promises";
2617
- import { basename as basename2, isAbsolute as isAbsolute3, join as join15 } from "path";
3556
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join18 } from "path";
2618
3557
  function shotMimetype(path) {
2619
3558
  const dot = path.lastIndexOf(".");
2620
3559
  if (dot < 0) return null;
@@ -2640,7 +3579,7 @@ async function readShots(paths, opts) {
2640
3579
  }
2641
3580
  let bytes;
2642
3581
  try {
2643
- bytes = await read(isAbsolute3(path) ? path : join15(opts.worktree, path));
3582
+ bytes = await read(isAbsolute3(path) ? path : join18(opts.worktree, path));
2644
3583
  } catch (e) {
2645
3584
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
2646
3585
  continue;
@@ -2719,7 +3658,7 @@ var init_shotUpload = __esm({
2719
3658
  // src/oauth.ts
2720
3659
  import { createHash, randomBytes as randomBytes2 } from "crypto";
2721
3660
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
2722
- import { join as join16 } from "path";
3661
+ import { join as join19 } from "path";
2723
3662
  function makePkce() {
2724
3663
  const verifier = randomBytes2(48).toString("base64url");
2725
3664
  const challenge = createHash("sha256").update(verifier).digest("base64url");
@@ -2854,15 +3793,15 @@ var init_oauth = __esm({
2854
3793
  AUTHORIZE = "/api/oauth/authorize";
2855
3794
  TOKEN = "/api/oauth/token";
2856
3795
  SCOPE = "openid profile email taskless:full";
2857
- STORE_PATH = join16(SWARM_HOME, "oauth.json");
2858
- CLIENT_PATH = join16(SWARM_HOME, "oauth-client.json");
3796
+ STORE_PATH = join19(SWARM_HOME, "oauth.json");
3797
+ CLIENT_PATH = join19(SWARM_HOME, "oauth-client.json");
2859
3798
  base = (api) => api.replace(/\/$/, "");
2860
3799
  }
2861
3800
  });
2862
3801
 
2863
3802
  // src/agentToken.ts
2864
3803
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
2865
- import { join as join17 } from "path";
3804
+ import { join as join20 } from "path";
2866
3805
  function loadAgentToken() {
2867
3806
  try {
2868
3807
  const s = JSON.parse(readFileSync15(STORE_PATH2, "utf8"));
@@ -2902,14 +3841,14 @@ var init_agentToken = __esm({
2902
3841
  "src/agentToken.ts"() {
2903
3842
  "use strict";
2904
3843
  init_config();
2905
- STORE_PATH2 = join17(SWARM_HOME, "agent.json");
3844
+ STORE_PATH2 = join20(SWARM_HOME, "agent.json");
2906
3845
  base2 = (api) => api.replace(/\/$/, "");
2907
3846
  }
2908
3847
  });
2909
3848
 
2910
3849
  // src/workspace.ts
2911
3850
  import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
2912
- import { join as join18 } from "path";
3851
+ import { join as join21 } from "path";
2913
3852
  function loadWorkspace() {
2914
3853
  try {
2915
3854
  const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
@@ -2926,7 +3865,7 @@ var init_workspace = __esm({
2926
3865
  "src/workspace.ts"() {
2927
3866
  "use strict";
2928
3867
  init_config();
2929
- WORKSPACE_PATH = join18(SWARM_HOME, "workspace.json");
3868
+ WORKSPACE_PATH = join21(SWARM_HOME, "workspace.json");
2930
3869
  }
2931
3870
  });
2932
3871
 
@@ -3009,6 +3948,11 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
3009
3948
  if (!doc) return null;
3010
3949
  return { title: doc.title ?? "", body: extractText(doc.body) };
3011
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
+ }
3012
3956
  async function resolveId(idOrSlug) {
3013
3957
  const cached = idCache.get(idOrSlug);
3014
3958
  if (cached) return cached;
@@ -3076,6 +4020,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
3076
4020
  },
3077
4021
  backlogStatusId,
3078
4022
  getDocument,
4023
+ getTaskContext,
3079
4024
  createTask: async (input) => {
3080
4025
  if (!input.projectId) {
3081
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)");
@@ -3115,7 +4060,19 @@ function requireTaskBody(task, slug, workspaceId) {
3115
4060
  if (!task) throw new TaskBodyUnavailableError(slug, workspaceId);
3116
4061
  return task;
3117
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
+ }
3118
4073
  async function fetchTaskContext(client, task) {
4074
+ const brief = await client.getTaskContext(task.id);
4075
+ if (brief) return wrapUntrusted([brief]);
3119
4076
  const parts = [];
3120
4077
  const comments = task.comments ?? [];
3121
4078
  if (comments.length > 0) {
@@ -3151,14 +4108,7 @@ ${text || "(\u043F\u0443\u0441\u0442\u043E)"}`;
3151
4108
  )
3152
4109
  );
3153
4110
  }
3154
- if (parts.length === 0) return "";
3155
- return [
3156
- UNTRUSTED_TASK_INPUT_NOTICE,
3157
- "",
3158
- "\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",
3159
- parts.join("\n\n"),
3160
- "\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"
3161
- ].join("\n");
4111
+ return wrapUntrusted(parts);
3162
4112
  }
3163
4113
  function commentIdOf(res) {
3164
4114
  const comment = res?.structuredContent?.comment;
@@ -3353,7 +4303,7 @@ __export(db_exports, {
3353
4303
  SwarmDb: () => SwarmDb
3354
4304
  });
3355
4305
  import { createRequire as createRequire2 } from "module";
3356
- import { existsSync as existsSync10 } from "fs";
4306
+ import { existsSync as existsSync11 } from "fs";
3357
4307
  import { resolve as resolve3 } from "path";
3358
4308
  function rowToQuestion(row) {
3359
4309
  return {
@@ -3387,6 +4337,10 @@ function rowToRun(row) {
3387
4337
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
3388
4338
  model: row.model ?? null,
3389
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,
3390
4344
  pr_url: row.pr_url ?? null,
3391
4345
  superseded_by: row.superseded_by ?? null,
3392
4346
  decision: row.decision ? JSON.parse(row.decision) : null,
@@ -3452,6 +4406,9 @@ CREATE TABLE IF NOT EXISTS runs (
3452
4406
  depends_on TEXT,
3453
4407
  model TEXT,
3454
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,
3455
4412
  pr_url TEXT,
3456
4413
  superseded_by INTEGER,
3457
4414
  decision TEXT,
@@ -3545,7 +4502,11 @@ CREATE TABLE IF NOT EXISTS imports (
3545
4502
  // Базовая ветка PR (TASWM-285): куда дрон открывает PR и от чего ответвляется.
3546
4503
  "ALTER TABLE runs ADD COLUMN base_branch TEXT",
3547
4504
  // Воркспейс задачи прогона (TASWM-288): им пинятся задачные MCP-вызовы прогона.
3548
- "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"
3549
4510
  ]) {
3550
4511
  try {
3551
4512
  this.db.exec(alter);
@@ -3838,7 +4799,7 @@ CREATE TABLE IF NOT EXISTS imports (
3838
4799
  const repoN = normalizeRepo(repo);
3839
4800
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
3840
4801
  if (already) return { imported: false, runs: already.run_count };
3841
- if (!existsSync10(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}`);
3842
4803
  const src = new this.databaseCtor(abs);
3843
4804
  try {
3844
4805
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -4206,9 +5167,9 @@ var init_droneStore = __esm({
4206
5167
  });
4207
5168
 
4208
5169
  // src/overmind.ts
4209
- import { homedir as homedir4 } from "os";
4210
- import { existsSync as existsSync15, readFileSync as readFileSync20, readdirSync as readdirSync10, renameSync as renameSync10 } from "fs";
4211
- import { basename as basename4, dirname as dirname5, join as join25 } 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";
4212
5173
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
4213
5174
  import { spawn as spawn3 } from "child_process";
4214
5175
 
@@ -4288,13 +5249,13 @@ init_config();
4288
5249
  init_resources();
4289
5250
  init_names2();
4290
5251
  init_storm();
4291
- 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";
4292
5253
  import { dirname } from "path";
4293
5254
  import { randomUUID } from "crypto";
4294
5255
 
4295
5256
  // src/logTail.ts
4296
- import { closeSync, existsSync as existsSync2, openSync, readSync, readdirSync, statSync } from "fs";
4297
- 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";
4298
5259
  var TAIL_LINES_FALLBACK = 200;
4299
5260
  var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
4300
5261
  function fileHeader(name, size, shown) {
@@ -4304,7 +5265,7 @@ function fileHeader(name, size, shown) {
4304
5265
  function readTail(path, opts) {
4305
5266
  let size;
4306
5267
  try {
4307
- size = statSync(path).size;
5268
+ size = statSync3(path).size;
4308
5269
  } catch {
4309
5270
  return null;
4310
5271
  }
@@ -4335,10 +5296,10 @@ function readTail(path, opts) {
4335
5296
  return { text, size, shown: Buffer.byteLength(text, "utf8") };
4336
5297
  }
4337
5298
  function latestRunLaunch(runLogsDir, localRunId) {
4338
- const dir = join3(runLogsDir, String(localRunId));
5299
+ const dir = join6(runLogsDir, String(localRunId));
4339
5300
  let files;
4340
5301
  try {
4341
- files = readdirSync(dir);
5302
+ files = readdirSync4(dir);
4342
5303
  } catch {
4343
5304
  return null;
4344
5305
  }
@@ -4349,13 +5310,13 @@ function latestRunLaunch(runLogsDir, localRunId) {
4349
5310
  }
4350
5311
  if (indexes.size === 0) return null;
4351
5312
  const index = Math.max(...indexes);
4352
- const out = [join3(dir, `${index}.json`), join3(dir, `${index}.json.done`)].find((p) => existsSync2(p)) ?? null;
4353
- 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;
4354
5315
  return { index, total: indexes.size, out, err };
4355
5316
  }
4356
5317
  function runLogTail(runLogsDir, localRunId, opts) {
4357
5318
  const launch = latestRunLaunch(runLogsDir, localRunId);
4358
- 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)` };
4359
5320
  const parts = [];
4360
5321
  let budget = opts.maxBytes;
4361
5322
  if (launch.err) {
@@ -4488,6 +5449,15 @@ var SwarmClient = class _SwarmClient {
4488
5449
  nextRunId = 1;
4489
5450
  nextEventId = 1;
4490
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;
4491
5461
  // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
4492
5462
  // Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
4493
5463
  // переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
@@ -4498,6 +5468,9 @@ var SwarmClient = class _SwarmClient {
4498
5468
  // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
4499
5469
  // каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
4500
5470
  notifyLevelUnsupported = false;
5471
+ // TAS-485: то же для гашения разрешившихся тревог (swarm_notify_resolve) — прод старее
5472
+ // демона не знает тула; ретраить это каждый тик бессмысленно, см. resolveAnomalies.
5473
+ notifyResolveUnsupported = false;
4501
5474
  // Корреляция с продом.
4502
5475
  keyByLocal = /* @__PURE__ */ new Map();
4503
5476
  // local run id → runKey
@@ -4540,24 +5513,24 @@ var SwarmClient = class _SwarmClient {
4540
5513
  this.repoFullNames = registry.map((e) => e.repoFullName);
4541
5514
  this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
4542
5515
  this.host = opts.host ?? null;
4543
- this.repoExists = opts.repoExists ?? existsSync3;
5516
+ this.repoExists = opts.repoExists ?? existsSync5;
4544
5517
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
4545
5518
  this.outboxPath = opts.outboxPath;
4546
5519
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
4547
5520
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
4548
- mkdirSync2(dirname(this.outboxPath), { recursive: true });
5521
+ mkdirSync4(dirname(this.outboxPath), { recursive: true });
4549
5522
  this.recover();
4550
5523
  }
4551
5524
  // ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
4552
5525
  recover() {
4553
- if (existsSync3(this.mapPath)) {
5526
+ if (existsSync5(this.mapPath)) {
4554
5527
  try {
4555
5528
  const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
4556
5529
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
4557
5530
  } catch {
4558
5531
  }
4559
5532
  }
4560
- if (existsSync3(this.outboxPath)) {
5533
+ if (existsSync5(this.outboxPath)) {
4561
5534
  const txt = readFileSync4(this.outboxPath, "utf8");
4562
5535
  for (const line of txt.split("\n")) {
4563
5536
  const s = line.trim();
@@ -4578,7 +5551,7 @@ var SwarmClient = class _SwarmClient {
4578
5551
  // eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
4579
5552
  // переприсваиваем локально (id событий — только порядок, не корреляция с продом).
4580
5553
  replayGlobalEvents() {
4581
- if (!existsSync3(this.stormPath)) return;
5554
+ if (!existsSync5(this.stormPath)) return;
4582
5555
  let txt;
4583
5556
  try {
4584
5557
  txt = readFileSync4(this.stormPath, "utf8");
@@ -4647,8 +5620,8 @@ var SwarmClient = class _SwarmClient {
4647
5620
  this.scheduleFlush();
4648
5621
  }
4649
5622
  // Прод-патч прогона: локальные поля Run → поля swarm_run_upsert (см. TASWM-74).
4650
- // pid/worktree_path (рантайм), respawns (нет в схеме тула), снимок задачи (task_*,
4651
- // прод берёт join'ом из Task) и priority (локальная очередь ЭТОГО демона, TASWM-269)
5623
+ // pid/worktree_path (рантайм), снимок задачи (task_*, прод берёт join'ом из Task)
5624
+ // и priority (локальная очередь ЭТОГО демона, TASWM-269)
4652
5625
  // в прод НЕ уходят. kind → role (плоский enum). Self-ref
4653
5626
  // прогонов hero_run_id/resolves_run_id/superseded_by → heroId/resolvesRunId/
4654
5627
  // supersededById как ОТЛОЖЕННЫЕ ссылки (RunRef) — cuid резолвится на флаше.
@@ -4659,6 +5632,10 @@ var SwarmClient = class _SwarmClient {
4659
5632
  if ("model" in patch) out.model = patch.model ?? null;
4660
5633
  if ("name" in patch) out.droneName = patch.name ?? null;
4661
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;
4662
5639
  if ("pr_url" in patch) out.prUrl = patch.pr_url ?? null;
4663
5640
  if ("branch" in patch) out.branch = patch.branch ?? null;
4664
5641
  if ("repo" in patch) out.repo = patch.repo ?? null;
@@ -4724,6 +5701,10 @@ var SwarmClient = class _SwarmClient {
4724
5701
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
4725
5702
  model: input.model ?? null,
4726
5703
  cost_usd: 0,
5704
+ tokens_in: 0,
5705
+ // копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
5706
+ tokens_out: 0,
5707
+ duration_ms: 0,
4727
5708
  pr_url: null,
4728
5709
  superseded_by: null,
4729
5710
  decision: input.decision ?? null,
@@ -4846,6 +5827,7 @@ var SwarmClient = class _SwarmClient {
4846
5827
  if (!d) return 0;
4847
5828
  d.respawns += 1;
4848
5829
  d.updated_at = stamp();
5830
+ this.enqueueRun(d, this.runFields({ respawns: d.respawns }));
4849
5831
  return d.respawns;
4850
5832
  }
4851
5833
  cancel(id, reason) {
@@ -5261,7 +6243,7 @@ var SwarmClient = class _SwarmClient {
5261
6243
  * транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
5262
6244
  */
5263
6245
  reconcileRuntime(opts) {
5264
- const wtExists = opts.worktreeExists ?? existsSync3;
6246
+ const wtExists = opts.worktreeExists ?? existsSync5;
5265
6247
  const pidByWorktree = /* @__PURE__ */ new Map();
5266
6248
  try {
5267
6249
  for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
@@ -5349,23 +6331,69 @@ var SwarmClient = class _SwarmClient {
5349
6331
  * шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
5350
6332
  */
5351
6333
  async claim() {
6334
+ this.pendingClaimKey ??= randomUUID();
5352
6335
  let res;
5353
6336
  try {
5354
6337
  res = await this.call("swarm_claim_work", {
5355
6338
  ...this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {},
5356
6339
  // TASWM-281: взятая из общей очереди задача сразу принадлежит ЭТОЙ машине —
5357
6340
  // прод помечает созданный прогон hostId, чужой демон его не увидит.
5358
- ...this.host ? { hostId: this.host.hostId } : {}
6341
+ ...this.host ? { hostId: this.host.hostId } : {},
6342
+ idempotencyKey: this.pendingClaimKey
5359
6343
  });
5360
6344
  } catch {
5361
6345
  return null;
5362
6346
  }
5363
6347
  if (res?.isError) return null;
6348
+ this.pendingClaimKey = null;
5364
6349
  const sc = res.structuredContent ?? {};
6350
+ this.logClaimSkips(sc);
5365
6351
  if (!sc.claimed || typeof sc.run !== "object" || sc.run === null) return null;
5366
6352
  const local = this.hydrateRun(sc.run);
5367
6353
  return local ? this.mat(local) : null;
5368
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
+ }
5369
6397
  /**
5370
6398
  * TASWM-281: пометить своими прогоны, под которыми на ЭТОЙ машине нашёлся ЖИВОЙ
5371
6399
  * процесс (после reconcileRuntime на старте). Это ровно те прогоны, где сомнений в
@@ -5639,12 +6667,20 @@ var SwarmClient = class _SwarmClient {
5639
6667
  * Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
5640
6668
  * означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
5641
6669
  *
6670
+ * TASWM-352: тем же пульсом уходит ЁМКОСТЬ этой машины (`capacity` — живой потолок
6671
+ * дронов ЭТОГО демона: машинный оверрайд, иначе общая настройка владельца). По ней
6672
+ * планировщик роя раскладывает очередь между машинами: до этого у прода было ОДНО
6673
+ * число на владельца, и ноутбук с рабочей станцией делили его поровну, хотя тянут
6674
+ * разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
6675
+ * трогает прошлое значение — та же логика, что у repoFullNames.
6676
+ *
5642
6677
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
5643
6678
  */
5644
- async heartbeat(daemonVersion, pause) {
6679
+ async heartbeat(daemonVersion, pause, capacity) {
5645
6680
  try {
5646
6681
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
5647
6682
  if (daemonVersion) args.daemonVersion = daemonVersion;
6683
+ if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
5648
6684
  if (pause !== void 0) {
5649
6685
  args.paused = !!pause?.paused;
5650
6686
  if (pause?.reason) args.pauseReason = pause.reason;
@@ -5750,6 +6786,35 @@ var SwarmClient = class _SwarmClient {
5750
6786
  const res = await this.call("swarm_notify", base3);
5751
6787
  if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
5752
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
+ }
5753
6818
  /**
5754
6819
  * TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
5755
6820
  * через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
@@ -5814,6 +6879,10 @@ var SwarmClient = class _SwarmClient {
5814
6879
  depends_on: [],
5815
6880
  model: null,
5816
6881
  cost_usd: 0,
6882
+ tokens_in: 0,
6883
+ // токены и время работы (TASWM-353) — прод-поля, заполняются ниже
6884
+ tokens_out: 0,
6885
+ duration_ms: 0,
5817
6886
  pr_url: null,
5818
6887
  superseded_by: null,
5819
6888
  decision: null,
@@ -5824,9 +6893,9 @@ var SwarmClient = class _SwarmClient {
5824
6893
  review_verdict: null,
5825
6894
  review_phase: null,
5826
6895
  // вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
5827
- // Локальная бухгалтерия (в прод-схему не уходит, как respawns) — на гидрации из
5828
- // прода дефолт 0; существующая локальная запись НЕ перетирается (поле не в field-copy
5829
- // ниже), в отличие от respawns, — авто-ревью v2, TASWM-162.
6896
+ // Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
6897
+ // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
6898
+ // в отличие от respawns, — авто-ревью v2, TASWM-162.
5830
6899
  review_cycle: 0,
5831
6900
  task_title: null,
5832
6901
  task_desc: null,
@@ -5878,6 +6947,9 @@ var SwarmClient = class _SwarmClient {
5878
6947
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
5879
6948
  local.model = pr.model ?? null;
5880
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;
5881
6953
  local.pr_url = pr.prUrl ?? null;
5882
6954
  local.stop_reason = pr.stopReason ?? null;
5883
6955
  local.respawns = typeof pr.respawns === "number" ? pr.respawns : 0;
@@ -6135,13 +7207,13 @@ var SwarmClient = class _SwarmClient {
6135
7207
  }
6136
7208
  persistPending() {
6137
7209
  try {
6138
- 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" : ""));
6139
7211
  } catch {
6140
7212
  }
6141
7213
  }
6142
7214
  persistMap() {
6143
7215
  try {
6144
- writeFileSync3(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
7216
+ writeFileSync4(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
6145
7217
  } catch {
6146
7218
  }
6147
7219
  }
@@ -6171,11 +7243,11 @@ init_config();
6171
7243
  init_heartbeat();
6172
7244
 
6173
7245
  // src/logRotate.ts
6174
- import { copyFileSync, renameSync as renameSync4, statSync as statSync2, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync4 } from "fs";
7246
+ import { copyFileSync, renameSync as renameSync4, statSync as statSync4, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
6175
7247
  var realIo = {
6176
7248
  size: (path) => {
6177
7249
  try {
6178
- return statSync2(path).size;
7250
+ return statSync4(path).size;
6179
7251
  } catch {
6180
7252
  return null;
6181
7253
  }
@@ -6184,7 +7256,7 @@ var realIo = {
6184
7256
  rename: renameSync4,
6185
7257
  truncate: (path) => truncateSync(path, 0),
6186
7258
  unlink: unlinkSync2,
6187
- exists: existsSync4
7259
+ exists: existsSync6
6188
7260
  };
6189
7261
  var realOwnerIo = {
6190
7262
  statFd: (fd) => {
@@ -6197,7 +7269,7 @@ var realOwnerIo = {
6197
7269
  },
6198
7270
  statPath: (path) => {
6199
7271
  try {
6200
- const s = statSync2(path);
7272
+ const s = statSync4(path);
6201
7273
  return { dev: s.dev, ino: s.ino };
6202
7274
  } catch {
6203
7275
  return null;
@@ -6268,13 +7340,13 @@ async function buildRepoRegistry(paths) {
6268
7340
  }
6269
7341
 
6270
7342
  // src/repoMemory.ts
6271
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
6272
- import { join as join4 } from "path";
7343
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
7344
+ import { join as join7 } from "path";
6273
7345
  var FILE = "repos.json";
6274
7346
  function loadRememberedRepos(home) {
6275
7347
  let raw;
6276
7348
  try {
6277
- raw = readFileSync6(join4(home, FILE), "utf8");
7349
+ raw = readFileSync6(join7(home, FILE), "utf8");
6278
7350
  } catch {
6279
7351
  return [];
6280
7352
  }
@@ -6289,7 +7361,7 @@ function loadRememberedRepos(home) {
6289
7361
  function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
6290
7362
  const clean = paths.filter((p) => p.trim().length > 0);
6291
7363
  if (clean.length === 0) return;
6292
- 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)}
6293
7365
  `);
6294
7366
  }
6295
7367
  function pickRepoPaths(args) {
@@ -6302,13 +7374,13 @@ function pickRepoPaths(args) {
6302
7374
  // src/repoDiscovery.ts
6303
7375
  init_src();
6304
7376
  init_config();
6305
- import { homedir as homedir2 } from "os";
6306
- import { readFileSync as readFileSync8, renameSync as renameSync5, writeFileSync as writeFileSync6 } from "fs";
6307
- import { isAbsolute, join as join6, resolve as resolve2 } from "path";
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";
6308
7380
 
6309
7381
  // src/repoScan.ts
6310
- import { readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
6311
- 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";
6312
7384
  var SKIP = /* @__PURE__ */ new Set([
6313
7385
  "node_modules",
6314
7386
  "vendor",
@@ -6338,7 +7410,7 @@ function parseOriginFromGitConfig(text) {
6338
7410
  }
6339
7411
  function originOf(dir) {
6340
7412
  try {
6341
- return parseOriginFromGitConfig(readFileSync7(join5(dir, ".git", "config"), "utf8"));
7413
+ return parseOriginFromGitConfig(readFileSync7(join8(dir, ".git", "config"), "utf8"));
6342
7414
  } catch {
6343
7415
  return null;
6344
7416
  }
@@ -6367,15 +7439,15 @@ function walk(dir, depth, maxDepth, found) {
6367
7439
  }
6368
7440
  let entries;
6369
7441
  try {
6370
- entries = readdirSync2(dir);
7442
+ entries = readdirSync5(dir);
6371
7443
  } catch {
6372
7444
  return;
6373
7445
  }
6374
7446
  for (const name of entries) {
6375
7447
  if (name.startsWith(".") || SKIP.has(name)) continue;
6376
- const child = join5(dir, name);
7448
+ const child = join8(dir, name);
6377
7449
  try {
6378
- if (!statSync3(child).isDirectory()) continue;
7450
+ if (!statSync5(child).isDirectory()) continue;
6379
7451
  } catch {
6380
7452
  continue;
6381
7453
  }
@@ -6395,13 +7467,13 @@ function defaultScanRoots(home, knownPaths = []) {
6395
7467
  if (parent && parent !== p) add(parent);
6396
7468
  }
6397
7469
  for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
6398
- add(join5(home, name));
7470
+ add(join8(home, name));
6399
7471
  }
6400
7472
  return out;
6401
7473
  }
6402
7474
 
6403
7475
  // src/repoDiscovery.ts
6404
- function resolveScanRoots(raw, home = homedir2(), knownPaths = []) {
7476
+ function resolveScanRoots(raw, home = homedir3(), knownPaths = []) {
6405
7477
  const v = (raw ?? "").trim();
6406
7478
  if (!v) return defaultScanRoots(home, knownPaths);
6407
7479
  if (["off", "none", "0"].includes(v.toLowerCase())) return [];
@@ -6409,7 +7481,7 @@ function resolveScanRoots(raw, home = homedir2(), knownPaths = []) {
6409
7481
  for (const part of v.split(",")) {
6410
7482
  const p = part.trim();
6411
7483
  if (!p) continue;
6412
- const abs = p === "~" ? home : p.startsWith("~/") ? join6(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
7484
+ const abs = p === "~" ? home : p.startsWith("~/") ? join9(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
6413
7485
  if (!out.includes(abs)) out.push(abs);
6414
7486
  }
6415
7487
  return out;
@@ -6547,7 +7619,7 @@ function readRepoMapCache(path) {
6547
7619
  function writeRepoMapCache(path, entries) {
6548
7620
  try {
6549
7621
  const tmp = `${path}.tmp`;
6550
- writeFileSync6(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
7622
+ writeFileSync7(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
6551
7623
  renameSync5(tmp, path);
6552
7624
  } catch {
6553
7625
  }
@@ -6555,17 +7627,17 @@ function writeRepoMapCache(path, entries) {
6555
7627
 
6556
7628
  // src/hostIdentity.ts
6557
7629
  import { hostname } from "os";
6558
- import { existsSync as existsSync5, readFileSync as readFileSync9, renameSync as renameSync6, writeFileSync as writeFileSync7 } from "fs";
6559
- import { join as join7 } 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";
6560
7632
  import { randomUUID as randomUUID2 } from "crypto";
6561
7633
  function hostIdentityPath(home) {
6562
- return join7(home, "host.json");
7634
+ return join10(home, "host.json");
6563
7635
  }
6564
7636
  function loadHostIdentity(deps) {
6565
- const exists = deps.exists ?? existsSync5;
7637
+ const exists = deps.exists ?? existsSync7;
6566
7638
  const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
6567
7639
  const writeFile2 = deps.writeFile ?? ((p, data) => {
6568
- writeFileSync7(`${p}.tmp`, data);
7640
+ writeFileSync8(`${p}.tmp`, data);
6569
7641
  renameSync6(`${p}.tmp`, p);
6570
7642
  });
6571
7643
  const host = deps.hostname ?? hostname;
@@ -6706,129 +7778,57 @@ function capList(items) {
6706
7778
  }
6707
7779
  function reliefAdvice(ctx) {
6708
7780
  const steps = [];
6709
- if (!Number.isFinite(ctx.maxDrones)) steps.push("\u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0442\u044C \u0440\u043E\u0439 \u043E\u0434\u043D\u0438\u043C \u0434\u0440\u043E\u043D\u043E\u043C (maxDrones=1) \u2014 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u0440\u043E\u044F \u0432 /swarm");
6710
- else if (ctx.maxDrones > 1) steps.push(`\u0441\u043D\u0438\u0437\u0438\u0442\u044C maxDrones \u0434\u043E 1 (\u0441\u0435\u0439\u0447\u0430\u0441 ${ctx.maxDrones}) \u2014 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u0440\u043E\u044F \u0432 /swarm`);
6711
- if (ctx.exclusiveRepos.length) steps.push(`\u043F\u043E\u043C\u0435\u0442\u0438\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442 \u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u044B\u043C (\u043E\u0434\u0438\u043D \u0434\u0440\u043E\u043D \u043D\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439): ${capList(ctx.exclusiveRepos)}`);
6712
- if (ctx.otherHosts.length) steps.push(`\u0443\u0432\u0435\u0437\u0442\u0438 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u043C\u0430\u0448\u0438\u043D\u0443 \u0440\u043E\u044F (spawn --host): ${capList(ctx.otherHosts)}`);
6713
- steps.push(
6714
- ctx.totalGb > 0 ? `\u043C\u0430\u0448\u0438\u043D\u0430 \u043C\u043E\u0449\u043D\u0435\u0435: ${ctx.totalGb.toFixed(0)} \u0413\u0411 \u043D\u0430 ${Math.max(ctx.activeDrones, 2)} \u0434\u0440\u043E\u043D\u0430 Claude Code \u043F\u043B\u044E\u0441 \u0441\u0431\u043E\u0440\u043A\u0438 \u2014 \u0432\u043F\u0440\u0438\u0442\u044B\u043A` : `\u043C\u0430\u0448\u0438\u043D\u0430 \u043C\u043E\u0449\u043D\u0435\u0435: ${Math.max(ctx.activeDrones, 2)} \u0434\u0440\u043E\u043D\u0430 Claude Code \u043F\u043B\u044E\u0441 \u0441\u0431\u043E\u0440\u043A\u0438 \u044D\u0442\u0430 \u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u0442\u044F\u043D\u0435\u0442`
6715
- );
6716
- return steps;
6717
- }
6718
- function humanMinutes(ms) {
6719
- const total = Math.round(ms / 6e4);
6720
- if (total < 60) return `${total} \u043C\u0438\u043D`;
6721
- return `${Math.floor(total / 60)} \u0447 ${String(total % 60).padStart(2, "0")} \u043C\u0438\u043D`;
6722
- }
6723
- function reliefAlertBody(cost, pressure, advice, windowMs, minSteps) {
6724
- const window = humanMinutes(windowMs);
6725
- const perPause = cost.pauses > 0 ? humanMinutes(cost.lostMs / cost.pauses) : null;
6726
- const parts = [
6727
- `\u0417\u0430 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${window} \u0434\u0440\u043E\u043D\u043E\u0432 \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u043B\u0438 ${cost.pauses} \u0440\u0430\u0437${cost.runs > 1 ? ` (${cost.runs} \u043F\u0440\u043E\u0433\u043E\u043D\u0430)` : ""}, \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E \u043E\u043A\u043E\u043B\u043E ${humanMinutes(cost.lostMs)}${perPause ? `, \u0432 \u0441\u0440\u0435\u0434\u043D\u0435\u043C ${perPause} \u043C\u0435\u0436\u0434\u0443 \u043F\u0430\u0443\u0437\u043E\u0439 \u0438 \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u043E\u043C` : ""}.`
6728
- ];
6729
- if (cost.stepsBetween != null) {
6730
- parts.push(
6731
- cost.stepsBetween < minSteps ? `\u041C\u0435\u0436\u0434\u0443 \u043F\u0430\u0443\u0437\u0430\u043C\u0438 \u0434\u0440\u043E\u043D \u043D\u0435 \u0443\u0441\u043F\u0435\u0432\u0430\u0435\u0442 \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C \u0448\u0430\u0433 (\u043C\u0435\u0434\u0438\u0430\u043D\u0430 ${cost.stepsBetween}) \u2014 \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u0438\u0434\u0451\u0442, \u0434\u0440\u043E\u043D \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F \u043D\u0430 \u043C\u0435\u0441\u0442\u0435.` : `\u041C\u0435\u0436\u0434\u0443 \u043F\u0430\u0443\u0437\u0430\u043C\u0438 \u0434\u0440\u043E\u043D \u0443\u0441\u043F\u0435\u0432\u0430\u0435\u0442 ${cost.stepsBetween} \u0448\u0430\u0433(\u0430).`
6732
- );
6733
- }
6734
- if (cost.worst && cost.runs > 1) parts.push(`\u0411\u043E\u043B\u044C\u0448\u0435 \u0432\u0441\u0435\u0445 \u0434\u043E\u0441\u0442\u0430\u043B\u043E\u0441\u044C ${cost.worst.label} \u2014 ${cost.worst.pauses} \u0440\u0430\u0437.`);
6735
- if (pressure.culprits.length) parts.push(`\u0423\u043F\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u0432: ${pressure.culprits.join("; ")}.`);
6736
- parts.push(`\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u0438\u044F \u0445\u043E\u0441\u0442\u0430 \u2014 ${pressure.readings.join("; ")}.`);
6737
- parts.push(`\u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C \u043F\u043E \u043F\u043E\u0440\u044F\u0434\u043A\u0443: ${advice.map((s, i) => `${i + 1}) ${s}`).join("; ")}.`);
6738
- const body = parts.join(" ");
6739
- return body.length <= MAX_ALERT_BODY_CHARS ? body : `${body.slice(0, MAX_ALERT_BODY_CHARS - 1)}\u2026`;
6740
- }
6741
- function reliefAlertTitle(cost, windowMs) {
6742
- 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)}`;
6743
- }
6744
- function buildReliefCostCandidates(cost, t, sustainMs, build) {
6745
- if (!reliefAlertDue(cost, t)) return [];
6746
- return [{ key: "relief_cost", minDurationMs: sustainMs, build }];
6747
- }
6748
-
6749
- // src/launcher.ts
6750
- import { spawn } from "child_process";
6751
- import { closeSync as closeSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, openSync as openSync2, readdirSync as readdirSync5, statSync as statSync5, writeFileSync as writeFileSync8 } from "fs";
6752
- import { createRequire } from "module";
6753
- import { homedir as homedir3 } from "os";
6754
- import { dirname as dirname2, join as join9 } from "path";
6755
- import { fileURLToPath } from "url";
6756
-
6757
- // src/claudeBin.ts
6758
- import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
6759
- import { win32 as winPath } from "path";
6760
- var CLAUDE_FALLBACK = "claude";
6761
- function compareVersionDirs(a, b) {
6762
- const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
6763
- const pa = parts(a);
6764
- const pb = parts(b);
6765
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
6766
- const d = (pa[i] ?? 0) - (pb[i] ?? 0);
6767
- if (d !== 0) return d;
6768
- }
6769
- return 0;
6770
- }
6771
- function realListDir(dir) {
6772
- try {
6773
- return readdirSync3(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
6774
- } catch {
6775
- return [];
6776
- }
6777
- }
6778
- function winVersionRoots(env, listDir) {
6779
- const roots = [];
6780
- const appData = env.APPDATA;
6781
- if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
6782
- const localAppData = env.LOCALAPPDATA;
6783
- if (localAppData) {
6784
- const packages = winPath.join(localAppData, "Packages");
6785
- for (const name of listDir(packages)) {
6786
- if (!name.startsWith("Claude")) continue;
6787
- roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
6788
- }
6789
- }
6790
- return roots;
6791
- }
6792
- function findWindowsClaude(opts = {}) {
6793
- const env = opts.env ?? process.env;
6794
- const listDir = opts.listDir ?? realListDir;
6795
- const fileExists = opts.fileExists ?? existsSync6;
6796
- let best = null;
6797
- for (const root of winVersionRoots(env, listDir)) {
6798
- for (const version of listDir(root)) {
6799
- const candidate = winPath.join(root, version, "claude.exe");
6800
- if (!fileExists(candidate)) continue;
6801
- if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
6802
- }
6803
- }
6804
- return best?.path ?? null;
7781
+ if (!Number.isFinite(ctx.maxDrones)) steps.push("\u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0442\u044C \u0440\u043E\u0439 \u043E\u0434\u043D\u0438\u043C \u0434\u0440\u043E\u043D\u043E\u043C (maxDrones=1) \u2014 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u0440\u043E\u044F \u0432 /swarm");
7782
+ else if (ctx.maxDrones > 1) steps.push(`\u0441\u043D\u0438\u0437\u0438\u0442\u044C maxDrones \u0434\u043E 1 (\u0441\u0435\u0439\u0447\u0430\u0441 ${ctx.maxDrones}) \u2014 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u0440\u043E\u044F \u0432 /swarm`);
7783
+ if (ctx.exclusiveRepos.length) steps.push(`\u043F\u043E\u043C\u0435\u0442\u0438\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442 \u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u044B\u043C (\u043E\u0434\u0438\u043D \u0434\u0440\u043E\u043D \u043D\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439): ${capList(ctx.exclusiveRepos)}`);
7784
+ if (ctx.otherHosts.length) steps.push(`\u0443\u0432\u0435\u0437\u0442\u0438 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u043C\u0430\u0448\u0438\u043D\u0443 \u0440\u043E\u044F (spawn --host): ${capList(ctx.otherHosts)}`);
7785
+ steps.push(
7786
+ ctx.totalGb > 0 ? `\u043C\u0430\u0448\u0438\u043D\u0430 \u043C\u043E\u0449\u043D\u0435\u0435: ${ctx.totalGb.toFixed(0)} \u0413\u0411 \u043D\u0430 ${Math.max(ctx.activeDrones, 2)} \u0434\u0440\u043E\u043D\u0430 Claude Code \u043F\u043B\u044E\u0441 \u0441\u0431\u043E\u0440\u043A\u0438 \u2014 \u0432\u043F\u0440\u0438\u0442\u044B\u043A` : `\u043C\u0430\u0448\u0438\u043D\u0430 \u043C\u043E\u0449\u043D\u0435\u0435: ${Math.max(ctx.activeDrones, 2)} \u0434\u0440\u043E\u043D\u0430 Claude Code \u043F\u043B\u044E\u0441 \u0441\u0431\u043E\u0440\u043A\u0438 \u044D\u0442\u0430 \u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u0442\u044F\u043D\u0435\u0442`
7787
+ );
7788
+ return steps;
6805
7789
  }
6806
- var warnedNoWindowsClaude = false;
6807
- var cachedWindowsBin = null;
6808
- var windowsScanDone = false;
6809
- function resolveClaudeBin(opts = {}) {
6810
- const env = opts.env ?? process.env;
6811
- const platform = opts.platform ?? process.platform;
6812
- const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
6813
- if (explicit) return explicit;
6814
- if (platform !== "win32") return CLAUDE_FALLBACK;
6815
- if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
6816
- windowsScanDone = true;
6817
- const found = findWindowsClaude({ ...opts, env });
6818
- cachedWindowsBin = found;
6819
- if (found) return found;
6820
- if (!warnedNoWindowsClaude) {
6821
- warnedNoWindowsClaude = true;
6822
- console.warn(
6823
- "[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."
7790
+ function humanMinutes(ms) {
7791
+ const total = Math.round(ms / 6e4);
7792
+ if (total < 60) return `${total} \u043C\u0438\u043D`;
7793
+ return `${Math.floor(total / 60)} \u0447 ${String(total % 60).padStart(2, "0")} \u043C\u0438\u043D`;
7794
+ }
7795
+ function reliefAlertBody(cost, pressure, advice, windowMs, minSteps) {
7796
+ const window = humanMinutes(windowMs);
7797
+ const perPause = cost.pauses > 0 ? humanMinutes(cost.lostMs / cost.pauses) : null;
7798
+ const parts = [
7799
+ `\u0417\u0430 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${window} \u0434\u0440\u043E\u043D\u043E\u0432 \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u043B\u0438 ${cost.pauses} \u0440\u0430\u0437${cost.runs > 1 ? ` (${cost.runs} \u043F\u0440\u043E\u0433\u043E\u043D\u0430)` : ""}, \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E \u043E\u043A\u043E\u043B\u043E ${humanMinutes(cost.lostMs)}${perPause ? `, \u0432 \u0441\u0440\u0435\u0434\u043D\u0435\u043C ${perPause} \u043C\u0435\u0436\u0434\u0443 \u043F\u0430\u0443\u0437\u043E\u0439 \u0438 \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u043E\u043C` : ""}.`
7800
+ ];
7801
+ if (cost.stepsBetween != null) {
7802
+ parts.push(
7803
+ cost.stepsBetween < minSteps ? `\u041C\u0435\u0436\u0434\u0443 \u043F\u0430\u0443\u0437\u0430\u043C\u0438 \u0434\u0440\u043E\u043D \u043D\u0435 \u0443\u0441\u043F\u0435\u0432\u0430\u0435\u0442 \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C \u0448\u0430\u0433 (\u043C\u0435\u0434\u0438\u0430\u043D\u0430 ${cost.stepsBetween}) \u2014 \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u0438\u0434\u0451\u0442, \u0434\u0440\u043E\u043D \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F \u043D\u0430 \u043C\u0435\u0441\u0442\u0435.` : `\u041C\u0435\u0436\u0434\u0443 \u043F\u0430\u0443\u0437\u0430\u043C\u0438 \u0434\u0440\u043E\u043D \u0443\u0441\u043F\u0435\u0432\u0430\u0435\u0442 ${cost.stepsBetween} \u0448\u0430\u0433(\u0430).`
6824
7804
  );
6825
7805
  }
6826
- return CLAUDE_FALLBACK;
7806
+ if (cost.worst && cost.runs > 1) parts.push(`\u0411\u043E\u043B\u044C\u0448\u0435 \u0432\u0441\u0435\u0445 \u0434\u043E\u0441\u0442\u0430\u043B\u043E\u0441\u044C ${cost.worst.label} \u2014 ${cost.worst.pauses} \u0440\u0430\u0437.`);
7807
+ if (pressure.culprits.length) parts.push(`\u0423\u043F\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u0432: ${pressure.culprits.join("; ")}.`);
7808
+ parts.push(`\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u0438\u044F \u0445\u043E\u0441\u0442\u0430 \u2014 ${pressure.readings.join("; ")}.`);
7809
+ parts.push(`\u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C \u043F\u043E \u043F\u043E\u0440\u044F\u0434\u043A\u0443: ${advice.map((s, i) => `${i + 1}) ${s}`).join("; ")}.`);
7810
+ const body = parts.join(" ");
7811
+ return body.length <= MAX_ALERT_BODY_CHARS ? body : `${body.slice(0, MAX_ALERT_BODY_CHARS - 1)}\u2026`;
7812
+ }
7813
+ function reliefAlertTitle(cost, windowMs) {
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 }];
6827
7819
  }
6828
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
+
6829
7829
  // src/droneHome.ts
6830
- import { lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync4, rmSync, statSync as statSync4, symlinkSync } from "fs";
6831
- import { join as join8, 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";
6832
7832
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
6833
7833
  function droneHomeDeny(env = process.env) {
6834
7834
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -6846,34 +7846,34 @@ function buildDroneHome(opts) {
6846
7846
  const { realHome, droneHome } = opts;
6847
7847
  const deny = opts.deny ?? droneHomeDeny();
6848
7848
  const platform = opts.platform ?? process.platform;
6849
- mkdirSync3(droneHome, { recursive: true, mode: 448 });
6850
- 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));
6851
7851
  const rel = relative(realHome, droneHome);
6852
7852
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
6853
- for (const existing of readdirSync4(droneHome)) {
7853
+ for (const existing of readdirSync6(droneHome)) {
6854
7854
  if (wanted.has(existing)) continue;
6855
- const p = join8(droneHome, existing);
7855
+ const p = join11(droneHome, existing);
6856
7856
  try {
6857
7857
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
6858
7858
  } catch {
6859
7859
  }
6860
7860
  }
6861
7861
  for (const name of wanted) {
6862
- const link = join8(droneHome, name);
7862
+ const link = join11(droneHome, name);
6863
7863
  try {
6864
7864
  lstatSync(link);
6865
7865
  continue;
6866
7866
  } catch {
6867
7867
  }
6868
- const target = join8(realHome, name);
7868
+ const target = join11(realHome, name);
6869
7869
  let isDir;
6870
7870
  try {
6871
- isDir = statSync4(target).isDirectory();
7871
+ isDir = statSync6(target).isDirectory();
6872
7872
  } catch {
6873
7873
  continue;
6874
7874
  }
6875
7875
  try {
6876
- symlinkSync(target, link, isDir ? "junction" : "file");
7876
+ symlinkSync2(target, link, isDir ? "junction" : "file");
6877
7877
  } catch (e) {
6878
7878
  if (e.code !== "EEXIST") throw e;
6879
7879
  }
@@ -6886,10 +7886,10 @@ var REAL_PATH_ENV_PINS = [
6886
7886
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
6887
7887
  ];
6888
7888
  function realPathEnvPins(realHome) {
6889
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join8(realHome, ...segments)]));
7889
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join11(realHome, ...segments)]));
6890
7890
  }
6891
- function droneDenyRules(realHome, swarmHome) {
6892
- const paths = [
7891
+ function dronePrivatePaths(realHome, swarmHome) {
7892
+ return [
6893
7893
  // НЕ `${swarmHome}/**`. Worktree дрона живёт ВНУТРИ swarmHome
6894
7894
  // (`<swarmHome>/worktrees.noindex/<repo>/<slug>`), и правило на весь каталог закрывало
6895
7895
  // дрону его собственный рабочий каталог: Read/Write/Edit отвечали «File is in a directory
@@ -6910,11 +7910,6 @@ function droneDenyRules(realHome, swarmHome) {
6910
7910
  `${realHome}/.claude/.credentials.json`,
6911
7911
  `${realHome}/.npmrc`
6912
7912
  ];
6913
- const abs = (p) => `//${p.replace(/^\/+/, "")}`;
6914
- return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
6915
- }
6916
- function droneSettingsJson(realHome, swarmHome) {
6917
- return JSON.stringify({ permissions: { deny: droneDenyRules(realHome, swarmHome) } });
6918
7913
  }
6919
7914
 
6920
7915
  // src/testForks.ts
@@ -6946,6 +7941,7 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
6946
7941
  }
6947
7942
 
6948
7943
  // src/launcher.ts
7944
+ init_executors();
6949
7945
  var requireFromHere = createRequire(import.meta.url);
6950
7946
  function resolveTsxCli() {
6951
7947
  try {
@@ -6956,29 +7952,12 @@ function resolveTsxCli() {
6956
7952
  );
6957
7953
  }
6958
7954
  }
6959
- var ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
6960
- function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
6961
- const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
6962
- const base3 = [
6963
- "--dangerously-skip-permissions",
6964
- ...ISOLATION_FLAGS,
6965
- "--mcp-config",
6966
- cfgPath,
6967
- ...settings ? ["--settings", settings] : [],
6968
- ...pluginFlags,
6969
- "--output-format",
6970
- "json",
6971
- "-p",
6972
- prompt
6973
- ];
6974
- const withModel = model ? ["--model", model, ...base3] : base3;
6975
- return continueSession ? ["--continue", ...withModel] : withModel;
6976
- }
7955
+ var MCP_SERVER_NAME = "overmind";
6977
7956
  function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
6978
7957
  const env = { ...base3 };
6979
7958
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
6980
7959
  if (isolate && !base3.TURBO_CACHE_DIR) {
6981
- env.TURBO_CACHE_DIR = join9(worktree, ".turbo", "cache");
7960
+ env.TURBO_CACHE_DIR = join12(worktree, ".turbo", "cache");
6982
7961
  }
6983
7962
  Object.assign(env, droneTestEnv(droneCap, base3));
6984
7963
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -7006,49 +7985,20 @@ var DAEMON_STATE_ENV = [
7006
7985
  "TASKLESS_TOKEN"
7007
7986
  ];
7008
7987
  function bundledRoleDir(kind) {
7009
- return join9(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
7988
+ return join12(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
7010
7989
  }
7011
7990
  function roleDir(worktree, kind) {
7012
- return join9(worktree, ".claude", "roles", kind);
7991
+ return join12(worktree, ".claude", "roles", kind);
7013
7992
  }
7014
7993
  function roleDirs(worktree, kind) {
7015
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync7(dir));
7016
- }
7017
- function transcriptDir(worktree, projectsDir) {
7018
- return join9(projectsDir, worktree.replace(/[/.]/g, "-"));
7019
- }
7020
- function sessionTranscriptExists(worktree, projectsDir = join9(homedir3(), ".claude", "projects")) {
7021
- try {
7022
- return readdirSync5(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
7023
- } catch {
7024
- return false;
7025
- }
7026
- }
7027
- function transcriptMtimeMs(worktree, projectsDir = join9(homedir3(), ".claude", "projects")) {
7028
- const dir = transcriptDir(worktree, projectsDir);
7029
- let files;
7030
- try {
7031
- files = readdirSync5(dir);
7032
- } catch {
7033
- return null;
7034
- }
7035
- let latest = null;
7036
- for (const f of files) {
7037
- if (!f.endsWith(".jsonl")) continue;
7038
- try {
7039
- const mtime2 = statSync5(join9(dir, f)).mtimeMs;
7040
- if (latest === null || mtime2 > latest) latest = mtime2;
7041
- } catch {
7042
- }
7043
- }
7044
- return latest;
7994
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync8(dir));
7045
7995
  }
7046
7996
  function nextRunLogPath(runLogsDir, runId) {
7047
- const dir = join9(runLogsDir, String(runId));
7048
- mkdirSync4(dir, { recursive: true });
7997
+ const dir = join12(runLogsDir, String(runId));
7998
+ mkdirSync6(dir, { recursive: true });
7049
7999
  let files = [];
7050
8000
  try {
7051
- files = readdirSync5(dir);
8001
+ files = readdirSync7(dir);
7052
8002
  } catch {
7053
8003
  }
7054
8004
  let next = 0;
@@ -7056,7 +8006,7 @@ function nextRunLogPath(runLogsDir, runId) {
7056
8006
  const m = /^(\d+)\.json/.exec(f);
7057
8007
  if (m) next = Math.max(next, Number(m[1]) + 1);
7058
8008
  }
7059
- return join9(dir, `${next}.json`);
8009
+ return join12(dir, `${next}.json`);
7060
8010
  }
7061
8011
  function openNextRunLog(runLogsDir, runId) {
7062
8012
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -7091,57 +8041,63 @@ function makeLauncher(cfg) {
7091
8041
  );
7092
8042
  }
7093
8043
  const channelEnv = cfg.channel && channelToken ? { SWARM_DRONE_CHANNEL_URL: cfg.channel.url, SWARM_DRONE_CHANNEL_TOKEN: channelToken } : { SWARM_DB: cfg.dbPath };
7094
- const mcpConfig = {
7095
- mcpServers: {
7096
- overmind: {
7097
- command: process.execPath,
7098
- args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["--conditions=development", resolveTsxCli(), cfg.overmindEntry, "--drone", "--run", String(run.id)],
7099
- // SWARM_DRONE_CHANNEL_URL/TOKEN (TASWM-295) ИЛИ SWARM_DB (откат) — см. channelEnv выше.
7100
- // Канал: дрон говорит только с демоном, файл базы ему не отдаётся вовсе. Токен подписан
7101
- // на СТАБИЛЬНЫЙ ключ прогона, поэтому переживает рестарт демона.
7102
- // SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — доверенный
7103
- // источник «чья это сессия» (read_task и отчёт о сдаче доверяют ему, а не строке,
7104
- // найденной по номеру прогона).
7105
- // SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID (TASWM-263) и SWARM_RUN_NAME
7106
- // (TASWM-265): роль, связи и имя прогона той же спавн-фиксацией. Нужны РЕЖИМУ ОТКАТА —
7107
- // ensureRunHydrated заводит ими стаб-строку в sqlite, если демон в client-режиме её там
7108
- // не заводил. На канале прогон отдаёт демон целиком, и стаб не нужен.
7109
- // SWARM_WORKSPACE_ID (TASWM-288): ВОРКСПЕЙС прогона — той же спавн-фиксацией: задачные
7110
- // вызовы (read_task, отчёт, тулы Героя) дрон делает НЕ через канал, а прямо в Taskless,
7111
- // и воркспейс им нужен верно сразу. Без него read_task/отчёт дрона
7112
- // ушли бы с глобальным пином демона — ровно то, что чинит эта задача. Пусто
7113
- // (прогон до фичи / sqlite-бэкенд) → ключ не кладём, дрон берёт глобальный пин.
7114
- // SWARM_HOME (TASWM-289): дрон едет с ТЕНЕВЫМ HOME, в котором `.taskless-swarm`
7115
- // нет вовсе, — а дрон-MCP из этого же дерева процессов читает oauth.json
7116
- // (connectTaskless в runDroneServer), пульс овермайнда и workspace.json. Наследовать
7117
- // ему нечего, поэтому реальный SWARM_HOME кладём явно. Дрон этот конфиг прочитать
7118
- // может, но нового ему это не даёт: его собственный worktree лежит ВНУТРИ
7119
- // SWARM_HOME, путь он и так знает (см. «чего этот модуль не делает» в droneHome.ts).
7120
- env: {
7121
- ...channelEnv,
7122
- ...cfg.droneIsolation ? { SWARM_HOME: cfg.droneIsolation.swarmHome } : {},
7123
- SWARM_TASK_SLUG: run.task_slug,
7124
- SWARM_RUN_KIND: run.kind,
7125
- SWARM_RUN_NAME: run.name,
7126
- ...run.workspace_id ? { SWARM_WORKSPACE_ID: run.workspace_id } : {},
7127
- ...run.reviews_run_id != null ? { SWARM_REVIEWS_RUN_ID: String(run.reviews_run_id) } : {},
7128
- ...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
7129
- }
7130
- }
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) } : {}
7131
8078
  }
7132
8079
  };
7133
- const cfgPath = join9(worktree, ".swarm-mcp.json");
7134
- writeFileSync8(cfgPath, JSON.stringify(mcpConfig, null, 2));
7135
- const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
7136
- const pluginDirs = roleDirs(worktree, run.kind);
7137
- const realHome = cfg.droneIsolation?.realHome ?? homedir3();
8080
+ const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8081
+ const realHome = cfg.droneIsolation?.realHome ?? homedir4();
7138
8082
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
7139
- const settings = cfg.droneIsolation ? droneSettingsJson(realHome, cfg.droneIsolation.swarmHome) : void 0;
7140
- if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync7(join9(cfg.droneIsolation.swarmHome, "token"))) {
8083
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join12(cfg.droneIsolation.swarmHome, "token"))) {
7141
8084
  console.error(
7142
- `[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 ${join9(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.`
7143
8086
  );
7144
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
+ });
7145
8101
  let stdio = "ignore";
7146
8102
  let outFd;
7147
8103
  let errFd;
@@ -7151,15 +8107,16 @@ function makeLauncher(cfg) {
7151
8107
  errFd = fds.err ?? void 0;
7152
8108
  stdio = ["ignore", outFd, errFd ?? "ignore"];
7153
8109
  }
7154
- const child = spawn(cfg.claudeBin ?? resolveClaudeBin(), claudeArgs(cfgPath, prompt, continueSession, model, pluginDirs, settings), {
8110
+ const child = spawn(cfg.bin ?? plan.bin, plan.args, {
7155
8111
  cwd: worktree,
7156
8112
  detached: true,
7157
8113
  stdio,
7158
8114
  // Пер-worktree TURBO_CACHE_DIR (TASWM-232): дрон не отыгрывает кеш соседнего
7159
8115
  // worktree, проверки исполняются реально. Теневой HOME + чистка env от состояния
7160
8116
  // демона (TASWM-289). Доля тестовых форков от числа дронов на хосте (TASWM-246).
7161
- // Остальной env демона наследуется, как раньше.
7162
- 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 }
7163
8120
  });
7164
8121
  if (outFd !== void 0) closeSync2(outFd);
7165
8122
  if (errFd !== void 0) closeSync2(errFd);
@@ -7173,89 +8130,56 @@ function makeLauncher(cfg) {
7173
8130
  init_procCleanup();
7174
8131
 
7175
8132
  // src/cost.ts
7176
- import { readFileSync as readFileSync10, readdirSync as readdirSync6, renameSync as renameSync7 } from "fs";
7177
- import { join as join10 } from "path";
7178
-
7179
- // src/pricing.ts
7180
- var MODEL_PRICING = {
7181
- // Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
7182
- // Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
7183
- // ветвление по времени внутри расчёта стоимости.
7184
- "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
7185
- "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
7186
- // Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
7187
- "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
7188
- "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
7189
- };
7190
- var CACHE_READ_FACTOR = 0.1;
7191
- var CACHE_WRITE_5M_FACTOR = 1.25;
7192
- function costFromUsage(usage, model) {
7193
- const pricing = model ? MODEL_PRICING[model] : void 0;
7194
- if (!pricing) {
7195
- 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` };
7196
- }
7197
- const input = usage.input_tokens ?? 0;
7198
- const output = usage.output_tokens ?? 0;
7199
- const cacheWrite = usage.cache_creation_input_tokens ?? 0;
7200
- const cacheRead = usage.cache_read_input_tokens ?? 0;
7201
- const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
7202
- return { costUsd };
7203
- }
7204
-
7205
- // src/claudeResult.ts
7206
- function parseClaudeResultJson(raw) {
7207
- const trimmed = raw.trim();
7208
- if (!trimmed) return null;
7209
- try {
7210
- const parsed = JSON.parse(trimmed);
7211
- return typeof parsed === "object" && parsed !== null ? parsed : null;
7212
- } catch {
7213
- return null;
7214
- }
7215
- }
7216
- function launchCostUsd(result, model) {
7217
- if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
7218
- return { costUsd: result.total_cost_usd };
7219
- }
7220
- if (!result.usage) return { costUsd: 0 };
7221
- return costFromUsage(result.usage, model);
7222
- }
7223
-
7224
- // 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";
7225
8136
  var UNPROCESSED_LOG_RE = /^\d+\.json$/;
7226
8137
  function unprocessedLogFiles(dir) {
7227
8138
  try {
7228
- return readdirSync6(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
8139
+ return readdirSync8(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
7229
8140
  } catch {
7230
8141
  return [];
7231
8142
  }
7232
8143
  }
7233
8144
  function accountRunCost(deps, run) {
7234
- const dir = join10(deps.runLogsDir, String(run.id));
8145
+ const dir = join13(deps.runLogsDir, String(run.id));
7235
8146
  const files = unprocessedLogFiles(dir);
7236
8147
  if (files.length === 0) return;
7237
8148
  const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8149
+ const executor = deps.executor ?? resolveExecutor();
7238
8150
  let added = 0;
8151
+ let tokensIn = 0;
8152
+ let tokensOut = 0;
8153
+ let durationMs = 0;
7239
8154
  const processed = [];
7240
8155
  for (const f of files) {
7241
- const full = join10(dir, f);
8156
+ const full = join13(dir, f);
7242
8157
  let raw = null;
7243
8158
  try {
7244
8159
  raw = readFileSync10(full, "utf8");
7245
8160
  } catch {
7246
8161
  continue;
7247
8162
  }
7248
- const result = parseClaudeResultJson(raw);
8163
+ const result = executor.parseResult(raw, model);
7249
8164
  if (!result) {
7250
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?)`);
7251
8166
  } else {
7252
- const est = launchCostUsd(result, model);
7253
- if (est.warning) console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${est.warning}`);
7254
- 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;
7255
8172
  }
7256
8173
  processed.push(full);
7257
8174
  }
7258
- 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
+ }
7259
8183
  for (const full of processed) {
7260
8184
  try {
7261
8185
  renameSync7(full, `${full}.done`);
@@ -7268,14 +8192,15 @@ function makeCostAccountant(deps) {
7268
8192
  }
7269
8193
 
7270
8194
  // src/runOutcome.ts
7271
- import { readdirSync as readdirSync7, readFileSync as readFileSync11 } from "fs";
7272
- import { join as join11 } from "path";
8195
+ init_executors();
8196
+ import { readdirSync as readdirSync9, readFileSync as readFileSync11 } from "fs";
8197
+ import { join as join14 } from "path";
7273
8198
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
7274
- function classifyExit(runLogsDir, runId) {
7275
- const dir = join11(runLogsDir, String(runId));
8199
+ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
8200
+ const dir = join14(runLogsDir, String(runId));
7276
8201
  let files;
7277
8202
  try {
7278
- files = readdirSync7(dir);
8203
+ files = readdirSync9(dir);
7279
8204
  } catch {
7280
8205
  return "unknown";
7281
8206
  }
@@ -7289,14 +8214,15 @@ function classifyExit(runLogsDir, runId) {
7289
8214
  if (!latest) return "unknown";
7290
8215
  let raw;
7291
8216
  try {
7292
- raw = readFileSync11(join11(dir, latest.name), "utf8");
8217
+ raw = readFileSync11(join14(dir, latest.name), "utf8");
7293
8218
  } catch {
7294
8219
  return "unknown";
7295
8220
  }
7296
- return parseClaudeResultJson(raw) ? "graceful" : "crash";
8221
+ return executor.parseResult(raw, null) ? "graceful" : "crash";
7297
8222
  }
7298
- function makeExitClassifier(runLogsDir) {
7299
- return (run) => classifyExit(runLogsDir, run.id);
8223
+ function makeExitClassifier(runLogsDir, executor) {
8224
+ const exec = executor ?? resolveExecutor();
8225
+ return (run) => classifyExit(runLogsDir, run.id, exec);
7300
8226
  }
7301
8227
 
7302
8228
  // src/orchestrator.ts
@@ -7304,8 +8230,8 @@ init_src();
7304
8230
  init_git();
7305
8231
 
7306
8232
  // src/deps.ts
7307
- import { existsSync as existsSync9, readdirSync as readdirSync9, readFileSync as readFileSync12 } from "fs";
7308
- import { join as join13 } from "path";
8233
+ import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync12 } from "fs";
8234
+ import { join as join16 } from "path";
7309
8235
  import { execa } from "execa";
7310
8236
  var PREPARE_TIMEOUT_MS = 15 * 6e4;
7311
8237
  var MAX_PLAN_COMMANDS = 12;
@@ -7328,14 +8254,14 @@ function safeReadText(p) {
7328
8254
  }
7329
8255
  function safeListDirs(p) {
7330
8256
  try {
7331
- 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);
7332
8258
  } catch {
7333
8259
  return [];
7334
8260
  }
7335
8261
  }
7336
8262
  function safeListFiles(p) {
7337
8263
  try {
7338
- 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);
7339
8265
  } catch {
7340
8266
  return [];
7341
8267
  }
@@ -7361,21 +8287,21 @@ function overridePlan(override) {
7361
8287
  };
7362
8288
  }
7363
8289
  function detectPackageManager(worktree, io = {}) {
7364
- const exists = io.exists ?? existsSync9;
8290
+ const exists = io.exists ?? existsSync10;
7365
8291
  const readText = io.readText ?? safeReadText;
7366
- const hasPkgJson = exists(join13(worktree, "package.json"));
8292
+ const hasPkgJson = exists(join16(worktree, "package.json"));
7367
8293
  if (!hasPkgJson) {
7368
- if (exists(join13(worktree, "deno.json")) || exists(join13(worktree, "deno.jsonc")) || exists(join13(worktree, "deno.lock"))) return "deno";
8294
+ if (exists(join16(worktree, "deno.json")) || exists(join16(worktree, "deno.jsonc")) || exists(join16(worktree, "deno.lock"))) return "deno";
7369
8295
  return null;
7370
8296
  }
7371
- const pkg = readJson(readText, join13(worktree, "package.json"));
8297
+ const pkg = readJson(readText, join16(worktree, "package.json"));
7372
8298
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
7373
8299
  if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
7374
- if (exists(join13(worktree, "bun.lock")) || exists(join13(worktree, "bun.lockb"))) return "bun";
7375
- if (exists(join13(worktree, "pnpm-lock.yaml"))) return "pnpm";
7376
- if (exists(join13(worktree, "yarn.lock"))) return "yarn";
7377
- if (exists(join13(worktree, "package-lock.json"))) return "npm";
7378
- if (exists(join13(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";
7379
8305
  return "npm";
7380
8306
  }
7381
8307
  function jsInstallCommand(pm) {
@@ -7428,7 +8354,7 @@ var ECOSYSTEM_MARKERS = [
7428
8354
  // конфигурация — дело сборки, не подготовки
7429
8355
  ];
7430
8356
  function detectPlan(worktree, io) {
7431
- const exists = io.exists ?? existsSync9;
8357
+ const exists = io.exists ?? existsSync10;
7432
8358
  const listFiles = io.listFiles ?? safeListFiles;
7433
8359
  const pm = detectPackageManager(worktree, io);
7434
8360
  if (pm) {
@@ -7436,7 +8362,7 @@ function detectPlan(worktree, io) {
7436
8362
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
7437
8363
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
7438
8364
  // заново, но он дешёвый: тянет из того же общего кэша.
7439
- { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join13(worktree, "node_modules"), source: "detect" }
8365
+ { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join16(worktree, "node_modules"), source: "detect" }
7440
8366
  ];
7441
8367
  for (const target of workspaceBuildTargets(worktree, io)) {
7442
8368
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
@@ -7444,12 +8370,12 @@ function detectPlan(worktree, io) {
7444
8370
  return { source: "detect", ecosystem: `node/${pm}`, commands };
7445
8371
  }
7446
8372
  for (const marker of ECOSYSTEM_MARKERS) {
7447
- const hit = marker.files.some((f) => exists(join13(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));
7448
8374
  if (!hit) continue;
7449
8375
  let ecosystem = marker.ecosystem;
7450
8376
  let command = marker.command;
7451
8377
  for (const refine of marker.refine ?? []) {
7452
- if (!exists(join13(worktree, refine.file))) continue;
8378
+ if (!exists(join16(worktree, refine.file))) continue;
7453
8379
  ecosystem = refine.ecosystem;
7454
8380
  command = refine.command;
7455
8381
  break;
@@ -7506,12 +8432,12 @@ var CI_INSTALL_PATTERNS = [
7506
8432
  function ciWorkflowPlan(worktree, io) {
7507
8433
  const readText = io.readText ?? safeReadText;
7508
8434
  const listFiles = io.listFiles ?? safeListFiles;
7509
- const dir = join13(worktree, ".github", "workflows");
8435
+ const dir = join16(worktree, ".github", "workflows");
7510
8436
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
7511
8437
  const seen = /* @__PURE__ */ new Set();
7512
8438
  const commands = [];
7513
8439
  for (const name of files.sort()) {
7514
- const yaml = readText(join13(dir, name));
8440
+ const yaml = readText(join16(dir, name));
7515
8441
  if (yaml === null) continue;
7516
8442
  for (const raw of workflowRunLines(yaml)) {
7517
8443
  const line = raw.replace(/\s+/g, " ").trim();
@@ -7526,7 +8452,7 @@ function ciWorkflowPlan(worktree, io) {
7526
8452
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
7527
8453
  }
7528
8454
  function workspaceGlobs(worktree, readText) {
7529
- const yaml = readText(join13(worktree, "pnpm-workspace.yaml"));
8455
+ const yaml = readText(join16(worktree, "pnpm-workspace.yaml"));
7530
8456
  if (yaml !== null) {
7531
8457
  const globs = [];
7532
8458
  let inPackages = false;
@@ -7546,7 +8472,7 @@ function workspaceGlobs(worktree, readText) {
7546
8472
  }
7547
8473
  if (globs.length) return globs;
7548
8474
  }
7549
- const pkg = readJson(readText, join13(worktree, "package.json"));
8475
+ const pkg = readJson(readText, join16(worktree, "package.json"));
7550
8476
  const ws = pkg?.workspaces;
7551
8477
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
7552
8478
  return list.filter((g) => typeof g === "string");
@@ -7557,14 +8483,14 @@ function workspaceDirs(worktree, globs, io) {
7557
8483
  if (glob.startsWith("!")) continue;
7558
8484
  const star = glob.indexOf("*");
7559
8485
  if (star === -1) {
7560
- const dir = join13(worktree, glob);
7561
- if (io.exists(join13(dir, "package.json"))) dirs.add(dir);
8486
+ const dir = join16(worktree, glob);
8487
+ if (io.exists(join16(dir, "package.json"))) dirs.add(dir);
7562
8488
  continue;
7563
8489
  }
7564
- const base3 = join13(worktree, glob.slice(0, star).replace(/\/$/, ""));
8490
+ const base3 = join16(worktree, glob.slice(0, star).replace(/\/$/, ""));
7565
8491
  for (const name of io.listDirs(base3)) {
7566
- const dir = join13(base3, name);
7567
- if (io.exists(join13(dir, "package.json"))) dirs.add(dir);
8492
+ const dir = join16(base3, name);
8493
+ if (io.exists(join16(dir, "package.json"))) dirs.add(dir);
7568
8494
  }
7569
8495
  }
7570
8496
  return [...dirs];
@@ -7585,18 +8511,18 @@ function entryPaths(pkg) {
7585
8511
  return out;
7586
8512
  }
7587
8513
  function workspaceBuildTargets(worktree, io = {}) {
7588
- const exists = io.exists ?? existsSync9;
8514
+ const exists = io.exists ?? existsSync10;
7589
8515
  const readText = io.readText ?? safeReadText;
7590
8516
  const listDirs = io.listDirs ?? safeListDirs;
7591
8517
  const globs = workspaceGlobs(worktree, readText);
7592
8518
  if (!globs.length) return [];
7593
8519
  const pkgs = [];
7594
8520
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
7595
- const pkg = readJson(readText, join13(dir, "package.json"));
8521
+ const pkg = readJson(readText, join16(dir, "package.json"));
7596
8522
  if (!pkg || typeof pkg.name !== "string") continue;
7597
8523
  const scripts = pkg.scripts ?? {};
7598
8524
  const entries = entryPaths(pkg);
7599
- const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join13(dir, e)));
8525
+ const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join16(dir, e)));
7600
8526
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
7601
8527
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
7602
8528
  }
@@ -7624,7 +8550,7 @@ function planPrepare(worktree, io = {}) {
7624
8550
  return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
7625
8551
  }
7626
8552
  async function ensureWorktreeDeps(worktree, io = {}) {
7627
- const exists = io.exists ?? existsSync9;
8553
+ const exists = io.exists ?? existsSync10;
7628
8554
  const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
7629
8555
  const approve = io.approve ?? (() => true);
7630
8556
  const plan = planPrepare(worktree, io);
@@ -7715,7 +8641,9 @@ var BaseBranchMissingError = class extends Error {
7715
8641
  branch;
7716
8642
  repo;
7717
8643
  };
7718
- var AreaBusyError = class extends Error {
8644
+ var SpawnDeferredError = class extends Error {
8645
+ };
8646
+ var AreaBusyError = class extends SpawnDeferredError {
7719
8647
  constructor(area) {
7720
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`);
7721
8649
  this.area = area;
@@ -7723,6 +8651,14 @@ var AreaBusyError = class extends Error {
7723
8651
  }
7724
8652
  area;
7725
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
+ };
7726
8662
  function healTaskSlug(db, run, task) {
7727
8663
  const slug = task?.slug;
7728
8664
  if (!slug || slug === run.task_slug || !isTaskSlug(slug) || isTaskSlug(run.task_slug)) return run;
@@ -7732,7 +8668,9 @@ function healTaskSlug(db, run, task) {
7732
8668
  async function spawnDrone(deps, run) {
7733
8669
  const repo = normalizeRepo(run.repo || deps.repo || "");
7734
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`);
7735
- 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)) {
7736
8674
  throw new AreaBusyError(run.area);
7737
8675
  }
7738
8676
  const branch = run.task_slug;
@@ -7783,13 +8721,13 @@ function isGithubPrUrl(url) {
7783
8721
  return GH_PR_RE.test(url.trim());
7784
8722
  }
7785
8723
  function softStatus() {
7786
- 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 };
7787
8725
  }
7788
8726
  async function prStatus(prUrl, run = realRunner) {
7789
8727
  if (!isGithubPrUrl(prUrl)) return softStatus();
7790
8728
  let out;
7791
8729
  try {
7792
- 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"]);
7793
8731
  } catch {
7794
8732
  return softStatus();
7795
8733
  }
@@ -7810,9 +8748,20 @@ async function prStatus(prUrl, run = realRunner) {
7810
8748
  // не смержен, открыт и GitHub уверенно говорит CONFLICTING
7811
8749
  conflicting: !merged && state === "OPEN" && mergeable === "CONFLICTING",
7812
8750
  isDraft: j.isDraft === true,
7813
- 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
7814
8753
  };
7815
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
+ }
7816
8765
  async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
7817
8766
  if (!branch || !repoDir) return [];
7818
8767
  try {
@@ -8020,6 +8969,13 @@ function reviewTransitionPatch(run, prUrl) {
8020
8969
  }
8021
8970
  return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
8022
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
+ }
8023
8979
  function applyReviewTransition(db, run, prUrl) {
8024
8980
  const { patch, ...t } = reviewTransitionPatch(run, prUrl);
8025
8981
  db.updateRun(run.id, patch);
@@ -8028,14 +8984,14 @@ function applyReviewTransition(db, run, prUrl) {
8028
8984
 
8029
8985
  // src/droneServer.ts
8030
8986
  init_src();
8031
- 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") {
8032
8988
  return [
8033
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`,
8034
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.",
8035
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:",
8036
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).",
8037
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.",
8038
- `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.`,
8039
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.",
8040
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",
8041
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."
@@ -8068,10 +9024,12 @@ async function buildDroneServer(store, opts = {}) {
8068
9024
  const slugForInstruction = (fallback) => opts.expectedTaskSlug ?? fallback ?? "<slug>";
8069
9025
  const freeze = async (withdraw = false) => {
8070
9026
  let slug;
9027
+ let branch;
8071
9028
  try {
8072
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");
8073
9030
  const run = await store.myRun();
8074
9031
  slug = run?.task_slug;
9032
+ branch = !opts.expectedTaskSlug || opts.expectedTaskSlug === run?.task_slug ? run?.branch : null;
8075
9033
  if (run && run.status !== "paused") {
8076
9034
  await store.updateRun(null, { status: "paused" });
8077
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");
@@ -8079,7 +9037,7 @@ async function buildDroneServer(store, opts = {}) {
8079
9037
  } catch (e) {
8080
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);
8081
9039
  }
8082
- return { content: [{ type: "text", text: freezeInstruction(slugForInstruction(slug), opts.deadReason) }] };
9040
+ return { content: [{ type: "text", text: freezeInstruction(branch || slugForInstruction(slug), opts.deadReason) }] };
8083
9041
  };
8084
9042
  const guard = (fn) => async (a) => {
8085
9043
  try {
@@ -8103,7 +9061,7 @@ async function buildDroneServer(store, opts = {}) {
8103
9061
  );
8104
9062
  server.tool(
8105
9063
  "read_task",
8106
- "\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).",
8107
9065
  {},
8108
9066
  async () => {
8109
9067
  if (!opts.taskless) {
@@ -8316,9 +9274,12 @@ ${report}${shots.warning}`,
8316
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)}`);
8317
9275
  }
8318
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);
8319
9280
  if (run) {
8320
9281
  const t = reviewTransitionPatch(run, pr_url);
8321
- await store.updateRun(null, t.patch);
9282
+ await store.updateRun(null, { ...t.patch, ...branchFix?.patch ?? {} });
8322
9283
  await store.addEvent(null, "pr_opened", pr_url);
8323
9284
  await postReport(reportSlug(run.task_slug), t.cycle ?? run.review_cycle ?? 0);
8324
9285
  if (t.isFixIteration) {
@@ -8331,7 +9292,7 @@ ${report}${shots.warning}`,
8331
9292
  }
8332
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)}` }] };
8333
9294
  }
8334
- await store.updateRun(null, { status: "review", pr_url });
9295
+ await store.updateRun(null, { status: "review", pr_url, ...branchFix?.patch ?? {} });
8335
9296
  await store.addEvent(null, "pr_opened", pr_url);
8336
9297
  const fallbackSlug = reportSlug(void 0);
8337
9298
  if (fallbackSlug) await postReport(fallbackSlug, void 0);
@@ -8496,7 +9457,7 @@ async function runDroneServer(runId) {
8496
9457
  );
8497
9458
  const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
8498
9459
  const expectedKind = toRunKind(process.env.SWARM_RUN_KIND);
8499
- const common = { taskless, runKind: expectedKind, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
9460
+ const common = { taskless, runKind: expectedKind, prHeadBranch, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
8500
9461
  const channelUrl = process.env.SWARM_DRONE_CHANNEL_URL;
8501
9462
  const channelToken = process.env.SWARM_DRONE_CHANNEL_TOKEN;
8502
9463
  if (channelUrl && channelToken) {
@@ -8589,12 +9550,15 @@ var AnomalyTracker = class {
8589
9550
  for (const [k, v] of Object.entries(restored.since)) this.since.set(k, v);
8590
9551
  for (const [k, v] of Object.entries(restored.lastFiredAt)) this.lastFiredAt.set(k, v);
8591
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);
8592
9554
  }
8593
9555
  }
8594
9556
  renotifyCooldownMs;
8595
9557
  since = /* @__PURE__ */ new Map();
8596
9558
  lastFiredAt = /* @__PURE__ */ new Map();
8597
9559
  fires = /* @__PURE__ */ new Map();
9560
+ /** TAS-485: очередь «уже звонили, а условие ушло» — к гашению в инбоксе. */
9561
+ resolved = /* @__PURE__ */ new Set();
8598
9562
  maxRepeats;
8599
9563
  io;
8600
9564
  /** Кандидаты, готовые к сигналу (отлежали minDurationMs, не в кулдауне) — НИЧЕГО не коммитит. */
@@ -8615,6 +9579,7 @@ var AnomalyTracker = class {
8615
9579
  for (const map of [this.since, this.lastFiredAt, this.fires]) {
8616
9580
  for (const k of [...map.keys()]) {
8617
9581
  if (!seen.has(k)) {
9582
+ if (map === this.lastFiredAt) this.resolved.add(k);
8618
9583
  map.delete(k);
8619
9584
  pruned = true;
8620
9585
  }
@@ -8627,8 +9592,25 @@ var AnomalyTracker = class {
8627
9592
  commit(key, now) {
8628
9593
  this.lastFiredAt.set(key, now);
8629
9594
  this.fires.set(key, (this.fires.get(key) ?? 0) + 1);
9595
+ this.resolved.delete(key);
8630
9596
  this.persist();
8631
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
+ }
8632
9614
  /**
8633
9615
  * Следующий сигнал по этому ключу — ПОСЛЕДНИЙ разрешённый в эпизоде: вызывающий меняет
8634
9616
  * формулировку (LAST_REMINDER_SUFFIX), чтобы затихание не выглядело как «рой забыл».
@@ -8644,7 +9626,8 @@ var AnomalyTracker = class {
8644
9626
  this.io.save({
8645
9627
  since: Object.fromEntries(this.since),
8646
9628
  lastFiredAt: Object.fromEntries(this.lastFiredAt),
8647
- fires: Object.fromEntries(this.fires)
9629
+ fires: Object.fromEntries(this.fires),
9630
+ resolved: [...this.resolved]
8648
9631
  });
8649
9632
  } catch (e) {
8650
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);
@@ -8698,8 +9681,9 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8698
9681
  const changedAt = sqliteMs(run.updated_at);
8699
9682
  return changedAt >= q.from && (q.to == null || changedAt <= q.to);
8700
9683
  };
9684
+ const closedBeforeStart = (run) => opts.daemonStartedAt != null && sqliteMs(run.updated_at) < opts.daemonStartedAt;
8701
9685
  const recentFailed = allRuns.filter(
8702
- (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)
8703
9687
  );
8704
9688
  if (recentFailed.length >= thresholds.failedSpikeCount) {
8705
9689
  candidates.push({
@@ -8736,6 +9720,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8736
9720
  if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
8737
9721
  if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
8738
9722
  if (killedByOperator(run)) continue;
9723
+ if (closedBeforeStart(run)) continue;
8739
9724
  const repeat = isRepeatDown(run);
8740
9725
  candidates.push({
8741
9726
  key: `run_stalled:${run.id}`,
@@ -8756,6 +9741,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8756
9741
  if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
8757
9742
  if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
8758
9743
  if (killedByOperator(run)) continue;
9744
+ if (closedBeforeStart(run)) continue;
8759
9745
  const repeat = isRepeatDown(run);
8760
9746
  candidates.push({
8761
9747
  key: `run_failed:${run.id}`,
@@ -9175,22 +10161,22 @@ function makeExclusiveReader(opts) {
9175
10161
  }
9176
10162
 
9177
10163
  // src/envGate.ts
9178
- import { existsSync as existsSync12 } from "fs";
9179
- import { join as join20 } from "path";
10164
+ import { existsSync as existsSync13 } from "fs";
10165
+ import { join as join23 } from "path";
9180
10166
  import { createConnection } from "net";
9181
10167
  import { execa as execa2 } from "execa";
9182
10168
 
9183
10169
  // src/platform.ts
9184
- import { existsSync as existsSync11, readFileSync as readFileSync17 } from "fs";
9185
- import { delimiter, join as join19 } from "path";
10170
+ import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
10171
+ import { delimiter, join as join22 } from "path";
9186
10172
  function resolveCommandPath(name, env = process.env, platform = process.platform) {
9187
- if (name.includes("/") || name.includes("\\")) return existsSync11(name) ? name : null;
10173
+ if (name.includes("/") || name.includes("\\")) return existsSync12(name) ? name : null;
9188
10174
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
9189
10175
  const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
9190
10176
  for (const dir of dirs) {
9191
10177
  for (const ext of exts) {
9192
- const candidate = join19(dir, name + ext);
9193
- if (existsSync11(candidate)) return candidate;
10178
+ const candidate = join22(dir, name + ext);
10179
+ if (existsSync12(candidate)) return candidate;
9194
10180
  }
9195
10181
  }
9196
10182
  return null;
@@ -9248,8 +10234,8 @@ var COMPOSE_MARKERS = [
9248
10234
  "compose.yaml"
9249
10235
  ];
9250
10236
  function detectRequirements(repo, io = {}) {
9251
- const exists = io.exists ?? existsSync12;
9252
- const marker = COMPOSE_MARKERS.find((f) => exists(join20(repo, f)));
10237
+ const exists = io.exists ?? existsSync13;
10238
+ const marker = COMPOSE_MARKERS.find((f) => exists(join23(repo, f)));
9253
10239
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
9254
10240
  }
9255
10241
  function resolveRequirements(repo, declared, io = {}) {
@@ -9385,8 +10371,8 @@ import { basename as basename3 } from "path";
9385
10371
  import { serve as serve2 } from "@hono/node-server";
9386
10372
 
9387
10373
  // src/diff.ts
9388
- import { statSync as statSync7 } from "fs";
9389
- import { join as join21 } from "path";
10374
+ import { statSync as statSync8 } from "fs";
10375
+ import { join as join24 } from "path";
9390
10376
  import { $ as $4 } from "execa";
9391
10377
  var MAX_FILES = 200;
9392
10378
  var MAX_LINES_PER_FILE = 1500;
@@ -9529,7 +10515,7 @@ async function resolveBase(wt, baseBranch) {
9529
10515
  }
9530
10516
  function mtime(path) {
9531
10517
  try {
9532
- return statSync7(path).mtimeMs;
10518
+ return statSync8(path).mtimeMs;
9533
10519
  } catch {
9534
10520
  return 0;
9535
10521
  }
@@ -9554,7 +10540,7 @@ async function collectRunDiff(worktree, opts = {}) {
9554
10540
  files.push(pf);
9555
10541
  }
9556
10542
  }
9557
- files.sort((a, b) => mtime(join21(worktree, b.path)) - mtime(join21(worktree, a.path)));
10543
+ files.sort((a, b) => mtime(join24(worktree, b.path)) - mtime(join24(worktree, a.path)));
9558
10544
  const truncatedFiles = files.length > MAX_FILES;
9559
10545
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
9560
10546
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -9569,11 +10555,25 @@ async function collectRunDiff(worktree, opts = {}) {
9569
10555
  ...truncatedFiles ? { truncated: true } : {}
9570
10556
  };
9571
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
+ }
9572
10572
 
9573
10573
  // package.json
9574
10574
  var package_default = {
9575
10575
  name: "@taskless-app/swarm",
9576
- version: "0.5.0",
10576
+ version: "0.6.0",
9577
10577
  type: "module",
9578
10578
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
9579
10579
  keywords: [
@@ -9690,6 +10690,10 @@ function roySnapshot(db, now = Date.now(), overmindLog = [], paused = false, dae
9690
10690
  stopReason: r.stop_reason,
9691
10691
  reason,
9692
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})
9693
10697
  step,
9694
10698
  hasDecision: withQ.has(r.id),
9695
10699
  rescued: evs.some((e) => e.kind === "rescued"),
@@ -11237,6 +12241,7 @@ async function checkDaemonVersion(start, now = () => /* @__PURE__ */ new Date())
11237
12241
  }
11238
12242
 
11239
12243
  // src/release.ts
12244
+ init_claudeBin();
11240
12245
  var SWARM_PACKAGE = "@taskless-app/swarm";
11241
12246
  var LATEST_TIMEOUT_MS = 4e3;
11242
12247
  async function fetchLatestVersion(opts = {}) {
@@ -11271,9 +12276,9 @@ function outdatedLine(current, latest, updateCommand = "swarm update") {
11271
12276
  // src/hostCommands.ts
11272
12277
  init_src();
11273
12278
  import { readFileSync as readFileSync18, renameSync as renameSync8, writeFileSync as writeFileSync13 } from "fs";
11274
- import { join as join22 } from "path";
12279
+ import { join as join25 } from "path";
11275
12280
  function hostCommandStatePath(home) {
11276
- return join22(home, "host-command.json");
12281
+ return join25(home, "host-command.json");
11277
12282
  }
11278
12283
  function readState(path) {
11279
12284
  try {
@@ -11383,6 +12388,28 @@ async function executeOne(cmd, opts) {
11383
12388
  await ack(cmd.id, "done", { result: `\u043F\u0440\u043E\u0433\u043E\u043D ${slug} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D` });
11384
12389
  return false;
11385
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
+ }
11386
12413
  case "restart":
11387
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");
11388
12415
  case "update": {
@@ -11420,8 +12447,8 @@ async function doRestart(cmd, opts, note) {
11420
12447
 
11421
12448
  // src/selfUpdate.ts
11422
12449
  init_git();
11423
- import { existsSync as existsSync13 } from "fs";
11424
- import { join as join23 } from "path";
12450
+ import { existsSync as existsSync14 } from "fs";
12451
+ import { join as join26 } from "path";
11425
12452
  import { $ as $6 } from "execa";
11426
12453
  var DAEMON_PACKAGE = "@taskless-app/swarm";
11427
12454
  var defaultRun = async (cmd, args, cwd) => {
@@ -11434,7 +12461,7 @@ function tail(text, max = 400) {
11434
12461
  }
11435
12462
  async function runSelfUpdate(io) {
11436
12463
  const run = io.run ?? defaultRun;
11437
- const exists = io.exists ?? existsSync13;
12464
+ const exists = io.exists ?? existsSync14;
11438
12465
  const repo = io.repo;
11439
12466
  await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
11440
12467
  const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
@@ -11463,7 +12490,7 @@ async function runSelfUpdate(io) {
11463
12490
  };
11464
12491
  }
11465
12492
  await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
11466
- if (!exists(join23(repo, "pnpm-lock.yaml"))) {
12493
+ if (!exists(join26(repo, "pnpm-lock.yaml"))) {
11467
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" };
11468
12495
  }
11469
12496
  const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
@@ -11497,11 +12524,12 @@ function makeOvermindNarrator() {
11497
12524
  }
11498
12525
 
11499
12526
  // src/probe.ts
12527
+ init_executors();
11500
12528
  import { spawn as spawn2 } from "child_process";
11501
- var LIMIT_RE = /(weekly limit|usage limit|hit your .*limit)/i;
11502
12529
  var RESET_RE = /resets[^\n]*/i;
11503
- function anthropicHealthy(opts = {}) {
11504
- const bin = opts.claudeBin ?? resolveClaudeBin();
12530
+ function executorHealthy(opts = {}) {
12531
+ const executor = opts.executor ?? resolveExecutor();
12532
+ const bin = opts.bin ?? executor.resolveBin();
11505
12533
  const timeoutMs = opts.timeoutMs ?? 2e4;
11506
12534
  return new Promise((resolve4) => {
11507
12535
  let done = false;
@@ -11516,7 +12544,7 @@ function anthropicHealthy(opts = {}) {
11516
12544
  }
11517
12545
  resolve4(r);
11518
12546
  };
11519
- 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"] });
11520
12548
  child.stdout?.on("data", (d) => {
11521
12549
  out += d.toString();
11522
12550
  });
@@ -11527,7 +12555,9 @@ function anthropicHealthy(opts = {}) {
11527
12555
  child.on("error", () => finish({ ok: false, reason: "exec", resetHint: null }));
11528
12556
  child.on("exit", (code) => {
11529
12557
  if (code === 0) return finish({ ok: true, reason: "ok", resetHint: null });
11530
- 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
+ }
11531
12561
  finish({ ok: false, reason: "provider", resetHint: null });
11532
12562
  });
11533
12563
  });
@@ -11553,18 +12583,21 @@ async function fetchExternalStorm(url, opts = {}) {
11553
12583
  }
11554
12584
 
11555
12585
  // src/anomalyState.ts
11556
- import { existsSync as existsSync14, readFileSync as readFileSync19, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
11557
- import { join as join24 } 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";
11558
12588
  var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
11559
- var EMPTY = { since: {}, lastFiredAt: {}, fires: {}, stopQuiet: null };
12589
+ var MAX_RESOLVED = 200;
12590
+ function emptyState() {
12591
+ return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
12592
+ }
11560
12593
  function anomalyStatePath(home) {
11561
- return join24(home, "anomaly-state.json");
12594
+ return join27(home, "anomaly-state.json");
11562
12595
  }
11563
12596
  function loadAnomalyState(home, now = Date.now(), io = {}) {
11564
- const exists = io.exists ?? existsSync14;
12597
+ const exists = io.exists ?? existsSync15;
11565
12598
  const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
11566
12599
  const path = anomalyStatePath(home);
11567
- if (!exists(path)) return { ...EMPTY };
12600
+ if (!exists(path)) return emptyState();
11568
12601
  try {
11569
12602
  const raw = JSON.parse(readFile2(path));
11570
12603
  const since = numbers(raw.since);
@@ -11579,10 +12612,14 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
11579
12612
  since: pick(since, fresh),
11580
12613
  lastFiredAt: pick(lastFiredAt, fresh),
11581
12614
  fires: pick(fires, fresh),
12615
+ // TAS-485: очередь гашения НЕ фильтруем тем же TTL, что ключи. Она про
12616
+ // уведомление, которое УЖЕ висит у человека в инбоксе: пролежав неделю
12617
+ // недоставленным, гашение не становится лишним — оно становится нужнее.
12618
+ resolved: strings(raw.resolved).slice(0, MAX_RESOLVED),
11582
12619
  stopQuiet: parseQuiet(raw.stopQuiet)
11583
12620
  };
11584
12621
  } catch {
11585
- return { ...EMPTY };
12622
+ return emptyState();
11586
12623
  }
11587
12624
  }
11588
12625
  function saveAnomalyState(home, state, io = {}) {
@@ -11605,9 +12642,14 @@ var AnomalyStateStore = class {
11605
12642
  /** Инъекция в AnomalyTracker (см. AnomalyTrackerOpts.io). */
11606
12643
  trackerIo() {
11607
12644
  return {
11608
- 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
+ }),
11609
12651
  save: (snapshot) => {
11610
- this.state = { ...snapshot, stopQuiet: this.state.stopQuiet };
12652
+ this.state = { ...snapshot, resolved: (snapshot.resolved ?? []).slice(0, MAX_RESOLVED), stopQuiet: this.state.stopQuiet };
11611
12653
  this.flush();
11612
12654
  }
11613
12655
  };
@@ -11649,6 +12691,10 @@ function numbers(raw) {
11649
12691
  }
11650
12692
  return out;
11651
12693
  }
12694
+ function strings(raw) {
12695
+ if (!Array.isArray(raw)) return [];
12696
+ return raw.filter((v) => typeof v === "string" && v.length > 0);
12697
+ }
11652
12698
  function pick(src, keys) {
11653
12699
  const out = {};
11654
12700
  for (const k of keys) if (src[k] != null) out[k] = src[k];
@@ -11681,7 +12727,7 @@ function isRepeatEvent(prev, cur) {
11681
12727
  }
11682
12728
  function swarmLastActivityMs(db) {
11683
12729
  let latest = null;
11684
- const bump = (iso) => {
12730
+ const bump2 = (iso) => {
11685
12731
  const ms = sqliteMs(iso);
11686
12732
  if (latest == null || ms > latest) latest = ms;
11687
12733
  };
@@ -11689,12 +12735,12 @@ function swarmLastActivityMs(db) {
11689
12735
  if (isTerminalStatus(run.status)) continue;
11690
12736
  const evs = db.events(run.id);
11691
12737
  if (!evs.length) {
11692
- bump(run.updated_at);
12738
+ bump2(run.updated_at);
11693
12739
  continue;
11694
12740
  }
11695
12741
  let i = evs.length - 1;
11696
12742
  while (i > 0 && isRepeatEvent(evs[i - 1], evs[i])) i--;
11697
- bump(evs[i].at);
12743
+ bump2(evs[i].at);
11698
12744
  }
11699
12745
  return latest;
11700
12746
  }
@@ -11820,7 +12866,193 @@ function decideWatchdogAction(db, now, state, thresholds, droneCap, wt, exclusiv
11820
12866
  // src/overmind.ts
11821
12867
  init_storm();
11822
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;
11823
13054
  var THIS_FILE = fileURLToPath2(import.meta.url);
13055
+ var executorHasSession = (p) => EXECUTOR.session.exists(p);
11824
13056
  var OVERMIND_ENTRY = THIS_FILE;
11825
13057
  var STALL_MS = Number(process.env.SWARM_STALL_MS) || 10 * 60 * 1e3;
11826
13058
  var STALL_FLOOR_MS = Number(process.env.SWARM_STALL_FLOOR_MS) || 30 * 60 * 1e3;
@@ -11834,6 +13066,10 @@ var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
11834
13066
  var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
11835
13067
  var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
11836
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
+ }
11837
13073
  var REVIEW_CYCLE_LIMIT = Number(process.env.SWARM_REVIEW_CYCLE_LIMIT) || 3;
11838
13074
  var REVIEW_CYCLE_ESCALATION_MARK = "\u0440\u0435-\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0446\u0438\u043A\u043B\u0438\u043B\u043E\u0441\u044C";
11839
13075
  var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
@@ -11860,6 +13096,7 @@ var CHANGES_WAKE_ESCALATION_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\
11860
13096
  var NEEDS_HUMAN_MARKS = [
11861
13097
  RESOLVE_ESCALATION_MARK,
11862
13098
  REVIEW_ESCALATION_MARK,
13099
+ REVIEW_UNRECOVERABLE_MARK,
11863
13100
  REVIEW_CYCLE_ESCALATION_MARK,
11864
13101
  CI_WAKE_ESCALATION_MARK,
11865
13102
  CHANGES_WAKE_ESCALATION_MARK
@@ -12188,8 +13425,12 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
12188
13425
  // авторские подписи внутри него уже приведены к одной строке (renderAttachmentsBlock).
12189
13426
  ...attachmentsBlock ? ["", attachmentsBlock] : [],
12190
13427
  "",
12191
- "\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.",
12192
- "\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).",
12193
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).",
12194
13435
  // TASWM-293: живой случай SCE-5 — ревьюер заблокировал PR за отсутствие CI (у проекта
12195
13436
  // его нет и не предполагается), автора разбудили «закрывать замечания», замечаний по коду
@@ -12206,7 +13447,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
12206
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.`,
12207
13448
  "",
12208
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:",
12209
- `\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.`,
12210
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.",
12211
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.',
12212
13453
  "",
@@ -12231,7 +13472,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
12231
13472
  }
12232
13473
  return parts.join("\n");
12233
13474
  }
12234
- var HERO_CHARTER_PATH = join25(dirname5(THIS_FILE), "..", "docs", "HERO.md");
13475
+ var HERO_CHARTER_PATH = join28(dirname5(THIS_FILE), "..", "docs", "HERO.md");
12235
13476
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
12236
13477
  const name = run.name || `#${run.id}`;
12237
13478
  const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
@@ -12485,8 +13726,8 @@ async function wakeRun(deps, runId) {
12485
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` };
12486
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` };
12487
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` };
12488
- const worktreeExists = deps.worktreeExists ?? existsSync15;
12489
- const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
13729
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
13730
+ const hasTranscript = deps.hasTranscript ?? executorHasSession;
12490
13731
  const wt = run.worktree_path;
12491
13732
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
12492
13733
  const isService = run.reviews_run_id != null || run.resolves_run_id != null;
@@ -12570,9 +13811,9 @@ function repoHasCi(deps, run) {
12570
13811
  function repoHasCiWorkflows(worktreePath, repo) {
12571
13812
  for (const base3 of [worktreePath, repo]) {
12572
13813
  if (!base3) continue;
12573
- const dir = join25(base3, ".github", "workflows");
13814
+ const dir = join28(base3, ".github", "workflows");
12574
13815
  try {
12575
- 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;
12576
13817
  } catch {
12577
13818
  }
12578
13819
  }
@@ -12719,8 +13960,8 @@ async function wakeReviewerOnCiReady(deps, run, over) {
12719
13960
  return true;
12720
13961
  }
12721
13962
  function worktreeAndTranscriptAlive(deps, worktree) {
12722
- const exists = deps.worktreeExists ?? existsSync15;
12723
- const transcript = deps.hasTranscript ?? sessionTranscriptExists;
13963
+ const exists = deps.worktreeExists ?? existsSync16;
13964
+ const transcript = deps.hasTranscript ?? executorHasSession;
12724
13965
  return exists(worktree) && transcript(worktree);
12725
13966
  }
12726
13967
  async function spawnFreshReviewerAfterStall(deps, run, why) {
@@ -12823,6 +14064,22 @@ async function envGateAllows(deps, run) {
12823
14064
  function effectiveRepo(deps, run) {
12824
14065
  return normalizeRepo(run.repo || deps.repoFallback || "");
12825
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
+ }
12826
14083
  function activeDronesOnRepo(deps, repo, exceptRunId) {
12827
14084
  return deps.db.listRuns().filter(
12828
14085
  (r) => r.id !== exceptRunId && (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status) && effectiveRepo(deps, r) === repo
@@ -12863,10 +14120,10 @@ async function exclusiveGateAllows(deps, run) {
12863
14120
  return true;
12864
14121
  }
12865
14122
  function pruneGateLogs(deps) {
12866
- if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size) return;
14123
+ if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size && !deps.overlapLogged?.size) return;
12867
14124
  const open = /* @__PURE__ */ new Set();
12868
14125
  for (const run of deps.db.listRuns()) if (!isTerminalStatus(run.status)) open.add(run.id);
12869
- for (const map of [deps.envGateLogged, deps.exclusiveLogged]) {
14126
+ for (const map of [deps.envGateLogged, deps.exclusiveLogged, deps.overlapLogged]) {
12870
14127
  if (!map) continue;
12871
14128
  for (const id of [...map.keys()]) if (!open.has(id)) map.delete(id);
12872
14129
  }
@@ -12887,6 +14144,18 @@ async function exclusivityBlockedRunIds(deps) {
12887
14144
  }
12888
14145
  return blocked;
12889
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
+ }
12890
14159
  async function trySpawnQueuedRun(deps, run) {
12891
14160
  if (deps.control?.paused) return "skipped";
12892
14161
  const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
@@ -12902,11 +14171,21 @@ async function trySpawnQueuedRun(deps, run) {
12902
14171
  if (!await exclusiveGateAllows(deps, run)) return "skipped";
12903
14172
  if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
12904
14173
  if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
14174
+ await fillSuggestedArea(deps, run);
12905
14175
  try {
12906
14176
  await deps.spawn(run);
12907
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)}.`);
12908
14178
  return "spawned";
12909
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
+ }
12910
14189
  if (e instanceof AreaBusyError) return "skipped";
12911
14190
  deps.db.updateRun(run.id, { status: "failed" });
12912
14191
  deps.db.addEvent(run.id, "failed", String(e));
@@ -12933,8 +14212,8 @@ async function resumeWatchdogParked(deps, droneCap) {
12933
14212
  }
12934
14213
  if (!candidate) return;
12935
14214
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
12936
- const worktreeExists = deps.worktreeExists ?? existsSync15;
12937
- const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
14215
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
14216
+ const hasTranscript = deps.hasTranscript ?? executorHasSession;
12938
14217
  const wt = candidate.worktree_path;
12939
14218
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
12940
14219
  const mode = canContinue ? "continue" : "fresh";
@@ -13102,8 +14381,8 @@ async function manageRelief(deps, now, droneCap, stormActive) {
13102
14381
  if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
13103
14382
  if (!deps.respawn) return;
13104
14383
  const wt = resumeCandidate.worktree_path;
13105
- const worktreeExists = deps.worktreeExists ?? existsSync15;
13106
- const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
14384
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
14385
+ const hasTranscript = deps.hasTranscript ?? executorHasSession;
13107
14386
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
13108
14387
  const mode = canContinue ? "continue" : "fresh";
13109
14388
  if (mode === "continue" && wt && deps.reclaimWorktree) {
@@ -13226,15 +14505,28 @@ ${journal}`);
13226
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)}`);
13227
14506
  });
13228
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
+ }
13229
14520
  async function adoptDroneToReview(deps, run, prUrl) {
13230
14521
  const t = applyReviewTransition(deps.db, run, prUrl);
14522
+ const adopted = syncBranchFromPr(deps.db, run, deps.prHeadBranch ? await deps.prHeadBranch(prUrl).catch(() => null) : null);
13231
14523
  deps.db.addEvent(run.id, "pr_opened", prUrl);
13232
14524
  deps.db.addEvent(
13233
14525
  run.id,
13234
14526
  "step",
13235
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}`
13236
14528
  );
13237
- await postAdoptionReport(deps, run, prUrl, t.cycle ?? run.review_cycle ?? 0);
14529
+ await postAdoptionReport(deps, adopted, prUrl, t.cycle ?? run.review_cycle ?? 0);
13238
14530
  }
13239
14531
  async function postScrapedVerdictReport(deps, runId, taskSlug, verdict, droneName, workspaceId, cycle) {
13240
14532
  if (!deps.reportComment) return;
@@ -13264,8 +14556,8 @@ function buildEscalationCandidates(db) {
13264
14556
  }
13265
14557
  async function tickOnce(deps) {
13266
14558
  const now = deps.now ? deps.now() : Date.now();
13267
- const worktreeExists = deps.worktreeExists ?? existsSync15;
13268
- const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
14559
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
14560
+ const hasTranscript = deps.hasTranscript ?? executorHasSession;
13269
14561
  if (deps.rehydrate) {
13270
14562
  const rh = deps.rehydrate;
13271
14563
  const sinceLast = rh.lastAt == null ? Infinity : now - rh.lastAt;
@@ -13391,7 +14683,7 @@ async function tickOnce(deps) {
13391
14683
  if (!stormActive) {
13392
14684
  await resumeWatchdogParked(deps, droneCap);
13393
14685
  }
13394
- for (const run of deps.db.listRuns("review")) {
14686
+ for (let run of deps.db.listRuns("review")) {
13395
14687
  try {
13396
14688
  if (run.resolves_run_id != null) {
13397
14689
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
@@ -13410,7 +14702,9 @@ async function tickOnce(deps) {
13410
14702
  continue;
13411
14703
  }
13412
14704
  if (!run.pr_url) continue;
13413
- 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);
13414
14708
  if (st.merged) {
13415
14709
  await finalizeMergedReview(deps, run);
13416
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.`);
@@ -13479,11 +14773,13 @@ async function tickOnce(deps) {
13479
14773
  continue;
13480
14774
  }
13481
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);
13482
14778
  let verdictFromComment = null;
13483
14779
  let lastAttempt = null;
13484
14780
  if (attempts > 0 && deps.verdictComment) {
13485
14781
  lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
13486
- 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);
13487
14783
  }
13488
14784
  if (verdictFromComment) {
13489
14785
  const mark = verdictCommentMark(verdictFromComment.createdAt);
@@ -13498,15 +14794,16 @@ async function tickOnce(deps) {
13498
14794
  await wakeAuthorOnChanges(deps, run, now, droneCap);
13499
14795
  }
13500
14796
  }
14797
+ } else if (runEvents.some((e) => (e.text ?? "").includes(unrecoverableMark))) {
13501
14798
  } else if (attempts >= REVIEW_SPAWN_LIMIT) {
13502
- 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))) {
13503
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`);
13504
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.`);
13505
14802
  }
13506
14803
  } else {
13507
14804
  const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
13508
14805
  if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk) {
13509
- const freshSt = await deps.prStatus(run.pr_url);
14806
+ const freshSt = await deps.prStatus(prUrl);
13510
14807
  if (freshSt.merged) {
13511
14808
  await finalizeMergedReview(deps, run);
13512
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.`);
@@ -13519,11 +14816,21 @@ async function tickOnce(deps) {
13519
14816
  } else if (freshSt.conflicting || freshSt.isDraft) {
13520
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");
13521
14818
  } else {
14819
+ run = syncBranchFromPr(deps.db, run, freshSt.headBranch);
13522
14820
  try {
13523
14821
  await deps.spawnReviewer(run, st.files ?? []);
13524
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.`);
13525
14823
  } catch (e) {
13526
- 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
+ }
13527
14834
  }
13528
14835
  }
13529
14836
  }
@@ -13790,7 +15097,9 @@ async function tickOnce(deps) {
13790
15097
  // resource-gate-stall (TASWM-212): та же причина, что видит лог manageRelief — один
13791
15098
  // deps.gateReason() на тик (кэш hostReading() внутри resources.ts, лишнего спавна нет).
13792
15099
  ...deps.gateReason ? { gateReason: deps.gateReason() } : {},
13793
- ...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {}
15100
+ ...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {},
15101
+ // Рестарт не переигрывает падения (TAS-485) — см. closedBeforeStart в anomalies.ts.
15102
+ ...az.startedAt != null ? { daemonStartedAt: az.startedAt } : {}
13794
15103
  }),
13795
15104
  ...buildEscalationCandidates(deps.db),
13796
15105
  // ЦЕНА РАЗГРУЗКИ (TASWM-316): «машина не тянет» с числами и рекомендацией — та же
@@ -13820,6 +15129,20 @@ async function tickOnce(deps) {
13820
15129
  }
13821
15130
  narrate(deps, `\u26A0\uFE0F ${signal.title}`);
13822
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
+ }
13823
15146
  }
13824
15147
  if (deps.watchdog) {
13825
15148
  try {
@@ -13885,7 +15208,7 @@ async function cancelRun(deps, slug, reason) {
13885
15208
  }
13886
15209
  async function resolveConflict(deps, reviewRun) {
13887
15210
  const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
13888
- const branch = reviewRun.branch || reviewRun.task_slug;
15211
+ const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
13889
15212
  const resolver = deps.db.createRun({
13890
15213
  task_slug: reviewRun.task_slug,
13891
15214
  repo,
@@ -13895,7 +15218,7 @@ async function resolveConflict(deps, reviewRun) {
13895
15218
  workspace_id: reviewRun.workspace_id
13896
15219
  // та же задача, тот же воркспейс (TASWM-288)
13897
15220
  });
13898
- const wt = join25(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
15221
+ const wt = join28(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
13899
15222
  try {
13900
15223
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
13901
15224
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -13925,14 +15248,14 @@ async function resolveConflict(deps, reviewRun) {
13925
15248
  await deps.git.removeWorktree(repo, wt).catch(() => {
13926
15249
  });
13927
15250
  deps.db.updateRun(resolver.id, { status: "failed" });
13928
- 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";
13929
15252
  deps.db.addEvent(resolver.id, "failed", `${marker}: ${String(e)}`);
13930
15253
  throw e;
13931
15254
  }
13932
15255
  }
13933
15256
  async function spawnReviewer(deps, reviewRun, files) {
13934
15257
  const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
13935
- const branch = reviewRun.branch || reviewRun.task_slug;
15258
+ const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
13936
15259
  const priorReview = reviewRun.review_cycle > 0 ? lastChangesSummary(deps.db, reviewRun.id) : null;
13937
15260
  const verifyMode = deps.verifyMode ? await deps.verifyMode(repo).catch(() => void 0) : void 0;
13938
15261
  const checklist = buildReviewChecklist(reviewRun, files, priorReview, verifyMode);
@@ -13947,7 +15270,7 @@ async function spawnReviewer(deps, reviewRun, files) {
13947
15270
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
13948
15271
  workspace_id: reviewRun.workspace_id
13949
15272
  });
13950
- const wt = join25(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
15273
+ const wt = join28(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
13951
15274
  try {
13952
15275
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
13953
15276
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -13982,7 +15305,7 @@ async function respawnRun(deps, run, mode, nudge) {
13982
15305
  ]);
13983
15306
  const base3 = effectiveBaseBranch(run.base_branch, projectBase);
13984
15307
  const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
13985
- if (!existsSync15(wt)) {
15308
+ if (!existsSync16(wt)) {
13986
15309
  const branch = runToSpawn.branch || runToSpawn.task_slug;
13987
15310
  const source = await addWorktree(repo, wt, branch, {
13988
15311
  ...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
@@ -14011,7 +15334,7 @@ ${notice}` : freshPrompt;
14011
15334
  deps.db.updateRun(run.id, { status: "running", pid });
14012
15335
  }
14013
15336
  async function syncCrashedOnStartup(deps) {
14014
- const worktreeExists = deps.worktreeExists ?? existsSync15;
15337
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
14015
15338
  let orphaned = 0;
14016
15339
  let merged = 0;
14017
15340
  let requeued = 0;
@@ -14074,7 +15397,7 @@ async function makeStore(repo, worktreesDir, repoFlag) {
14074
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`);
14075
15398
  }
14076
15399
  }
14077
- const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW, homedir4(), repoPaths);
15400
+ const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW, homedir5(), repoPaths);
14078
15401
  const repoMap = emptyRepoMapState(scanRootsList);
14079
15402
  if (SWARM_BACKEND !== "client") {
14080
15403
  const { SwarmDb: SwarmDb2 } = await loadSqlite();
@@ -14148,10 +15471,10 @@ async function migrateWorktreesAtStartup() {
14148
15471
  legacyDir: WORKTREES_LEGACY_DIR,
14149
15472
  targetDir: WORKTREES_DIR,
14150
15473
  envOverride: process.env.SWARM_WORKTREES || "",
14151
- exists: (d) => existsSync15(d),
15474
+ exists: (d) => existsSync16(d),
14152
15475
  isNonEmptyDir: (d) => {
14153
15476
  try {
14154
- return readdirSync10(d).length > 0;
15477
+ return readdirSync12(d).length > 0;
14155
15478
  } catch {
14156
15479
  return false;
14157
15480
  }
@@ -14163,7 +15486,7 @@ async function migrateWorktreesAtStartup() {
14163
15486
  });
14164
15487
  } catch (e) {
14165
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);
14166
- return existsSync15(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
15489
+ return existsSync16(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
14167
15490
  }
14168
15491
  }
14169
15492
  async function reportWorkspaceAccess(tl, db) {
@@ -14233,18 +15556,23 @@ async function runDaemon(opts) {
14233
15556
  overmindEntry: THIS_FILE,
14234
15557
  dbPath: DB_PATH,
14235
15558
  runLogsDir: RUN_LOGS_DIR,
15559
+ // Кем исполнять (TASWM-349) — настройка SWARM_EXECUTOR, один резолв на весь демон.
15560
+ executor: EXECUTOR,
14236
15561
  droneIsolation: DRONE_ISOLATION,
14237
15562
  channel: droneChannelLaunch,
14238
15563
  droneCap: () => lastDroneCap
14239
15564
  });
14240
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 }));
14241
15568
  const baseSpawn = {
14242
15569
  db,
14243
15570
  repo: opts.repo,
14244
15571
  // фолбэк, если у прогона repo пуст
14245
15572
  worktreesDir,
14246
15573
  launch,
14247
- 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)
14248
15576
  };
14249
15577
  let intake;
14250
15578
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_MAX_IN_FLIGHT > 0) {
@@ -14278,7 +15606,7 @@ async function runDaemon(opts) {
14278
15606
  const client = db;
14279
15607
  heartbeat = {
14280
15608
  push: async () => {
14281
- const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null);
15609
+ const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastDroneCap);
14282
15610
  if (reply.commands.length) onHostCommands(reply.commands);
14283
15611
  if (reply.logRequests.length) {
14284
15612
  await serveLogRequests(
@@ -14348,7 +15676,14 @@ async function runDaemon(opts) {
14348
15676
  autoReviewEnabled: SWARM_AUTO_REVIEW,
14349
15677
  outboxPendingCount: () => client.pendingCount(),
14350
15678
  notify: (signal) => client.notifyAnomaly(signal),
14351
- 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()
14352
15687
  };
14353
15688
  }
14354
15689
  const watchdog = {
@@ -14421,6 +15756,9 @@ async function runDaemon(opts) {
14421
15756
  worktreesDir,
14422
15757
  launch,
14423
15758
  git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
15759
+ // TASWM-356: ветка ревьюера — фактическая ветка PR. Штатно она уже в поле прогона
15760
+ // (реконсайл синхронит её из headBranch), этот ридер — для прогонов без неё.
15761
+ prHeadBranch,
14424
15762
  // TASWM-293: ТОТ ЖЕ живой ридер verifyMode, что у исполнителя (spawnDrone) —
14425
15763
  // иначе ревьюер судит проект с verifyMode=local по правилам ci и блокирует PR
14426
15764
  // за отсутствие CI, которого у проекта нет и не предполагается.
@@ -14513,7 +15851,7 @@ async function runDaemon(opts) {
14513
15851
  if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
14514
15852
  if (taskClient) await taskClient.startTask(runToSpawn.task_slug);
14515
15853
  },
14516
- 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 })),
14517
15855
  // Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
14518
15856
  // (фолбэк — дефолт-репо роя). Реальный gh; логика усыновления — в tickOnce (тестируется).
14519
15857
  openPrsForBranch: (branch, repoDir) => openPrsForBranch(branch, repoDir || opts.repo),
@@ -14537,6 +15875,8 @@ async function runDaemon(opts) {
14537
15875
  reportComment: (slug, body, label, ws) => tl.forWorkspace(ws).addComment(slug, body, label)
14538
15876
  } : {},
14539
15877
  prCommitTitles: (prUrl) => prCommitTitles(prUrl),
15878
+ prHeadBranch: (prUrl) => prHeadBranch(prUrl),
15879
+ // TASWM-356: ветка PR на усыновлении, узким запросом
14540
15880
  journalForBranch: (repo, branch, slug) => journalForBranch(repo || opts.repo, branch, slug),
14541
15881
  removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p),
14542
15882
  // TASWM-303: тот же фолбэк repo, что у removeWorktree/journalForBranch — run.repo пуст
@@ -14546,15 +15886,17 @@ async function runDaemon(opts) {
14546
15886
  repoHasCi: (run) => repoHasCiWorkflows(run.worktree_path, run.repo || opts.repo),
14547
15887
  isAlive: pidAlive,
14548
15888
  // Учёт cost/usage (TASWM-188): реальный аккаунтант поверх RUN_LOGS_DIR + db.
14549
- accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db }),
15889
+ accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR }),
14550
15890
  // Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
14551
15891
  // (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
14552
- exitOutcome: makeExitClassifier(RUN_LOGS_DIR),
15892
+ exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
14553
15893
  // Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
14554
15894
  livePpids,
14555
15895
  // Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
14556
15896
  // POST /swarm/kill → stopped в проде → overmind гасит живой pid на реконсайле).
14557
- kill: (pid) => killProcessGroup(pid, "SIGTERM"),
15897
+ // TASWM-349: свой способ остановки — за адаптером; не задан (оба сегодняшних) → общий
15898
+ // групповой SIGTERM.
15899
+ kill: (pid) => EXECUTOR.stop ? EXECUTOR.stop(pid, "SIGTERM") : killProcessGroup(pid, "SIGTERM"),
14558
15900
  // Реклейм worktree паузнутого/возвращаемого дрона (TASWM-228): тот же cwd-скан, что и
14559
15901
  // очистка перед сносом worktree, но БЕЗ сноса каталога — worktree остаётся под
14560
15902
  // --continue. Гасит тяжёлую работу дрона, ушедшую из его process group (Bash-тул
@@ -14593,7 +15935,7 @@ async function runDaemon(opts) {
14593
15935
  lastCommitMs: (p) => worktreeLastCommitMs(p),
14594
15936
  // mtime транскрипта claude-сессии как liveness (TASWM-136): точнее коммитов — обновляется
14595
15937
  // на каждое действие дрона (Edit/Read/tool-use), не только на коммит.
14596
- transcriptMtimeMs: (p) => transcriptMtimeMs(p),
15938
+ transcriptMtimeMs: (p) => EXECUTOR.session.mtimeMs(p),
14597
15939
  // Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
14598
15940
  resourcesOk: hostResourcesOk,
14599
15941
  // Ресурс-гейт v2 (TASWM-151): жёсткий порог активной разгрузки — те же сырые показания
@@ -14624,6 +15966,13 @@ async function runDaemon(opts) {
14624
15966
  ...exclusiveProject ? { exclusiveProject } : {},
14625
15967
  repoFallback: opts.repo,
14626
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(),
14627
15976
  // Ротация своего лога (TASWM-330): демон пишет в него неделями, а до этого ротация была
14628
15977
  // только при старте. Итог ротации печатаем в сам лог — первой строкой нового поколения
14629
15978
  // видно, что история не потерялась, а уехала в `.1`.
@@ -14648,7 +15997,9 @@ async function runDaemon(opts) {
14648
15997
  repoFallback: opts.repo,
14649
15998
  worktreesDir,
14650
15999
  launch,
14651
- git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree }
16000
+ git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree },
16001
+ prHeadBranch
16002
+ // TASWM-356: ветка PR для прогонов без записанного поля branch
14652
16003
  },
14653
16004
  reviewRun
14654
16005
  ).then(() => {
@@ -14679,8 +16030,8 @@ async function runDaemon(opts) {
14679
16030
  mode,
14680
16031
  nudge
14681
16032
  ),
14682
- worktreeExists: (p) => existsSync15(p),
14683
- hasTranscript: (p) => sessionTranscriptExists(p),
16033
+ worktreeExists: (p) => existsSync16(p),
16034
+ hasTranscript: executorHasSession,
14684
16035
  // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
14685
16036
  // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
14686
16037
  // строится над failing/failingRunIds ИЗ checks — без повторного `gh pr checks`.
@@ -14691,7 +16042,7 @@ async function runDaemon(opts) {
14691
16042
  // Штормовой режим (TASWM-23): health-probe на отбой (реальный claude) + внешний
14692
16043
  // сигнал SWARM_STORM_URL (2-й слой, если задан). Логика/пороги — в manageStorm/storm.ts.
14693
16044
  storm: {
14694
- probe: () => anthropicHealthy({ timeoutMs: resolveStormConfig().probeTimeoutMs }),
16045
+ probe: () => executorHealthy({ executor: EXECUTOR, timeoutMs: resolveStormConfig().probeTimeoutMs }),
14695
16046
  // externalDown опционален (exactOptionalPropertyTypes): ключ есть только при
14696
16047
  // заданном SWARM_STORM_URL, иначе опускаем — не передаём undefined.
14697
16048
  ...SWARM_STORM_URL ? { externalDown: () => fetchExternalStorm(SWARM_STORM_URL) } : {}
@@ -14837,6 +16188,17 @@ async function runDaemon(opts) {
14837
16188
  );
14838
16189
  return run ? { id: run.id } : null;
14839
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
+ },
14840
16202
  update: async (onStep) => {
14841
16203
  if (!daemonStart) {
14842
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" };
@@ -15048,6 +16410,7 @@ export {
15048
16410
  REVIEW_CYCLE_LIMIT,
15049
16411
  REVIEW_ESCALATION_MARK,
15050
16412
  REVIEW_SPAWN_LIMIT,
16413
+ REVIEW_UNRECOVERABLE_MARK,
15051
16414
  SELF_REFERENCE_MARK,
15052
16415
  STALL_FLOOR_MS,
15053
16416
  STALL_MS,
@@ -15070,6 +16433,7 @@ export {
15070
16433
  repoHasCiWorkflows,
15071
16434
  resolveConflict,
15072
16435
  respawnRun,
16436
+ reviewUnrecoverableMark,
15073
16437
  reviveStuck,
15074
16438
  runDaemon,
15075
16439
  spawnReviewer,