@taskless-app/swarm 0.5.0 → 0.6.1

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 +59 -0
  2. package/dist/cli.js +1551 -312
  3. package/dist/overmind.js +2249 -636
  4. package/package.json +2 -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,22 @@ 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 SWARM_PACKAGE_NAME, MANUAL_UPDATE_COMMAND, HOST_COMMAND_KINDS, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, UPDATE_STEP_LABELS, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_ACK_GRACE_MS, HOST_COMMAND_MUTE_REASON, 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
+ SWARM_PACKAGE_NAME = "@taskless-app/swarm";
754
+ MANUAL_UPDATE_COMMAND = `npm i -g ${SWARM_PACKAGE_NAME}@latest`;
755
+ HOST_COMMAND_KINDS = ["pause", "resume", "restart", "update", "set_max_drones", "stop_run", "wake_run"];
743
756
  HOST_COMMAND_STATUSES = ["pending", "delivered", "running", "done", "failed", "rejected", "expired"];
744
757
  TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "rejected", "expired"]);
745
758
  HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
@@ -752,46 +765,956 @@ var init_hostCommands = __esm({
752
765
  };
753
766
  HOST_COMMAND_TTL_MS = 30 * 6e4;
754
767
  HOST_COMMAND_KEEPALIVE_MS = 10 * 6e4;
768
+ HOST_COMMAND_ACK_GRACE_MS = 5 * 6e4;
769
+ HOST_COMMAND_MUTE_REASON = `\u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B\u0430 \u0438\u0441\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u2014 \u0432\u0435\u0440\u043E\u044F\u0442\u043D\u043E, \u0434\u0435\u043C\u043E\u043D \u0441\u0442\u0430\u0440\u0435\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B. \u041E\u0431\u043D\u043E\u0432\u0438 \u0432\u0440\u0443\u0447\u043D\u0443\u044E: ${MANUAL_UPDATE_COMMAND}`;
755
770
  HOST_COMMAND_LABELS = {
756
771
  pause: "\u043F\u0430\u0443\u0437\u0430",
757
772
  resume: "\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C",
758
773
  restart: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A",
759
774
  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
775
  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"
776
+ stop_run: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
777
+ 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
778
  };
779
+ RUN_SCOPED_COMMAND_KINDS = ["stop_run", "wake_run"];
780
+ RUN_SCOPED = new Set(RUN_SCOPED_COMMAND_KINDS);
763
781
  MAX_DRONES_LIMIT = 8;
764
782
  }
765
783
  });
766
784
 
767
- // ../swarm-core/src/relief.ts
768
- var RELIEF_PAUSE_MARK;
769
- var init_relief = __esm({
770
- "../swarm-core/src/relief.ts"() {
785
+ // ../swarm-core/src/relief.ts
786
+ var RELIEF_PAUSE_MARK;
787
+ var init_relief = __esm({
788
+ "../swarm-core/src/relief.ts"() {
789
+ "use strict";
790
+ init_time();
791
+ RELIEF_PAUSE_MARK = "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430";
792
+ }
793
+ });
794
+
795
+ // ../swarm-core/src/cost.ts
796
+ var EMPTY_TASK_COST;
797
+ var init_cost = __esm({
798
+ "../swarm-core/src/cost.ts"() {
799
+ "use strict";
800
+ EMPTY_TASK_COST = Object.freeze({
801
+ costUsd: 0,
802
+ tokensIn: 0,
803
+ tokensOut: 0,
804
+ durationMs: 0,
805
+ runs: 0,
806
+ respawns: 0
807
+ });
808
+ }
809
+ });
810
+
811
+ // ../swarm-core/src/runGuards.ts
812
+ var DEFAULT_RUN_LIMITS;
813
+ var init_runGuards = __esm({
814
+ "../swarm-core/src/runGuards.ts"() {
815
+ "use strict";
816
+ DEFAULT_RUN_LIMITS = {
817
+ maxCostUsd: 20,
818
+ runTimeoutMs: 8 * 60 * 6e4,
819
+ stepTimeoutMs: 90 * 6e4,
820
+ lostMs: 30 * 6e4
821
+ };
822
+ }
823
+ });
824
+
825
+ // ../swarm-core/src/codeZones.ts
826
+ function hasZoneModel(m) {
827
+ return m.tasks > 0 && m.zones.length > 0;
828
+ }
829
+ function testFileFeature(basename5) {
830
+ const head = basename5.replace(/\.(test|spec)\.[a-z]+$/i, "").split(".")[0].toLowerCase();
831
+ return head && !GENERIC_DIRS.has(head) ? head : null;
832
+ }
833
+ function pathZoneCandidate(path) {
834
+ const parts = path.split("/").filter(Boolean);
835
+ if (parts.length <= 1) return "root";
836
+ if (WORKSPACE_DIRS.has(parts[0]) && parts.length >= 3) {
837
+ const pkg = parts[1];
838
+ if (parts[2] === "src" && parts.length >= 5) {
839
+ const feature = parts[3];
840
+ if (!GENERIC_DIRS.has(feature)) return feature;
841
+ }
842
+ if (GENERIC_TEST_DIRS.has(parts[2])) {
843
+ const feature = testFileFeature(parts[parts.length - 1]);
844
+ if (feature) return feature;
845
+ }
846
+ return pkg;
847
+ }
848
+ if (parts[0] === "src" && parts.length >= 3 && !GENERIC_DIRS.has(parts[1])) return parts[1];
849
+ return parts[0];
850
+ }
851
+ function dirOf(path) {
852
+ const i = path.lastIndexOf("/");
853
+ return i < 0 ? "" : path.slice(0, i);
854
+ }
855
+ function tokenize(text) {
856
+ const out = [];
857
+ for (const raw of text.split(/[^\p{L}\p{N}]+/u)) {
858
+ if (!raw) continue;
859
+ for (const piece of raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")) {
860
+ const t = piece.toLowerCase();
861
+ if (t.length < 3 || STOP_TERMS.has(t) || /^\d+$/.test(t)) continue;
862
+ out.push(t);
863
+ }
864
+ }
865
+ return out;
866
+ }
867
+ function normalizeZoneName(raw) {
868
+ if (!raw) return null;
869
+ const s = raw.trim().toLowerCase().replace(/^area:/, "");
870
+ return s || null;
871
+ }
872
+ function bump(m, k, by = 1) {
873
+ m.set(k, (m.get(k) ?? 0) + by);
874
+ }
875
+ function topKey(m) {
876
+ let best = null;
877
+ let bestN = 0;
878
+ let total = 0;
879
+ for (const [k, n] of m) {
880
+ total += n;
881
+ if (n > bestN) {
882
+ best = k;
883
+ bestN = n;
884
+ }
885
+ }
886
+ return best === null || total === 0 ? null : { key: best, count: bestN, share: bestN / total };
887
+ }
888
+ function buildZoneModel(entries, opts = {}) {
889
+ const o = { ...DEFAULTS, ...opts };
890
+ const tasks = entries.filter((e) => e.files.length > 0);
891
+ if (tasks.length < o.minTasks) return EMPTY_ZONE_MODEL;
892
+ const fileTasks = /* @__PURE__ */ new Map();
893
+ const candidate = /* @__PURE__ */ new Map();
894
+ for (const t of tasks) {
895
+ for (const f of new Set(t.files)) {
896
+ bump(fileTasks, f);
897
+ if (!candidate.has(f)) candidate.set(f, pathZoneCandidate(f));
898
+ }
899
+ }
900
+ const zoneOf = new Map(candidate);
901
+ for (const [file, seen] of fileTasks) {
902
+ if (seen < 2) continue;
903
+ const votes = /* @__PURE__ */ new Map();
904
+ for (const t of tasks) {
905
+ const files = [...new Set(t.files)];
906
+ if (!files.includes(file)) continue;
907
+ const others = files.filter((f) => f !== file);
908
+ if (!others.length) continue;
909
+ for (const f of others) bump(votes, candidate.get(f), 1 / others.length);
910
+ }
911
+ const top = topKey(votes);
912
+ if (top && top.share >= 0.6 && top.key !== candidate.get(file)) zoneOf.set(file, top.key);
913
+ }
914
+ const voteWeight = (file) => 1 / Math.log2(2 + (fileTasks.get(file) ?? 1));
915
+ const taskZonePre = /* @__PURE__ */ new Map();
916
+ for (const t of tasks) {
917
+ const votes = /* @__PURE__ */ new Map();
918
+ for (const f of new Set(t.files)) bump(votes, zoneOf.get(f), voteWeight(f));
919
+ const top = topKey(votes);
920
+ if (top) taskZonePre.set(t.slug, top.key);
921
+ }
922
+ const fileZones = /* @__PURE__ */ new Map();
923
+ for (const t of tasks) {
924
+ const tz = taskZonePre.get(t.slug);
925
+ if (!tz) continue;
926
+ for (const f of new Set(t.files)) {
927
+ let m = fileZones.get(f);
928
+ if (!m) fileZones.set(f, m = /* @__PURE__ */ new Map());
929
+ bump(m, tz);
930
+ }
931
+ }
932
+ const spineSet = /* @__PURE__ */ new Set();
933
+ for (const [file, seen] of fileTasks) {
934
+ if (seen < o.spineMinTasks) continue;
935
+ const zonesOfFile = fileZones.get(file);
936
+ const dominant = zonesOfFile ? topKey(zonesOfFile) : null;
937
+ if ((zonesOfFile?.size ?? 0) < o.spineMinZones) continue;
938
+ if (dominant && dominant.share >= o.spineDominantShare) continue;
939
+ spineSet.add(file);
940
+ }
941
+ const fileZone = /* @__PURE__ */ new Map();
942
+ for (const [f, z2] of zoneOf) if (!spineSet.has(f)) fileZone.set(f, z2);
943
+ const taskZone = /* @__PURE__ */ new Map();
944
+ for (const t of tasks) {
945
+ const votes = /* @__PURE__ */ new Map();
946
+ for (const f of new Set(t.files)) {
947
+ const z2 = fileZone.get(f);
948
+ if (z2) bump(votes, z2, voteWeight(f));
949
+ }
950
+ const top = topKey(votes);
951
+ taskZone.set(t.slug, top ? top.key : SPINE_ZONE);
952
+ }
953
+ const labelVotes = /* @__PURE__ */ new Map();
954
+ for (const t of tasks) {
955
+ const manual = normalizeZoneName(t.area);
956
+ const z2 = taskZone.get(t.slug);
957
+ if (!manual || !z2 || z2 === SPINE_ZONE || manual === SPINE_ZONE) continue;
958
+ let m = labelVotes.get(z2);
959
+ if (!m) labelVotes.set(z2, m = /* @__PURE__ */ new Map());
960
+ bump(m, manual);
961
+ }
962
+ const rename = /* @__PURE__ */ new Map();
963
+ const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
964
+ const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
965
+ const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
966
+ return sum(b[1]) - sum(a[1]) || a[0].localeCompare(b[0]);
967
+ });
968
+ for (const [zone, votes] of clustersByEvidence) {
969
+ const top = topKey(votes);
970
+ if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
971
+ rename.set(zone, top.key);
972
+ takenNames.add(top.key);
973
+ }
974
+ const renamed = (z2) => rename.get(z2) ?? z2;
975
+ if (rename.size) {
976
+ for (const [f, z2] of fileZone) fileZone.set(f, renamed(z2));
977
+ for (const [s, z2] of taskZone) taskZone.set(s, renamed(z2));
978
+ }
979
+ const zoneTerms = /* @__PURE__ */ new Map();
980
+ const addTerm = (zone, term, w) => {
981
+ let m = zoneTerms.get(zone);
982
+ if (!m) zoneTerms.set(zone, m = /* @__PURE__ */ new Map());
983
+ bump(m, term, w);
984
+ };
985
+ for (const [f, z2] of fileZone) for (const term of tokenize(f)) addTerm(z2, term, 1);
986
+ for (const f of spineSet) for (const term of tokenize(f)) addTerm(SPINE_ZONE, term, 1);
987
+ for (const t of tasks) {
988
+ const z2 = taskZone.get(t.slug);
989
+ if (!z2 || !t.text) continue;
990
+ for (const term of tokenize(t.text)) addTerm(z2, term, 2);
991
+ }
992
+ const termZones = /* @__PURE__ */ new Map();
993
+ for (const terms of zoneTerms.values()) for (const term of terms.keys()) bump(termZones, term);
994
+ const zoneFiles = /* @__PURE__ */ new Map();
995
+ for (const [f, z2] of fileZone) {
996
+ const arr = zoneFiles.get(z2) ?? [];
997
+ arr.push(f);
998
+ zoneFiles.set(z2, arr);
999
+ }
1000
+ const zoneTaskCount = /* @__PURE__ */ new Map();
1001
+ for (const z2 of taskZone.values()) bump(zoneTaskCount, z2);
1002
+ const byFreq = (a, b) => (fileTasks.get(b) ?? 0) - (fileTasks.get(a) ?? 0) || a.localeCompare(b);
1003
+ 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));
1004
+ const spine = [...spineSet].sort(byFreq);
1005
+ if (spineSet.size) zones.push({ zone: SPINE_ZONE, tasks: zoneTaskCount.get(SPINE_ZONE) ?? 0, files: spine });
1006
+ const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
1007
+ return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
1008
+ }
1009
+ function zoneForFiles(model, files) {
1010
+ if (!hasZoneModel(model) || !files.length) return null;
1011
+ const spineSet = new Set(model.spine);
1012
+ const touchedSpine = files.filter((f) => spineSet.has(f));
1013
+ const votes = /* @__PURE__ */ new Map();
1014
+ for (const f of new Set(files)) {
1015
+ const z2 = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
1016
+ if (z2) bump(votes, z2);
1017
+ }
1018
+ const top = topKey(votes);
1019
+ if (!top) {
1020
+ return {
1021
+ zone: SPINE_ZONE,
1022
+ confidence: 1,
1023
+ 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(", ")})`,
1024
+ spine: true
1025
+ };
1026
+ }
1027
+ return {
1028
+ zone: top.key,
1029
+ confidence: top.share,
1030
+ 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}`,
1031
+ spine: touchedSpine.length > 0
1032
+ };
1033
+ }
1034
+ function suggestZonesFromText(model, text, limit = 3) {
1035
+ if (!hasZoneModel(model)) return [];
1036
+ const terms = tokenize(text);
1037
+ if (!terms.length) return [];
1038
+ const zoneCount = model.zoneTerms.size || 1;
1039
+ const idf = (term) => Math.log(1 + zoneCount / (model.termZones.get(term) ?? 1));
1040
+ const scores = /* @__PURE__ */ new Map();
1041
+ const hits = /* @__PURE__ */ new Map();
1042
+ const tasksOfZone = new Map(model.zones.map((z2) => [z2.zone, z2.tasks]));
1043
+ for (const [zone, dict] of model.zoneTerms) {
1044
+ if ((tasksOfZone.get(zone) ?? 0) < 2) continue;
1045
+ let dot = 0;
1046
+ let norm = 0;
1047
+ const matched = [];
1048
+ for (const [term, w] of dict) {
1049
+ const v = Math.log(1 + w) * idf(term);
1050
+ norm += v * v;
1051
+ }
1052
+ if (!norm) continue;
1053
+ for (const term of new Set(terms)) {
1054
+ const w = dict.get(term);
1055
+ if (!w) continue;
1056
+ dot += Math.log(1 + w) * idf(term) * idf(term);
1057
+ matched.push(term);
1058
+ }
1059
+ const score = dot / Math.sqrt(norm) * Math.log(1 + (tasksOfZone.get(zone) ?? 1));
1060
+ if (score > 0) {
1061
+ scores.set(zone, score);
1062
+ hits.set(zone, matched);
1063
+ }
1064
+ }
1065
+ const total = [...scores.values()].reduce((a, b) => a + b, 0);
1066
+ if (!total) return [];
1067
+ return [...scores.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([zone, s]) => ({
1068
+ zone,
1069
+ confidence: s / total,
1070
+ why: `\u0441\u043E\u0432\u043F\u0430\u043B\u043E \u043F\u043E \u0441\u043B\u043E\u0432\u0430\u043C: ${(hits.get(zone) ?? []).slice(0, 5).join(", ")}`,
1071
+ spine: zone === SPINE_ZONE
1072
+ }));
1073
+ }
1074
+ function similarTasks(model, text, limit = 5) {
1075
+ const terms = new Set(tokenize(text));
1076
+ if (!terms.size) return [];
1077
+ const scored = [];
1078
+ for (const [i, e] of model.entries.entries()) {
1079
+ const other = model.entryTerms[i];
1080
+ if (!other?.size) continue;
1081
+ let inter = 0;
1082
+ for (const t of terms) if (other.has(t)) inter += Math.log(1 + (model.zoneTerms.size || 1) / (model.termZones.get(t) ?? 1));
1083
+ if (inter <= 0) continue;
1084
+ scored.push({ entry: e, score: inter / Math.sqrt(terms.size * other.size) });
1085
+ }
1086
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit);
1087
+ }
1088
+ function predictFiles(model, opts) {
1089
+ if (!hasZoneModel(model)) return [];
1090
+ const limit = opts.limit ?? 40;
1091
+ const best = /* @__PURE__ */ new Map();
1092
+ const put = (path, weight, why) => {
1093
+ const cur = best.get(path);
1094
+ if (!cur || cur.weight < weight) best.set(path, { path, weight, why });
1095
+ };
1096
+ if (opts.text) {
1097
+ const sim = similarTasks(model, opts.text, opts.similar ?? 5);
1098
+ const topScore = sim[0]?.score ?? 0;
1099
+ for (const { entry, score } of sim) {
1100
+ const w = topScore > 0 ? Math.min(1, score / topScore) : 0;
1101
+ 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}`);
1102
+ }
1103
+ }
1104
+ const zone = normalizeZoneName(opts.zone) ?? (opts.text ? suggestZonesFromText(model, opts.text)[0]?.zone ?? null : null);
1105
+ if (zone) {
1106
+ const stat = model.zones.find((z2) => z2.zone === zone);
1107
+ 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}`));
1108
+ }
1109
+ return [...best.values()].sort((a, b) => b.weight - a.weight || a.path.localeCompare(b.path)).slice(0, limit);
1110
+ }
1111
+ function overlapRisk(predicted, actual, opts = {}) {
1112
+ const o = { ...OVERLAP_DEFAULTS, ...opts };
1113
+ const actualSet = new Set(actual);
1114
+ const actualDirs = new Set([...actualSet].map(dirOf).filter(Boolean));
1115
+ const totalWeight = predicted.reduce((a, p) => a + p.weight, 0);
1116
+ if (!totalWeight) return { overlap: false, score: 0, sharedFiles: [], sharedDirs: [] };
1117
+ let hit = 0;
1118
+ let strong = false;
1119
+ const sharedFiles = [];
1120
+ const sharedDirs = /* @__PURE__ */ new Set();
1121
+ for (const p of predicted) {
1122
+ if (actualSet.has(p.path)) {
1123
+ hit += p.weight;
1124
+ sharedFiles.push(p.path);
1125
+ if (p.weight >= o.strongFile) strong = true;
1126
+ continue;
1127
+ }
1128
+ const d = dirOf(p.path);
1129
+ if (d && actualDirs.has(d)) {
1130
+ hit += p.weight * 0.25;
1131
+ sharedDirs.add(d);
1132
+ }
1133
+ }
1134
+ const score = Math.min(1, hit / totalWeight);
1135
+ return {
1136
+ overlap: strong || score >= o.threshold,
1137
+ score,
1138
+ sharedFiles: sharedFiles.slice(0, 10),
1139
+ sharedDirs: [...sharedDirs].slice(0, 5)
1140
+ };
1141
+ }
1142
+ var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS, OVERLAP_DEFAULTS;
1143
+ var init_codeZones = __esm({
1144
+ "../swarm-core/src/codeZones.ts"() {
1145
+ "use strict";
1146
+ SPINE_ZONE = "core";
1147
+ DEFAULTS = {
1148
+ minTasks: 5,
1149
+ spineMinTasks: 3,
1150
+ spineMinZones: 3,
1151
+ spineDominantShare: 0.6
1152
+ };
1153
+ EMPTY_ZONE_MODEL = {
1154
+ tasks: 0,
1155
+ zones: [],
1156
+ fileZone: /* @__PURE__ */ new Map(),
1157
+ spine: [],
1158
+ taskZone: /* @__PURE__ */ new Map(),
1159
+ zoneTerms: /* @__PURE__ */ new Map(),
1160
+ termZones: /* @__PURE__ */ new Map(),
1161
+ entries: [],
1162
+ entryTerms: []
1163
+ };
1164
+ GENERIC_DIRS = /* @__PURE__ */ new Set([
1165
+ "src",
1166
+ "source",
1167
+ "lib",
1168
+ "libs",
1169
+ "util",
1170
+ "utils",
1171
+ "common",
1172
+ "internal",
1173
+ "app",
1174
+ "test",
1175
+ "tests",
1176
+ "__tests__",
1177
+ "spec",
1178
+ "specs",
1179
+ "index"
1180
+ ]);
1181
+ WORKSPACE_DIRS = /* @__PURE__ */ new Set(["packages", "apps", "services", "libs", "modules"]);
1182
+ GENERIC_TEST_DIRS = /* @__PURE__ */ new Set(["tests", "test", "__tests__", "spec"]);
1183
+ STOP_TERMS = /* @__PURE__ */ new Set([
1184
+ "the",
1185
+ "and",
1186
+ "for",
1187
+ "with",
1188
+ "from",
1189
+ "into",
1190
+ "src",
1191
+ "test",
1192
+ "tests",
1193
+ "index",
1194
+ "packages",
1195
+ "package",
1196
+ "json",
1197
+ "yml",
1198
+ "yaml",
1199
+ "tsx",
1200
+ "mjs",
1201
+ "cjs",
1202
+ "\u0447\u0442\u043E",
1203
+ "\u043A\u0430\u043A",
1204
+ "\u0434\u043B\u044F",
1205
+ "\u043F\u0440\u0438",
1206
+ "\u044D\u0442\u043E",
1207
+ "\u0438\u043B\u0438",
1208
+ "\u0435\u0441\u043B\u0438",
1209
+ "\u0447\u0442\u043E\u0431\u044B",
1210
+ "\u0442\u043E\u0436\u0435",
1211
+ "\u0435\u0433\u043E",
1212
+ "\u0432\u0441\u0435",
1213
+ "feat",
1214
+ "fix",
1215
+ "chore",
1216
+ "docs",
1217
+ "refactor",
1218
+ "\u043F\u0435\u0440\u0444",
1219
+ "wip"
1220
+ ]);
1221
+ OVERLAP_DEFAULTS = { threshold: 0.2, strongFile: 0.35 };
1222
+ }
1223
+ });
1224
+
1225
+ // ../swarm-core/src/index.ts
1226
+ var init_src = __esm({
1227
+ "../swarm-core/src/index.ts"() {
1228
+ "use strict";
1229
+ init_types();
1230
+ init_timeline();
1231
+ init_reviewPhase();
1232
+ init_status();
1233
+ init_time();
1234
+ init_windows();
1235
+ init_slug();
1236
+ init_text();
1237
+ init_comment();
1238
+ init_names();
1239
+ init_baseBranch();
1240
+ init_hosts();
1241
+ init_hostPool();
1242
+ init_hostCommands();
1243
+ init_relief();
1244
+ init_cost();
1245
+ init_runGuards();
1246
+ init_codeZones();
1247
+ }
1248
+ });
1249
+
1250
+ // src/claudeBin.ts
1251
+ import { existsSync as existsSync2, readdirSync } from "fs";
1252
+ import { win32 as winPath } from "path";
1253
+ function compareVersionDirs(a, b) {
1254
+ const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
1255
+ const pa = parts(a);
1256
+ const pb = parts(b);
1257
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1258
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1259
+ if (d !== 0) return d;
1260
+ }
1261
+ return 0;
1262
+ }
1263
+ function realListDir(dir) {
1264
+ try {
1265
+ return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1266
+ } catch {
1267
+ return [];
1268
+ }
1269
+ }
1270
+ function winVersionRoots(env, listDir) {
1271
+ const roots = [];
1272
+ const appData = env.APPDATA;
1273
+ if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
1274
+ const localAppData = env.LOCALAPPDATA;
1275
+ if (localAppData) {
1276
+ const packages = winPath.join(localAppData, "Packages");
1277
+ for (const name of listDir(packages)) {
1278
+ if (!name.startsWith("Claude")) continue;
1279
+ roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
1280
+ }
1281
+ }
1282
+ return roots;
1283
+ }
1284
+ function findWindowsClaude(opts = {}) {
1285
+ const env = opts.env ?? process.env;
1286
+ const listDir = opts.listDir ?? realListDir;
1287
+ const fileExists = opts.fileExists ?? existsSync2;
1288
+ let best = null;
1289
+ for (const root of winVersionRoots(env, listDir)) {
1290
+ for (const version of listDir(root)) {
1291
+ const candidate = winPath.join(root, version, "claude.exe");
1292
+ if (!fileExists(candidate)) continue;
1293
+ if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
1294
+ }
1295
+ }
1296
+ return best?.path ?? null;
1297
+ }
1298
+ function resolveClaudeBin(opts = {}) {
1299
+ const env = opts.env ?? process.env;
1300
+ const platform = opts.platform ?? process.platform;
1301
+ const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
1302
+ if (explicit) return explicit;
1303
+ if (platform !== "win32") return CLAUDE_FALLBACK;
1304
+ if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
1305
+ windowsScanDone = true;
1306
+ const found = findWindowsClaude({ ...opts, env });
1307
+ cachedWindowsBin = found;
1308
+ if (found) return found;
1309
+ if (!warnedNoWindowsClaude) {
1310
+ warnedNoWindowsClaude = true;
1311
+ console.warn(
1312
+ "[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."
1313
+ );
1314
+ }
1315
+ return CLAUDE_FALLBACK;
1316
+ }
1317
+ var CLAUDE_FALLBACK, warnedNoWindowsClaude, cachedWindowsBin, windowsScanDone;
1318
+ var init_claudeBin = __esm({
1319
+ "src/claudeBin.ts"() {
1320
+ "use strict";
1321
+ CLAUDE_FALLBACK = "claude";
1322
+ warnedNoWindowsClaude = false;
1323
+ cachedWindowsBin = null;
1324
+ windowsScanDone = false;
1325
+ }
1326
+ });
1327
+
1328
+ // src/pricing.ts
1329
+ function costFromUsage(usage, model, table = MODEL_PRICING) {
1330
+ const pricing = model ? table[model] : void 0;
1331
+ if (!pricing) {
1332
+ 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` };
1333
+ }
1334
+ const input = usage.input_tokens ?? 0;
1335
+ const output = usage.output_tokens ?? 0;
1336
+ const cacheWrite = usage.cache_creation_input_tokens ?? 0;
1337
+ const cacheRead = usage.cache_read_input_tokens ?? 0;
1338
+ const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
1339
+ return { costUsd };
1340
+ }
1341
+ var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
1342
+ var init_pricing = __esm({
1343
+ "src/pricing.ts"() {
1344
+ "use strict";
1345
+ MODEL_PRICING = {
1346
+ // Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
1347
+ // Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
1348
+ // ветвление по времени внутри расчёта стоимости.
1349
+ "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
1350
+ "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
1351
+ // Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
1352
+ "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
1353
+ "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
1354
+ };
1355
+ CACHE_READ_FACTOR = 0.1;
1356
+ CACHE_WRITE_5M_FACTOR = 1.25;
1357
+ }
1358
+ });
1359
+
1360
+ // src/claudeResult.ts
1361
+ function parseClaudeResultJson(raw) {
1362
+ const trimmed = raw.trim();
1363
+ if (!trimmed) return null;
1364
+ try {
1365
+ const parsed = JSON.parse(trimmed);
1366
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
1367
+ } catch {
1368
+ return null;
1369
+ }
1370
+ }
1371
+ function launchCostUsd(result, model) {
1372
+ if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
1373
+ return { costUsd: result.total_cost_usd };
1374
+ }
1375
+ if (!result.usage) return { costUsd: 0 };
1376
+ return costFromUsage(result.usage, model);
1377
+ }
1378
+ function launchSpend(result) {
1379
+ const u = result.usage ?? {};
1380
+ const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1381
+ return {
1382
+ tokensIn: num(u.input_tokens) + num(u.cache_creation_input_tokens) + num(u.cache_read_input_tokens),
1383
+ tokensOut: num(u.output_tokens),
1384
+ durationMs: num(result.duration_ms)
1385
+ };
1386
+ }
1387
+ var init_claudeResult = __esm({
1388
+ "src/claudeResult.ts"() {
1389
+ "use strict";
1390
+ init_pricing();
1391
+ }
1392
+ });
1393
+
1394
+ // src/executors/claudeCode.ts
1395
+ import { readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
1396
+ import { homedir } from "os";
1397
+ import { join as join2 } from "path";
1398
+ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
1399
+ const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
1400
+ const base3 = [
1401
+ "--dangerously-skip-permissions",
1402
+ ...ISOLATION_FLAGS,
1403
+ "--mcp-config",
1404
+ cfgPath,
1405
+ ...settings ? ["--settings", settings] : [],
1406
+ ...pluginFlags,
1407
+ "--output-format",
1408
+ "json",
1409
+ "-p",
1410
+ prompt
1411
+ ];
1412
+ const withModel = model ? ["--model", model, ...base3] : base3;
1413
+ return continueSession ? ["--continue", ...withModel] : withModel;
1414
+ }
1415
+ function droneDenyRules(paths) {
1416
+ const abs = (p) => `//${p.replace(/^\/+/, "")}`;
1417
+ return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
1418
+ }
1419
+ function droneSettingsJson(paths) {
1420
+ return JSON.stringify({ permissions: { deny: droneDenyRules(paths) } });
1421
+ }
1422
+ function transcriptDir(worktree, projectsDir) {
1423
+ return join2(projectsDir, worktree.replace(/[/.]/g, "-"));
1424
+ }
1425
+ function sessionTranscriptExists(worktree, projectsDir = join2(homedir(), ".claude", "projects")) {
1426
+ try {
1427
+ return readdirSync2(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
1428
+ } catch {
1429
+ return false;
1430
+ }
1431
+ }
1432
+ function transcriptMtimeMs(worktree, projectsDir = join2(homedir(), ".claude", "projects")) {
1433
+ const dir = transcriptDir(worktree, projectsDir);
1434
+ let files;
1435
+ try {
1436
+ files = readdirSync2(dir);
1437
+ } catch {
1438
+ return null;
1439
+ }
1440
+ let latest = null;
1441
+ for (const f of files) {
1442
+ if (!f.endsWith(".jsonl")) continue;
1443
+ try {
1444
+ const mtime2 = statSync(join2(dir, f)).mtimeMs;
1445
+ if (latest === null || mtime2 > latest) latest = mtime2;
1446
+ } catch {
1447
+ }
1448
+ }
1449
+ return latest;
1450
+ }
1451
+ function writeMcpConfig(spec) {
1452
+ const cfg = {
1453
+ mcpServers: {
1454
+ [spec.mcp.name]: { command: spec.mcp.command, args: spec.mcp.args, env: spec.mcp.env }
1455
+ }
1456
+ };
1457
+ const path = join2(spec.worktree, MCP_CONFIG_FILE);
1458
+ writeFileSync2(path, JSON.stringify(cfg, null, 2));
1459
+ return path;
1460
+ }
1461
+ var ISOLATION_FLAGS, MCP_CONFIG_FILE, claudeCodeExecutor;
1462
+ var init_claudeCode = __esm({
1463
+ "src/executors/claudeCode.ts"() {
1464
+ "use strict";
1465
+ init_claudeBin();
1466
+ init_claudeResult();
1467
+ ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
1468
+ MCP_CONFIG_FILE = ".swarm-mcp.json";
1469
+ claudeCodeExecutor = {
1470
+ id: "claude-code",
1471
+ title: "Claude Code (claude)",
1472
+ binEnv: "SWARM_CLAUDE_BIN",
1473
+ 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" },
1474
+ resolveBin: () => resolveClaudeBin(),
1475
+ // Стрелка, а не метод: `plan` могут вызвать в отрыве от объекта (деструктуризация в тестах,
1476
+ // проброс колбэком), и `this` тогда уедет в undefined.
1477
+ plan: (spec) => {
1478
+ const cfgPath = writeMcpConfig(spec);
1479
+ const settings = spec.denyPaths.length > 0 ? droneSettingsJson(spec.denyPaths) : void 0;
1480
+ return {
1481
+ bin: resolveClaudeBin(),
1482
+ args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings)
1483
+ };
1484
+ },
1485
+ parseResult(raw, model) {
1486
+ const result = parseClaudeResultJson(raw);
1487
+ if (!result) return null;
1488
+ const est = launchCostUsd(result, model);
1489
+ const spend = launchSpend(result);
1490
+ return {
1491
+ costUsd: est.costUsd,
1492
+ ...est.warning ? { warning: est.warning } : {},
1493
+ tokensIn: spend.tokensIn,
1494
+ tokensOut: spend.tokensOut,
1495
+ durationMs: spend.durationMs
1496
+ };
1497
+ },
1498
+ session: {
1499
+ exists: (worktree) => sessionTranscriptExists(worktree),
1500
+ mtimeMs: (worktree) => transcriptMtimeMs(worktree)
1501
+ },
1502
+ // `claude -p "ok" --max-turns 1` (TASWM-23) — прямее, чем HEAD на статус-хост: дёргает ТОТ
1503
+ // ЖЕ путь (auth + реальный ответ модели), что и дрон. ISOLATION_FLAGS те же, что у дрона
1504
+ // (TASWM-90): SessionStart-хуки юзера могли бы подвесить пробу и дать ложный шторм.
1505
+ probe: {
1506
+ args: [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"],
1507
+ // TASWM-214: исчерпанный лимит подписки — НЕ шторм провайдера, и путать их нельзя.
1508
+ limitPattern: /(weekly limit|usage limit|hit your .*limit)/i
1509
+ }
1510
+ };
1511
+ }
1512
+ });
1513
+
1514
+ // src/swarmHome.ts
1515
+ import { homedir as homedir2 } from "os";
1516
+ import { join as join3 } from "path";
1517
+ import { mkdirSync } from "fs";
1518
+ var SWARM_HOME;
1519
+ var init_swarmHome = __esm({
1520
+ "src/swarmHome.ts"() {
1521
+ "use strict";
1522
+ SWARM_HOME = process.env.SWARM_HOME || join3(homedir2(), ".taskless-swarm");
1523
+ mkdirSync(SWARM_HOME, { recursive: true });
1524
+ }
1525
+ });
1526
+
1527
+ // src/executors/codex.ts
1528
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync3, statSync as statSync2, symlinkSync } from "fs";
1529
+ import { join as join4 } from "path";
1530
+ function resolveCodexBin(env = process.env) {
1531
+ return (env.SWARM_CODEX_BIN ?? "").trim() || "codex";
1532
+ }
1533
+ function codexHomePath(worktree, swarmHome = SWARM_HOME) {
1534
+ return join4(swarmHome, "executor-state", "codex", worktree.replace(/[/.]/g, "-"));
1535
+ }
1536
+ function ensureCodexHome(worktree, realHome, env = process.env, swarmHome = SWARM_HOME) {
1537
+ const home = codexHomePath(worktree, swarmHome);
1538
+ mkdirSync2(home, { recursive: true });
1539
+ const real = (env.CODEX_HOME ?? "").trim() || join4(realHome, ".codex");
1540
+ for (const file of ["auth.json", "config.toml"]) {
1541
+ const target = join4(real, file);
1542
+ const link = join4(home, file);
1543
+ if (!existsSync3(target) || existsSync3(link)) continue;
1544
+ try {
1545
+ symlinkSync(target, link);
1546
+ } catch {
1547
+ }
1548
+ }
1549
+ return home;
1550
+ }
1551
+ function tomlValue(v) {
1552
+ if (typeof v === "string") {
1553
+ const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
1554
+ return `"${escaped}"`;
1555
+ }
1556
+ if (Array.isArray(v)) return `[${v.map((x) => tomlValue(x)).join(", ")}]`;
1557
+ return `{${Object.entries(v).map(([k, val]) => `${k} = ${tomlValue(val)}`).join(", ")}}`;
1558
+ }
1559
+ function mcpConfigArgs(spec) {
1560
+ const inline = `{command = ${tomlValue(spec.mcp.command)}, args = ${tomlValue(spec.mcp.args)}, env = ${tomlValue(spec.mcp.env)}}`;
1561
+ return ["-c", `mcp_servers.${spec.mcp.name}=${inline}`];
1562
+ }
1563
+ function skillsNotice(skillDirs) {
1564
+ if (skillDirs.length === 0) return "";
1565
+ 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.";
1566
+ }
1567
+ function readUsage(node) {
1568
+ if (typeof node !== "object" || node === null) return null;
1569
+ const rec = node;
1570
+ const direct = rec.total_token_usage ?? rec.usage ?? rec.token_usage;
1571
+ if (direct && typeof direct === "object") return direct;
1572
+ const info = rec.info;
1573
+ if (info && typeof info === "object") return readUsage(info);
1574
+ return null;
1575
+ }
1576
+ function parseCodexStream(raw) {
1577
+ let usage = null;
1578
+ let completed = false;
1579
+ for (const line of raw.split("\n")) {
1580
+ const trimmed = line.trim();
1581
+ if (!trimmed) continue;
1582
+ let parsed;
1583
+ try {
1584
+ parsed = JSON.parse(trimmed);
1585
+ } catch {
1586
+ continue;
1587
+ }
1588
+ if (typeof parsed !== "object" || parsed === null) continue;
1589
+ const rec = parsed;
1590
+ const msg = typeof rec.msg === "object" && rec.msg !== null ? rec.msg : rec;
1591
+ const type = typeof msg.type === "string" ? msg.type : null;
1592
+ const found = readUsage(msg);
1593
+ if (found) usage = found;
1594
+ if (type && TERMINAL_EVENTS.has(type)) completed = true;
1595
+ }
1596
+ return { usage, completed };
1597
+ }
1598
+ function newestRolloutMtime(dir, depth = 4) {
1599
+ let entries;
1600
+ try {
1601
+ entries = readdirSync3(dir);
1602
+ } catch {
1603
+ return null;
1604
+ }
1605
+ let latest = null;
1606
+ for (const name of entries) {
1607
+ const full = join4(dir, name);
1608
+ let st;
1609
+ try {
1610
+ st = statSync2(full);
1611
+ } catch {
1612
+ continue;
1613
+ }
1614
+ if (st.isDirectory()) {
1615
+ if (depth <= 0) continue;
1616
+ const nested = newestRolloutMtime(full, depth - 1);
1617
+ if (nested !== null && (latest === null || nested > latest)) latest = nested;
1618
+ } else if (name.endsWith(".jsonl")) {
1619
+ if (latest === null || st.mtimeMs > latest) latest = st.mtimeMs;
1620
+ }
1621
+ }
1622
+ return latest;
1623
+ }
1624
+ var BYPASS_FLAG, SKIP_GIT_CHECK_FLAG, CODEX_MODEL_PRICING, TERMINAL_EVENTS, codexExecutor;
1625
+ var init_codex = __esm({
1626
+ "src/executors/codex.ts"() {
1627
+ "use strict";
1628
+ init_swarmHome();
1629
+ init_pricing();
1630
+ BYPASS_FLAG = "--dangerously-bypass-approvals-and-sandbox";
1631
+ SKIP_GIT_CHECK_FLAG = "--skip-git-repo-check";
1632
+ CODEX_MODEL_PRICING = {};
1633
+ TERMINAL_EVENTS = /* @__PURE__ */ new Set(["task_complete", "turn.completed", "thread.completed", "session.completed"]);
1634
+ codexExecutor = {
1635
+ id: "codex",
1636
+ title: "Codex CLI (codex)",
1637
+ binEnv: "SWARM_CODEX_BIN",
1638
+ 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" },
1639
+ resolveBin: () => resolveCodexBin(),
1640
+ // Стрелка, а не метод — см. тот же комментарий в claudeCode.ts.
1641
+ plan: (spec) => {
1642
+ const notice = skillsNotice(spec.skillDirs);
1643
+ const prompt = notice ? `${spec.prompt}
1644
+
1645
+ ${notice}` : spec.prompt;
1646
+ const args = [
1647
+ "exec",
1648
+ // Продолжение оборванной сессии — ПОДКОМАНДА, а не флаг: `exec resume --last` берёт
1649
+ // последнюю сессию этого CODEX_HOME (то есть этого worktree, см. codexHome).
1650
+ ...spec.continueSession ? ["resume", "--last"] : [],
1651
+ "--json",
1652
+ SKIP_GIT_CHECK_FLAG,
1653
+ BYPASS_FLAG,
1654
+ ...spec.model ? ["-m", spec.model] : [],
1655
+ ...mcpConfigArgs(spec),
1656
+ prompt
1657
+ ];
1658
+ return { bin: resolveCodexBin(), args, env: { CODEX_HOME: ensureCodexHome(spec.worktree, spec.realHome) } };
1659
+ },
1660
+ parseResult(raw, model) {
1661
+ const { usage, completed } = parseCodexStream(raw);
1662
+ if (!completed) return null;
1663
+ 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" };
1664
+ const cached = usage.cached_input_tokens ?? 0;
1665
+ const common = {
1666
+ input_tokens: Math.max(0, (usage.input_tokens ?? 0) - cached),
1667
+ output_tokens: usage.output_tokens ?? 0,
1668
+ cache_read_input_tokens: cached
1669
+ };
1670
+ const est = costFromUsage(common, model, CODEX_MODEL_PRICING);
1671
+ return est.warning ? { costUsd: est.costUsd, warning: est.warning } : { costUsd: est.costUsd };
1672
+ },
1673
+ session: {
1674
+ exists: (worktree) => newestRolloutMtime(join4(codexHomePath(worktree), "sessions")) !== null,
1675
+ mtimeMs: (worktree) => newestRolloutMtime(join4(codexHomePath(worktree), "sessions"))
1676
+ },
1677
+ probe: {
1678
+ // Тот же принцип, что у claude: дешёвый реальный прогон — он проверяет и auth, и живость
1679
+ // провайдера разом. Один короткий ответ, без тулов и без сети сверх модели.
1680
+ args: ["exec", "--json", SKIP_GIT_CHECK_FLAG, BYPASS_FLAG, "ok"],
1681
+ limitPattern: /(usage limit|rate limit|quota exceeded|insufficient_quota)/i
1682
+ }
1683
+ };
1684
+ }
1685
+ });
1686
+
1687
+ // src/executors/types.ts
1688
+ var init_types2 = __esm({
1689
+ "src/executors/types.ts"() {
771
1690
  "use strict";
772
- init_time();
773
- RELIEF_PAUSE_MARK = "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430";
774
1691
  }
775
1692
  });
776
1693
 
777
- // ../swarm-core/src/index.ts
778
- var init_src = __esm({
779
- "../swarm-core/src/index.ts"() {
1694
+ // src/executors/index.ts
1695
+ function resolveExecutor(id) {
1696
+ const key = (id ?? "").trim();
1697
+ if (!key) return EXECUTORS[DEFAULT_EXECUTOR_ID];
1698
+ const found = EXECUTORS[key];
1699
+ if (!found) {
1700
+ 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(", ")}`);
1701
+ }
1702
+ return found;
1703
+ }
1704
+ var DEFAULT_EXECUTOR_ID, EXECUTORS;
1705
+ var init_executors = __esm({
1706
+ "src/executors/index.ts"() {
780
1707
  "use strict";
781
- init_types();
782
- init_timeline();
783
- init_reviewPhase();
784
- init_status();
785
- init_time();
786
- init_windows();
787
- init_slug();
788
- init_text();
789
- init_comment();
790
- init_names();
791
- init_baseBranch();
792
- init_hosts();
793
- init_hostCommands();
794
- init_relief();
1708
+ init_claudeCode();
1709
+ init_codex();
1710
+ init_types2();
1711
+ init_claudeCode();
1712
+ init_codex();
1713
+ DEFAULT_EXECUTOR_ID = "claude-code";
1714
+ EXECUTORS = {
1715
+ [claudeCodeExecutor.id]: claudeCodeExecutor,
1716
+ [codexExecutor.id]: codexExecutor
1717
+ };
795
1718
  }
796
1719
  });
797
1720
 
@@ -811,6 +1734,7 @@ __export(config_exports, {
811
1734
  DRONE_HOME: () => DRONE_HOME,
812
1735
  DRONE_HOME_ISOLATION: () => DRONE_HOME_ISOLATION,
813
1736
  DRONE_ISOLATION: () => DRONE_ISOLATION,
1737
+ EXECUTOR: () => EXECUTOR,
814
1738
  HEARTBEAT_PATH: () => HEARTBEAT_PATH,
815
1739
  HEARTBEAT_STALE_MS: () => HEARTBEAT_STALE_MS,
816
1740
  REPO_MAP_PATH: () => REPO_MAP_PATH,
@@ -880,9 +1804,8 @@ __export(config_exports, {
880
1804
  resolveEffectiveWorktreesDir: () => resolveEffectiveWorktreesDir,
881
1805
  reviewCutoffAt: () => reviewCutoffAt
882
1806
  });
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";
1807
+ import { basename, join as join5, resolve } from "path";
1808
+ import { readFileSync as readFileSync2, realpathSync, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
886
1809
  function normalizeRepo(repo) {
887
1810
  const r = (repo ?? "").trim();
888
1811
  if (!r) return "";
@@ -893,7 +1816,7 @@ function normalizeRepo(repo) {
893
1816
  }
894
1817
  }
895
1818
  function droneWorktreePath(worktreesDir, repo, slug) {
896
- return join2(worktreesDir, basename(normalizeRepo(repo)), slug);
1819
+ return join5(worktreesDir, basename(normalizeRepo(repo)), slug);
897
1820
  }
898
1821
  function resolveEffectiveWorktreesDir(deps) {
899
1822
  if (deps.envOverride) return deps.envOverride;
@@ -935,7 +1858,7 @@ function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
935
1858
  }
936
1859
  function tokenFromFile() {
937
1860
  try {
938
- return readFileSync2(join2(SWARM_HOME, "token"), "utf8").trim();
1861
+ return readFileSync2(join5(SWARM_HOME, "token"), "utf8").trim();
939
1862
  } catch {
940
1863
  return "";
941
1864
  }
@@ -949,34 +1872,36 @@ function reviewCutoffAt() {
949
1872
  const at = stamp();
950
1873
  try {
951
1874
  const tmp = `${REVIEW_CUTOFF_PATH}.tmp`;
952
- writeFileSync2(tmp, JSON.stringify({ at }));
1875
+ writeFileSync3(tmp, JSON.stringify({ at }));
953
1876
  renameSync2(tmp, REVIEW_CUTOFF_PATH);
954
1877
  } catch {
955
1878
  }
956
1879
  return at;
957
1880
  }
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;
1881
+ 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
1882
  var init_config = __esm({
960
1883
  "src/config.ts"() {
961
1884
  "use strict";
962
1885
  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");
1886
+ init_executors();
1887
+ init_swarmHome();
1888
+ init_swarmHome();
1889
+ DB_PATH = process.env.SWARM_DB || join5(SWARM_HOME, "swarm.db");
1890
+ EXECUTOR = resolveExecutor(process.env.SWARM_EXECUTOR);
1891
+ DRONE_HOME = process.env.SWARM_DRONE_HOME || join5(SWARM_HOME, "drone-home");
967
1892
  DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
968
1893
  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");
1894
+ WORKTREES_NOINDEX_DIR = join5(SWARM_HOME, "worktrees.noindex");
1895
+ WORKTREES_LEGACY_DIR = join5(SWARM_HOME, "worktrees");
971
1896
  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");
1897
+ RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join5(SWARM_HOME, "run-logs");
1898
+ DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join5(SWARM_HOME, "overmind.log");
974
1899
  DAEMON_LOG_MAX_BYTES = envNum("SWARM_DAEMON_LOG_MAX_BYTES", 16 * 1024 * 1024);
975
1900
  DAEMON_LOG_KEEP = envNum("SWARM_DAEMON_LOG_KEEP", 2);
976
1901
  DAEMON_LOG_CHECK_MS = envNum("SWARM_DAEMON_LOG_CHECK_MS", 6e4);
977
1902
  DIST_BUILD = true;
978
1903
  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");
1904
+ SWARM_OUTBOX = process.env.SWARM_OUTBOX || join5(SWARM_HOME, "outbox.jsonl");
980
1905
  SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
981
1906
  SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
982
1907
  SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
@@ -1001,15 +1926,15 @@ var init_config = __esm({
1001
1926
  DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
1002
1927
  DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
1003
1928
  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");
1929
+ DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join5(SWARM_HOME, "drone-channel.key");
1005
1930
  DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
1006
- HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join2(SWARM_HOME, "overmind.heartbeat");
1931
+ HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join5(SWARM_HOME, "overmind.heartbeat");
1007
1932
  HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
1008
1933
  SWARM_REPO = process.env.SWARM_REPO || "";
1009
1934
  SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
1010
1935
  SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
1011
1936
  SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
1012
- REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join2(SWARM_HOME, "repo-map.json");
1937
+ REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join5(SWARM_HOME, "repo-map.json");
1013
1938
  SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
1014
1939
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
1015
1940
  SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
@@ -1020,7 +1945,7 @@ var init_config = __esm({
1020
1945
  TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
1021
1946
  SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
1022
1947
  SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
1023
- REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join2(SWARM_HOME, "review-cutoff.json");
1948
+ REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join5(SWARM_HOME, "review-cutoff.json");
1024
1949
  SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
1025
1950
  SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
1026
1951
  WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
@@ -1377,10 +2302,10 @@ var init_storm = __esm({
1377
2302
  });
1378
2303
 
1379
2304
  // src/heartbeat.ts
1380
- import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
2305
+ import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
1381
2306
  function writeHeartbeat(path, now) {
1382
2307
  const tmp = `${path}.tmp`;
1383
- writeFileSync4(tmp, String(now));
2308
+ writeFileSync5(tmp, String(now));
1384
2309
  renameSync3(tmp, path);
1385
2310
  }
1386
2311
  function readHeartbeat(path) {
@@ -1721,16 +2646,21 @@ var init_procCleanup = __esm({
1721
2646
  });
1722
2647
 
1723
2648
  // src/git.ts
1724
- import { existsSync as existsSync8, readdirSync as readdirSync8, statSync as statSync6 } from "fs";
2649
+ import { existsSync as existsSync9, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
1725
2650
  import { appendFile, mkdir, readFile, rm } from "fs/promises";
1726
- import { isAbsolute as isAbsolute2, join as join12 } from "path";
2651
+ import { isAbsolute as isAbsolute2, join as join15 } from "path";
1727
2652
  import { $ as $2 } from "execa";
2653
+ function isUnrecoverableRefError(e) {
2654
+ if (e instanceof MissingRefError) return true;
2655
+ const msg = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
2656
+ return MISSING_REF_RE.test(msg);
2657
+ }
1728
2658
  async function reclaimStaleWorktree(repo, worktree) {
1729
2659
  await detachWorktree(repo, worktree);
1730
2660
  }
1731
2661
  function isLinkedWorktree(dir) {
1732
2662
  try {
1733
- return statSync6(join12(dir, ".git")).isFile();
2663
+ return statSync7(join15(dir, ".git")).isFile();
1734
2664
  } catch {
1735
2665
  return false;
1736
2666
  }
@@ -1742,7 +2672,7 @@ async function detachWorktree(repo, worktree) {
1742
2672
  }
1743
2673
  }
1744
2674
  async function ensureWorktreeDirGone(worktree) {
1745
- if (!existsSync8(worktree)) return false;
2675
+ if (!existsSync9(worktree)) return false;
1746
2676
  if (!isLinkedWorktree(worktree)) {
1747
2677
  console.error(
1748
2678
  `[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 +2687,7 @@ async function ensureWorktreeDirGone(worktree) {
1757
2687
  );
1758
2688
  return true;
1759
2689
  }
1760
- if (existsSync8(worktree)) {
2690
+ if (existsSync9(worktree)) {
1761
2691
  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
2692
  }
1763
2693
  return true;
@@ -1800,7 +2730,7 @@ async function addWorktree(repo, path, branch, opts = {}) {
1800
2730
  }
1801
2731
  const busyAt = await worktreeForBranch(repo, branch);
1802
2732
  if (busyAt) {
1803
- const exists = existsSync8(busyAt);
2733
+ const exists = existsSync9(busyAt);
1804
2734
  const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
1805
2735
  if (live) throw new WorktreeBusyError(branch, busyAt);
1806
2736
  if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
@@ -1836,9 +2766,9 @@ async function ensureGitExcluded(worktree, pattern) {
1836
2766
  const out = await $2({ reject: false })`git -C ${worktree} rev-parse --git-common-dir`;
1837
2767
  const raw = out.stdout.trim();
1838
2768
  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");
2769
+ const commonDir = isAbsolute2(raw) ? raw : join15(worktree, raw);
2770
+ const infoDir = join15(commonDir, "info");
2771
+ const excludeFile = join15(infoDir, "exclude");
1842
2772
  let current = "";
1843
2773
  try {
1844
2774
  current = await readFile(excludeFile, "utf8");
@@ -1871,7 +2801,7 @@ async function worktreeDirty(path) {
1871
2801
  async function repairMovedWorktrees(targetDir) {
1872
2802
  const subdirs = (dir) => {
1873
2803
  try {
1874
- return readdirSync8(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join12(dir, d.name));
2804
+ return readdirSync10(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join15(dir, d.name));
1875
2805
  } catch {
1876
2806
  return [];
1877
2807
  }
@@ -1915,7 +2845,9 @@ async function journalForBranch(repo, branch, slug) {
1915
2845
  }
1916
2846
  async function prepareDetachedWorktree(repo, branch, worktree) {
1917
2847
  await $2`git -C ${repo} fetch --quiet origin`;
1918
- await $2`git -C ${repo} worktree add --detach --force ${worktree} ${`origin/${branch}`}`;
2848
+ const ref = `origin/${branch}`;
2849
+ if (!await revParse(repo, `${ref}^{commit}`)) throw new MissingRefError(ref, repo);
2850
+ await $2`git -C ${repo} worktree add --detach --force ${worktree} ${ref}`;
1919
2851
  }
1920
2852
  async function mergeRef(worktree, ref) {
1921
2853
  const before = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
@@ -1948,7 +2880,7 @@ async function worktreeLastCommitMs(worktree) {
1948
2880
  const sec = Number(out);
1949
2881
  return Number.isInteger(sec) && sec > 0 ? sec * 1e3 : null;
1950
2882
  }
1951
- var WorktreeBusyError, GITHUB_REMOTE_RE;
2883
+ var WorktreeBusyError, MissingRefError, MISSING_REF_RE, GITHUB_REMOTE_RE;
1952
2884
  var init_git = __esm({
1953
2885
  "src/git.ts"() {
1954
2886
  "use strict";
@@ -1963,6 +2895,17 @@ var init_git = __esm({
1963
2895
  branch;
1964
2896
  worktreePath;
1965
2897
  };
2898
+ MissingRefError = class extends Error {
2899
+ constructor(ref, repo) {
2900
+ 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)`);
2901
+ this.ref = ref;
2902
+ this.repo = repo;
2903
+ this.name = "MissingRefError";
2904
+ }
2905
+ ref;
2906
+ repo;
2907
+ };
2908
+ MISSING_REF_RE = /invalid reference|unknown revision|not a valid object name|couldn't find remote ref/i;
1966
2909
  GITHUB_REMOTE_RE = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/i;
1967
2910
  }
1968
2911
  });
@@ -1983,7 +2926,7 @@ __export(droneChannel_exports, {
1983
2926
  verifyLocalCliToken: () => verifyLocalCliToken
1984
2927
  });
1985
2928
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
1986
- import { chmodSync, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
2929
+ import { chmodSync, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
1987
2930
  import { dirname as dirname3 } from "path";
1988
2931
  import { serve } from "@hono/node-server";
1989
2932
  function loadChannelSecret(path) {
@@ -1993,7 +2936,7 @@ function loadChannelSecret(path) {
1993
2936
  } catch {
1994
2937
  }
1995
2938
  const secret = randomBytes(32).toString("hex");
1996
- mkdirSync5(dirname3(path), { recursive: true });
2939
+ mkdirSync7(dirname3(path), { recursive: true });
1997
2940
  writeFileSync9(path, secret, { mode: 384 });
1998
2941
  try {
1999
2942
  chmodSync(path, 384);
@@ -2324,7 +3267,7 @@ var init_droneChannel = __esm({
2324
3267
 
2325
3268
  // src/attachments.ts
2326
3269
  import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
2327
- import { join as join14 } from "path";
3270
+ import { join as join18 } from "path";
2328
3271
  function attachmentsFromDoc(doc, origin) {
2329
3272
  const out = [];
2330
3273
  const walk2 = (node) => {
@@ -2415,7 +3358,7 @@ async function materializeAttachments(opts) {
2415
3358
  };
2416
3359
  const result = { files: [], skipped: [] };
2417
3360
  if (opts.refs.length === 0) return result;
2418
- const dir = join14(opts.worktree, ATTACHMENTS_DIR);
3361
+ const dir = join18(opts.worktree, ATTACHMENTS_DIR);
2419
3362
  await io.removeDir(dir);
2420
3363
  let dirReady = false;
2421
3364
  let total = 0;
@@ -2461,7 +3404,7 @@ async function materializeAttachments(opts) {
2461
3404
  await io.mkdir(dir);
2462
3405
  dirReady = true;
2463
3406
  }
2464
- await io.writeFile(join14(dir, name), file.bytes);
3407
+ await io.writeFile(join18(dir, name), file.bytes);
2465
3408
  } catch (e) {
2466
3409
  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
3410
  continue;
@@ -2614,7 +3557,7 @@ var init_attachments = __esm({
2614
3557
 
2615
3558
  // src/shotUpload.ts
2616
3559
  import { readFile as fsReadFile } from "fs/promises";
2617
- import { basename as basename2, isAbsolute as isAbsolute3, join as join15 } from "path";
3560
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join19 } from "path";
2618
3561
  function shotMimetype(path) {
2619
3562
  const dot = path.lastIndexOf(".");
2620
3563
  if (dot < 0) return null;
@@ -2640,7 +3583,7 @@ async function readShots(paths, opts) {
2640
3583
  }
2641
3584
  let bytes;
2642
3585
  try {
2643
- bytes = await read(isAbsolute3(path) ? path : join15(opts.worktree, path));
3586
+ bytes = await read(isAbsolute3(path) ? path : join19(opts.worktree, path));
2644
3587
  } catch (e) {
2645
3588
  skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
2646
3589
  continue;
@@ -2717,12 +3660,12 @@ var init_shotUpload = __esm({
2717
3660
  });
2718
3661
 
2719
3662
  // src/oauth.ts
2720
- import { createHash, randomBytes as randomBytes2 } from "crypto";
3663
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
2721
3664
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
2722
- import { join as join16 } from "path";
3665
+ import { join as join20 } from "path";
2723
3666
  function makePkce() {
2724
3667
  const verifier = randomBytes2(48).toString("base64url");
2725
- const challenge = createHash("sha256").update(verifier).digest("base64url");
3668
+ const challenge = createHash2("sha256").update(verifier).digest("base64url");
2726
3669
  return { verifier, challenge };
2727
3670
  }
2728
3671
  async function registerClient(args, fetchFn = fetch) {
@@ -2854,15 +3797,15 @@ var init_oauth = __esm({
2854
3797
  AUTHORIZE = "/api/oauth/authorize";
2855
3798
  TOKEN = "/api/oauth/token";
2856
3799
  SCOPE = "openid profile email taskless:full";
2857
- STORE_PATH = join16(SWARM_HOME, "oauth.json");
2858
- CLIENT_PATH = join16(SWARM_HOME, "oauth-client.json");
3800
+ STORE_PATH = join20(SWARM_HOME, "oauth.json");
3801
+ CLIENT_PATH = join20(SWARM_HOME, "oauth-client.json");
2859
3802
  base = (api) => api.replace(/\/$/, "");
2860
3803
  }
2861
3804
  });
2862
3805
 
2863
3806
  // src/agentToken.ts
2864
3807
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
2865
- import { join as join17 } from "path";
3808
+ import { join as join21 } from "path";
2866
3809
  function loadAgentToken() {
2867
3810
  try {
2868
3811
  const s = JSON.parse(readFileSync15(STORE_PATH2, "utf8"));
@@ -2882,6 +3825,14 @@ async function probeAgentToken(args, fetchFn = fetch) {
2882
3825
  return "unreachable";
2883
3826
  }
2884
3827
  }
3828
+ function isRevokedTokenError(e) {
3829
+ if (e && typeof e === "object" && e.code === 401) return true;
3830
+ const text = e instanceof Error ? e.message : typeof e === "string" ? e : "";
3831
+ return /invalid_token|(?:http|status|code)[ =:]*401\b|\(401\)|401 unauthorized/i.test(text);
3832
+ }
3833
+ function revokedTokenLine() {
3834
+ return "[taskless] \u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u043E\u0437\u0432\u0430\u043D (401 invalid_token) \u2014 \u0440\u043E\u0439 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442 \u0440\u0430\u0431\u043E\u0442\u0443. \u041F\u043E\u0447\u0438\u043D\u0438 \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: swarm login";
3835
+ }
2885
3836
  function revokedTokenComplaint() {
2886
3837
  return [
2887
3838
  "",
@@ -2902,14 +3853,14 @@ var init_agentToken = __esm({
2902
3853
  "src/agentToken.ts"() {
2903
3854
  "use strict";
2904
3855
  init_config();
2905
- STORE_PATH2 = join17(SWARM_HOME, "agent.json");
3856
+ STORE_PATH2 = join21(SWARM_HOME, "agent.json");
2906
3857
  base2 = (api) => api.replace(/\/$/, "");
2907
3858
  }
2908
3859
  });
2909
3860
 
2910
3861
  // src/workspace.ts
2911
3862
  import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
2912
- import { join as join18 } from "path";
3863
+ import { join as join22 } from "path";
2913
3864
  function loadWorkspace() {
2914
3865
  try {
2915
3866
  const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
@@ -2926,7 +3877,7 @@ var init_workspace = __esm({
2926
3877
  "src/workspace.ts"() {
2927
3878
  "use strict";
2928
3879
  init_config();
2929
- WORKSPACE_PATH = join18(SWARM_HOME, "workspace.json");
3880
+ WORKSPACE_PATH = join22(SWARM_HOME, "workspace.json");
2930
3881
  }
2931
3882
  });
2932
3883
 
@@ -3009,6 +3960,11 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
3009
3960
  if (!doc) return null;
3010
3961
  return { title: doc.title ?? "", body: extractText(doc.body) };
3011
3962
  }
3963
+ async function getTaskContext(idOrSlug) {
3964
+ const res = await safeCall("get_task_context", { id: idOrSlug });
3965
+ const text = res?.content?.find((c) => c.type === "text")?.text;
3966
+ return typeof text === "string" && text.trim() ? text : null;
3967
+ }
3012
3968
  async function resolveId(idOrSlug) {
3013
3969
  const cached = idCache.get(idOrSlug);
3014
3970
  if (cached) return cached;
@@ -3076,6 +4032,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
3076
4032
  },
3077
4033
  backlogStatusId,
3078
4034
  getDocument,
4035
+ getTaskContext,
3079
4036
  createTask: async (input) => {
3080
4037
  if (!input.projectId) {
3081
4038
  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 +4072,19 @@ function requireTaskBody(task, slug, workspaceId) {
3115
4072
  if (!task) throw new TaskBodyUnavailableError(slug, workspaceId);
3116
4073
  return task;
3117
4074
  }
4075
+ function wrapUntrusted(parts) {
4076
+ if (parts.length === 0) return "";
4077
+ return [
4078
+ UNTRUSTED_TASK_INPUT_NOTICE,
4079
+ "",
4080
+ "\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",
4081
+ parts.join("\n\n"),
4082
+ "\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"
4083
+ ].join("\n");
4084
+ }
3118
4085
  async function fetchTaskContext(client, task) {
4086
+ const brief = await client.getTaskContext(task.id);
4087
+ if (brief) return wrapUntrusted([brief]);
3119
4088
  const parts = [];
3120
4089
  const comments = task.comments ?? [];
3121
4090
  if (comments.length > 0) {
@@ -3151,14 +4120,7 @@ ${text || "(\u043F\u0443\u0441\u0442\u043E)"}`;
3151
4120
  )
3152
4121
  );
3153
4122
  }
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");
4123
+ return wrapUntrusted(parts);
3162
4124
  }
3163
4125
  function commentIdOf(res) {
3164
4126
  const comment = res?.structuredContent?.comment;
@@ -3270,7 +4232,9 @@ async function connectTaskless(cfg) {
3270
4232
  try {
3271
4233
  await client.connect(transport, { timeout: TASKLESS_CONNECT_TIMEOUT_MS });
3272
4234
  } catch (e) {
3273
- console.warn(`Taskless: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 slug-only. \u0412\u043E\u0439\u0434\u0438 \u0437\u0430\u043D\u043E\u0432\u043E.`);
4235
+ console.warn(
4236
+ isRevokedTokenError(e) ? `${revokedTokenLine()} (\u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442 \u2014 slug-only \u0440\u0435\u0436\u0438\u043C)` : `Taskless: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 slug-only. \u0412\u043E\u0439\u0434\u0438 \u0437\u0430\u043D\u043E\u0432\u043E.`
4237
+ );
3274
4238
  return null;
3275
4239
  }
3276
4240
  const call = async (name, args) => await client.callTool({ name, arguments: args });
@@ -3294,7 +4258,9 @@ async function connectSwarmMcp(cfg) {
3294
4258
  try {
3295
4259
  await client.connect(transport);
3296
4260
  } catch (e) {
3297
- console.warn(`Swarm MCP: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 \u0431\u0443\u0444\u0435\u0440 \u0432 outbox.`);
4261
+ console.warn(
4262
+ isRevokedTokenError(e) ? `${revokedTokenLine()} (\u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442 \u2014 \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u043A\u043E\u043F\u044F\u0442\u0441\u044F \u0432 outbox)` : `Swarm MCP: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 \u0431\u0443\u0444\u0435\u0440 \u0432 outbox.`
4263
+ );
3298
4264
  return null;
3299
4265
  }
3300
4266
  return async (name, args) => await client.callTool({ name, arguments: swarmToolArgs(name, args, workspace) });
@@ -3353,7 +4319,7 @@ __export(db_exports, {
3353
4319
  SwarmDb: () => SwarmDb
3354
4320
  });
3355
4321
  import { createRequire as createRequire2 } from "module";
3356
- import { existsSync as existsSync10 } from "fs";
4322
+ import { existsSync as existsSync11 } from "fs";
3357
4323
  import { resolve as resolve3 } from "path";
3358
4324
  function rowToQuestion(row) {
3359
4325
  return {
@@ -3387,6 +4353,10 @@ function rowToRun(row) {
3387
4353
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
3388
4354
  model: row.model ?? null,
3389
4355
  cost_usd: row.cost_usd ?? 0,
4356
+ // Токены/время работы (TASWM-353) — старые прогоны без колонок → 0.
4357
+ tokens_in: row.tokens_in ?? 0,
4358
+ tokens_out: row.tokens_out ?? 0,
4359
+ duration_ms: row.duration_ms ?? 0,
3390
4360
  pr_url: row.pr_url ?? null,
3391
4361
  superseded_by: row.superseded_by ?? null,
3392
4362
  decision: row.decision ? JSON.parse(row.decision) : null,
@@ -3452,6 +4422,9 @@ CREATE TABLE IF NOT EXISTS runs (
3452
4422
  depends_on TEXT,
3453
4423
  model TEXT,
3454
4424
  cost_usd REAL NOT NULL DEFAULT 0,
4425
+ tokens_in INTEGER NOT NULL DEFAULT 0,
4426
+ tokens_out INTEGER NOT NULL DEFAULT 0,
4427
+ duration_ms INTEGER NOT NULL DEFAULT 0,
3455
4428
  pr_url TEXT,
3456
4429
  superseded_by INTEGER,
3457
4430
  decision TEXT,
@@ -3545,7 +4518,11 @@ CREATE TABLE IF NOT EXISTS imports (
3545
4518
  // Базовая ветка PR (TASWM-285): куда дрон открывает PR и от чего ответвляется.
3546
4519
  "ALTER TABLE runs ADD COLUMN base_branch TEXT",
3547
4520
  // Воркспейс задачи прогона (TASWM-288): им пинятся задачные MCP-вызовы прогона.
3548
- "ALTER TABLE runs ADD COLUMN workspace_id TEXT"
4521
+ "ALTER TABLE runs ADD COLUMN workspace_id TEXT",
4522
+ // Чем обошёлся прогон, кроме денег (TASWM-353): токены вход/выход и время работы.
4523
+ "ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
4524
+ "ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
4525
+ "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0"
3549
4526
  ]) {
3550
4527
  try {
3551
4528
  this.db.exec(alter);
@@ -3838,7 +4815,7 @@ CREATE TABLE IF NOT EXISTS imports (
3838
4815
  const repoN = normalizeRepo(repo);
3839
4816
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
3840
4817
  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}`);
4818
+ if (!existsSync11(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
3842
4819
  const src = new this.databaseCtor(abs);
3843
4820
  try {
3844
4821
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -4206,9 +5183,9 @@ var init_droneStore = __esm({
4206
5183
  });
4207
5184
 
4208
5185
  // 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";
5186
+ import { homedir as homedir5 } from "os";
5187
+ import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync12, renameSync as renameSync10 } from "fs";
5188
+ import { basename as basename4, dirname as dirname4, join as join29 } from "path";
4212
5189
  import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
4213
5190
  import { spawn as spawn3 } from "child_process";
4214
5191
 
@@ -4288,13 +5265,13 @@ init_config();
4288
5265
  init_resources();
4289
5266
  init_names2();
4290
5267
  init_storm();
4291
- import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
5268
+ import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
4292
5269
  import { dirname } from "path";
4293
5270
  import { randomUUID } from "crypto";
4294
5271
 
4295
5272
  // src/logTail.ts
4296
- import { closeSync, existsSync as existsSync2, openSync, readSync, readdirSync, statSync } from "fs";
4297
- import { join as join3 } from "path";
5273
+ import { closeSync, existsSync as existsSync4, openSync, readSync, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
5274
+ import { join as join6 } from "path";
4298
5275
  var TAIL_LINES_FALLBACK = 200;
4299
5276
  var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
4300
5277
  function fileHeader(name, size, shown) {
@@ -4304,7 +5281,7 @@ function fileHeader(name, size, shown) {
4304
5281
  function readTail(path, opts) {
4305
5282
  let size;
4306
5283
  try {
4307
- size = statSync(path).size;
5284
+ size = statSync3(path).size;
4308
5285
  } catch {
4309
5286
  return null;
4310
5287
  }
@@ -4335,10 +5312,10 @@ function readTail(path, opts) {
4335
5312
  return { text, size, shown: Buffer.byteLength(text, "utf8") };
4336
5313
  }
4337
5314
  function latestRunLaunch(runLogsDir, localRunId) {
4338
- const dir = join3(runLogsDir, String(localRunId));
5315
+ const dir = join6(runLogsDir, String(localRunId));
4339
5316
  let files;
4340
5317
  try {
4341
- files = readdirSync(dir);
5318
+ files = readdirSync4(dir);
4342
5319
  } catch {
4343
5320
  return null;
4344
5321
  }
@@ -4349,13 +5326,13 @@ function latestRunLaunch(runLogsDir, localRunId) {
4349
5326
  }
4350
5327
  if (indexes.size === 0) return null;
4351
5328
  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;
5329
+ const out = [join6(dir, `${index}.json`), join6(dir, `${index}.json.done`)].find((p) => existsSync4(p)) ?? null;
5330
+ const err = existsSync4(join6(dir, `${index}.err.log`)) ? join6(dir, `${index}.err.log`) : null;
4354
5331
  return { index, total: indexes.size, out, err };
4355
5332
  }
4356
5333
  function runLogTail(runLogsDir, localRunId, opts) {
4357
5334
  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)` };
5335
+ 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
5336
  const parts = [];
4360
5337
  let budget = opts.maxBytes;
4361
5338
  if (launch.err) {
@@ -4488,6 +5465,15 @@ var SwarmClient = class _SwarmClient {
4488
5465
  nextRunId = 1;
4489
5466
  nextEventId = 1;
4490
5467
  nextQuestionId = 1;
5468
+ // TASWM-350: ключ НЕЗАКРЫТОЙ попытки взять работу. Живёт от первого вызова claim до
5469
+ // первого настоящего ответа прода; всё это время ретраи идут с ним, и повтор после
5470
+ // обрыва связи не превращается во второе взятие. См. claim().
5471
+ pendingClaimKey = null;
5472
+ // TASWM-357: последнее НАПЕЧАТАННОЕ про пропущенные claim'ом задачи (родители и
5473
+ // заблокированные). Дедуп по тексту: claim идёт раз в ~60 c, а набор пропущенных меняется
5474
+ // редко — без этого лог был бы простынёй из одной и той же строки, и в ней потонуло бы
5475
+ // всё остальное. Меняется набор (эпик закрыли / блокер прошли) → печатаем снова.
5476
+ lastClaimSkipLog = null;
4491
5477
  // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
4492
5478
  // Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
4493
5479
  // переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
@@ -4498,6 +5484,9 @@ var SwarmClient = class _SwarmClient {
4498
5484
  // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
4499
5485
  // каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
4500
5486
  notifyLevelUnsupported = false;
5487
+ // TAS-485: то же для гашения разрешившихся тревог (swarm_notify_resolve) — прод старее
5488
+ // демона не знает тула; ретраить это каждый тик бессмысленно, см. resolveAnomalies.
5489
+ notifyResolveUnsupported = false;
4501
5490
  // Корреляция с продом.
4502
5491
  keyByLocal = /* @__PURE__ */ new Map();
4503
5492
  // local run id → runKey
@@ -4540,24 +5529,24 @@ var SwarmClient = class _SwarmClient {
4540
5529
  this.repoFullNames = registry.map((e) => e.repoFullName);
4541
5530
  this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
4542
5531
  this.host = opts.host ?? null;
4543
- this.repoExists = opts.repoExists ?? existsSync3;
5532
+ this.repoExists = opts.repoExists ?? existsSync5;
4544
5533
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
4545
5534
  this.outboxPath = opts.outboxPath;
4546
5535
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
4547
5536
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
4548
- mkdirSync2(dirname(this.outboxPath), { recursive: true });
5537
+ mkdirSync4(dirname(this.outboxPath), { recursive: true });
4549
5538
  this.recover();
4550
5539
  }
4551
5540
  // ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
4552
5541
  recover() {
4553
- if (existsSync3(this.mapPath)) {
5542
+ if (existsSync5(this.mapPath)) {
4554
5543
  try {
4555
5544
  const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
4556
5545
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
4557
5546
  } catch {
4558
5547
  }
4559
5548
  }
4560
- if (existsSync3(this.outboxPath)) {
5549
+ if (existsSync5(this.outboxPath)) {
4561
5550
  const txt = readFileSync4(this.outboxPath, "utf8");
4562
5551
  for (const line of txt.split("\n")) {
4563
5552
  const s = line.trim();
@@ -4578,7 +5567,7 @@ var SwarmClient = class _SwarmClient {
4578
5567
  // eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
4579
5568
  // переприсваиваем локально (id событий — только порядок, не корреляция с продом).
4580
5569
  replayGlobalEvents() {
4581
- if (!existsSync3(this.stormPath)) return;
5570
+ if (!existsSync5(this.stormPath)) return;
4582
5571
  let txt;
4583
5572
  try {
4584
5573
  txt = readFileSync4(this.stormPath, "utf8");
@@ -4647,8 +5636,8 @@ var SwarmClient = class _SwarmClient {
4647
5636
  this.scheduleFlush();
4648
5637
  }
4649
5638
  // Прод-патч прогона: локальные поля Run → поля swarm_run_upsert (см. TASWM-74).
4650
- // pid/worktree_path (рантайм), respawns (нет в схеме тула), снимок задачи (task_*,
4651
- // прод берёт join'ом из Task) и priority (локальная очередь ЭТОГО демона, TASWM-269)
5639
+ // pid/worktree_path (рантайм), снимок задачи (task_*, прод берёт join'ом из Task)
5640
+ // и priority (локальная очередь ЭТОГО демона, TASWM-269)
4652
5641
  // в прод НЕ уходят. kind → role (плоский enum). Self-ref
4653
5642
  // прогонов hero_run_id/resolves_run_id/superseded_by → heroId/resolvesRunId/
4654
5643
  // supersededById как ОТЛОЖЕННЫЕ ссылки (RunRef) — cuid резолвится на флаше.
@@ -4659,6 +5648,10 @@ var SwarmClient = class _SwarmClient {
4659
5648
  if ("model" in patch) out.model = patch.model ?? null;
4660
5649
  if ("name" in patch) out.droneName = patch.name ?? null;
4661
5650
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
5651
+ if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
5652
+ if ("tokens_out" in patch && patch.tokens_out !== void 0) out.tokensOut = patch.tokens_out;
5653
+ if ("duration_ms" in patch && patch.duration_ms !== void 0) out.durationMs = patch.duration_ms;
5654
+ if ("respawns" in patch && patch.respawns !== void 0) out.respawns = patch.respawns;
4662
5655
  if ("pr_url" in patch) out.prUrl = patch.pr_url ?? null;
4663
5656
  if ("branch" in patch) out.branch = patch.branch ?? null;
4664
5657
  if ("repo" in patch) out.repo = patch.repo ?? null;
@@ -4724,6 +5717,10 @@ var SwarmClient = class _SwarmClient {
4724
5717
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
4725
5718
  model: input.model ?? null,
4726
5719
  cost_usd: 0,
5720
+ tokens_in: 0,
5721
+ // копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
5722
+ tokens_out: 0,
5723
+ duration_ms: 0,
4727
5724
  pr_url: null,
4728
5725
  superseded_by: null,
4729
5726
  decision: input.decision ?? null,
@@ -4846,6 +5843,7 @@ var SwarmClient = class _SwarmClient {
4846
5843
  if (!d) return 0;
4847
5844
  d.respawns += 1;
4848
5845
  d.updated_at = stamp();
5846
+ this.enqueueRun(d, this.runFields({ respawns: d.respawns }));
4849
5847
  return d.respawns;
4850
5848
  }
4851
5849
  cancel(id, reason) {
@@ -5261,7 +6259,7 @@ var SwarmClient = class _SwarmClient {
5261
6259
  * транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
5262
6260
  */
5263
6261
  reconcileRuntime(opts) {
5264
- const wtExists = opts.worktreeExists ?? existsSync3;
6262
+ const wtExists = opts.worktreeExists ?? existsSync5;
5265
6263
  const pidByWorktree = /* @__PURE__ */ new Map();
5266
6264
  try {
5267
6265
  for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
@@ -5349,23 +6347,69 @@ var SwarmClient = class _SwarmClient {
5349
6347
  * шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
5350
6348
  */
5351
6349
  async claim() {
6350
+ this.pendingClaimKey ??= randomUUID();
5352
6351
  let res;
5353
6352
  try {
5354
6353
  res = await this.call("swarm_claim_work", {
5355
6354
  ...this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {},
5356
6355
  // TASWM-281: взятая из общей очереди задача сразу принадлежит ЭТОЙ машине —
5357
6356
  // прод помечает созданный прогон hostId, чужой демон его не увидит.
5358
- ...this.host ? { hostId: this.host.hostId } : {}
6357
+ ...this.host ? { hostId: this.host.hostId } : {},
6358
+ idempotencyKey: this.pendingClaimKey
5359
6359
  });
5360
6360
  } catch {
5361
6361
  return null;
5362
6362
  }
5363
6363
  if (res?.isError) return null;
6364
+ this.pendingClaimKey = null;
5364
6365
  const sc = res.structuredContent ?? {};
6366
+ this.logClaimSkips(sc);
5365
6367
  if (!sc.claimed || typeof sc.run !== "object" || sc.run === null) return null;
5366
6368
  const local = this.hydrateRun(sc.run);
5367
6369
  return local ? this.mat(local) : null;
5368
6370
  }
6371
+ /**
6372
+ * TASWM-357: назвать в логе задачи, которые прод НЕ отдал в работу осознанно — родителей с
6373
+ * незавершёнными подзадачами (работа всегда в листьях) и листья с незакрытым блокером
6374
+ * (явный порядок, связь задач «блокирует»). Печатаем ЗДЕСЬ, а не на проде: оба фильтра
6375
+ * живут SQL-условиями и молчат по природе, а `display` claim'а демон не логирует — без
6376
+ * этой строки «почему эпик стоит нетронутым» не видно ниоткуда, и оператор ровно так же
6377
+ * гадает, как гадал, пока дрон вставал на эпике с вопросом «меня спавнили на родителя».
6378
+ *
6379
+ * Формат прода — `data.skippedParents: [{slug,title,openChildren}]` и
6380
+ * `data.skippedBlocked: [{slug,title,blockers[]}]`. Читаем защищённо: у старого прода
6381
+ * полей нет вовсе (тогда молчим), а разбирать чужую строку `display` вместо структуры —
6382
+ * тот же класс хрупкости, что уже кусал нас в notifyAnomaly.
6383
+ */
6384
+ logClaimSkips(sc) {
6385
+ const rows = (raw) => (Array.isArray(raw) ? raw : []).map((p) => typeof p === "object" && p !== null ? p : null).filter((p) => p !== null && typeof p.slug === "string");
6386
+ const others = (flag) => flag === true ? " \u0438 \u0434\u0440\u0443\u0433\u0438\u0435" : "";
6387
+ const lines = [];
6388
+ const parents = rows(sc.skippedParents).map(
6389
+ (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)`
6390
+ );
6391
+ if (parents.length > 0) {
6392
+ lines.push(
6393
+ `[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)}`
6394
+ );
6395
+ }
6396
+ const blocked = rows(sc.skippedBlocked).map(
6397
+ (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"}`
6398
+ );
6399
+ if (blocked.length > 0) {
6400
+ lines.push(
6401
+ `[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)}`
6402
+ );
6403
+ }
6404
+ const text = lines.join("\n");
6405
+ if (text.length === 0) {
6406
+ this.lastClaimSkipLog = null;
6407
+ return;
6408
+ }
6409
+ if (text === this.lastClaimSkipLog) return;
6410
+ this.lastClaimSkipLog = text;
6411
+ for (const line of lines) console.log(line);
6412
+ }
5369
6413
  /**
5370
6414
  * TASWM-281: пометить своими прогоны, под которыми на ЭТОЙ машине нашёлся ЖИВОЙ
5371
6415
  * процесс (после reconcileRuntime на старте). Это ровно те прогоны, где сомнений в
@@ -5639,12 +6683,20 @@ var SwarmClient = class _SwarmClient {
5639
6683
  * Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
5640
6684
  * означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
5641
6685
  *
6686
+ * TASWM-352: тем же пульсом уходит ЁМКОСТЬ этой машины (`capacity` — живой потолок
6687
+ * дронов ЭТОГО демона: машинный оверрайд, иначе общая настройка владельца). По ней
6688
+ * планировщик роя раскладывает очередь между машинами: до этого у прода было ОДНО
6689
+ * число на владельца, и ноутбук с рабочей станцией делили его поровну, хотя тянут
6690
+ * разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
6691
+ * трогает прошлое значение — та же логика, что у repoFullNames.
6692
+ *
5642
6693
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
5643
6694
  */
5644
- async heartbeat(daemonVersion, pause) {
6695
+ async heartbeat(daemonVersion, pause, capacity) {
5645
6696
  try {
5646
6697
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
5647
6698
  if (daemonVersion) args.daemonVersion = daemonVersion;
6699
+ if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
5648
6700
  if (pause !== void 0) {
5649
6701
  args.paused = !!pause?.paused;
5650
6702
  if (pause?.reason) args.pauseReason = pause.reason;
@@ -5750,6 +6802,35 @@ var SwarmClient = class _SwarmClient {
5750
6802
  const res = await this.call("swarm_notify", base3);
5751
6803
  if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
5752
6804
  }
6805
+ /**
6806
+ * TAS-485: погасить в инбоксе владельца тревоги, чьё условие ушло. Ключи — те же, с
6807
+ * которыми уходил notifyAnomaly (прод сам ставит им префикс `swarm:`).
6808
+ *
6809
+ * Бросает при сбое — СОЗНАТЕЛЬНО, как notifyAnomaly: вызывающий (tickOnce) вычёркивает
6810
+ * ключи из durable-очереди только после успеха, иначе «погашено» разошлось бы с тем,
6811
+ * что человек видит в инбоксе, и разошлось бы навсегда — повторить будет некому.
6812
+ *
6813
+ * ДЕПЛОЙ-ОКНО. Демон из свежего чекаута против ещё не задеплоенного прода получит
6814
+ * «неизвестный тул». Это не ошибка доставки, а отсутствие фичи на той стороне: ретраить
6815
+ * её каждые ~4 секунды бессмысленно, поэтому первую такую осечку запоминаем на жизнь
6816
+ * процесса и дальше молча пропускаем гашение (надзор в остальном работает как раньше).
6817
+ * Тот же приём, что у notifyAnomaly с аргументом `level`.
6818
+ */
6819
+ async resolveAnomalies(keys) {
6820
+ const tags = [...new Set(keys.filter((k) => k.length > 0 && k.length <= 120))];
6821
+ if (tags.length === 0 || this.notifyResolveUnsupported) return;
6822
+ const res = await this.call("swarm_notify_resolve", { tags });
6823
+ if (!res?.isError) return;
6824
+ const text = JSON.stringify(res);
6825
+ if (text.includes("swarm_notify_resolve") && (text.includes("Unknown tool") || text.includes("\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D"))) {
6826
+ this.notifyResolveUnsupported = true;
6827
+ console.warn(
6828
+ "[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."
6829
+ );
6830
+ return;
6831
+ }
6832
+ throw new Error(`swarm_notify_resolve \u043E\u0442\u0432\u0435\u0440\u0433 ${tags.length} \u043A\u043B\u044E\u0447(\u0435\u0439)`);
6833
+ }
5753
6834
  /**
5754
6835
  * TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
5755
6836
  * через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
@@ -5814,6 +6895,10 @@ var SwarmClient = class _SwarmClient {
5814
6895
  depends_on: [],
5815
6896
  model: null,
5816
6897
  cost_usd: 0,
6898
+ tokens_in: 0,
6899
+ // токены и время работы (TASWM-353) — прод-поля, заполняются ниже
6900
+ tokens_out: 0,
6901
+ duration_ms: 0,
5817
6902
  pr_url: null,
5818
6903
  superseded_by: null,
5819
6904
  decision: null,
@@ -5824,9 +6909,9 @@ var SwarmClient = class _SwarmClient {
5824
6909
  review_verdict: null,
5825
6910
  review_phase: null,
5826
6911
  // вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
5827
- // Локальная бухгалтерия (в прод-схему не уходит, как respawns) — на гидрации из
5828
- // прода дефолт 0; существующая локальная запись НЕ перетирается (поле не в field-copy
5829
- // ниже), в отличие от respawns, — авто-ревью v2, TASWM-162.
6912
+ // Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
6913
+ // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
6914
+ // в отличие от respawns, — авто-ревью v2, TASWM-162.
5830
6915
  review_cycle: 0,
5831
6916
  task_title: null,
5832
6917
  task_desc: null,
@@ -5878,6 +6963,9 @@ var SwarmClient = class _SwarmClient {
5878
6963
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
5879
6964
  local.model = pr.model ?? null;
5880
6965
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
6966
+ local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
6967
+ local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
6968
+ local.duration_ms = typeof pr.durationMs === "number" ? pr.durationMs : 0;
5881
6969
  local.pr_url = pr.prUrl ?? null;
5882
6970
  local.stop_reason = pr.stopReason ?? null;
5883
6971
  local.respawns = typeof pr.respawns === "number" ? pr.respawns : 0;
@@ -6135,13 +7223,13 @@ var SwarmClient = class _SwarmClient {
6135
7223
  }
6136
7224
  persistPending() {
6137
7225
  try {
6138
- writeFileSync3(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
7226
+ writeFileSync4(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
6139
7227
  } catch {
6140
7228
  }
6141
7229
  }
6142
7230
  persistMap() {
6143
7231
  try {
6144
- writeFileSync3(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
7232
+ writeFileSync4(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
6145
7233
  } catch {
6146
7234
  }
6147
7235
  }
@@ -6171,11 +7259,11 @@ init_config();
6171
7259
  init_heartbeat();
6172
7260
 
6173
7261
  // src/logRotate.ts
6174
- import { copyFileSync, renameSync as renameSync4, statSync as statSync2, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync4 } from "fs";
7262
+ import { copyFileSync, renameSync as renameSync4, statSync as statSync4, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
6175
7263
  var realIo = {
6176
7264
  size: (path) => {
6177
7265
  try {
6178
- return statSync2(path).size;
7266
+ return statSync4(path).size;
6179
7267
  } catch {
6180
7268
  return null;
6181
7269
  }
@@ -6184,7 +7272,7 @@ var realIo = {
6184
7272
  rename: renameSync4,
6185
7273
  truncate: (path) => truncateSync(path, 0),
6186
7274
  unlink: unlinkSync2,
6187
- exists: existsSync4
7275
+ exists: existsSync6
6188
7276
  };
6189
7277
  var realOwnerIo = {
6190
7278
  statFd: (fd) => {
@@ -6197,7 +7285,7 @@ var realOwnerIo = {
6197
7285
  },
6198
7286
  statPath: (path) => {
6199
7287
  try {
6200
- const s = statSync2(path);
7288
+ const s = statSync4(path);
6201
7289
  return { dev: s.dev, ino: s.ino };
6202
7290
  } catch {
6203
7291
  return null;
@@ -6268,13 +7356,13 @@ async function buildRepoRegistry(paths) {
6268
7356
  }
6269
7357
 
6270
7358
  // src/repoMemory.ts
6271
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
6272
- import { join as join4 } from "path";
7359
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
7360
+ import { join as join7 } from "path";
6273
7361
  var FILE = "repos.json";
6274
7362
  function loadRememberedRepos(home) {
6275
7363
  let raw;
6276
7364
  try {
6277
- raw = readFileSync6(join4(home, FILE), "utf8");
7365
+ raw = readFileSync6(join7(home, FILE), "utf8");
6278
7366
  } catch {
6279
7367
  return [];
6280
7368
  }
@@ -6289,7 +7377,7 @@ function loadRememberedRepos(home) {
6289
7377
  function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
6290
7378
  const clean = paths.filter((p) => p.trim().length > 0);
6291
7379
  if (clean.length === 0) return;
6292
- writeFileSync5(join4(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
7380
+ writeFileSync6(join7(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
6293
7381
  `);
6294
7382
  }
6295
7383
  function pickRepoPaths(args) {
@@ -6302,13 +7390,13 @@ function pickRepoPaths(args) {
6302
7390
  // src/repoDiscovery.ts
6303
7391
  init_src();
6304
7392
  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";
7393
+ import { homedir as homedir3 } from "os";
7394
+ import { readFileSync as readFileSync8, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
7395
+ import { isAbsolute, join as join9, resolve as resolve2 } from "path";
6308
7396
 
6309
7397
  // src/repoScan.ts
6310
- import { readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync3 } from "fs";
6311
- import { join as join5 } from "path";
7398
+ import { readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
7399
+ import { join as join8 } from "path";
6312
7400
  var SKIP = /* @__PURE__ */ new Set([
6313
7401
  "node_modules",
6314
7402
  "vendor",
@@ -6338,7 +7426,7 @@ function parseOriginFromGitConfig(text) {
6338
7426
  }
6339
7427
  function originOf(dir) {
6340
7428
  try {
6341
- return parseOriginFromGitConfig(readFileSync7(join5(dir, ".git", "config"), "utf8"));
7429
+ return parseOriginFromGitConfig(readFileSync7(join8(dir, ".git", "config"), "utf8"));
6342
7430
  } catch {
6343
7431
  return null;
6344
7432
  }
@@ -6367,15 +7455,15 @@ function walk(dir, depth, maxDepth, found) {
6367
7455
  }
6368
7456
  let entries;
6369
7457
  try {
6370
- entries = readdirSync2(dir);
7458
+ entries = readdirSync5(dir);
6371
7459
  } catch {
6372
7460
  return;
6373
7461
  }
6374
7462
  for (const name of entries) {
6375
7463
  if (name.startsWith(".") || SKIP.has(name)) continue;
6376
- const child = join5(dir, name);
7464
+ const child = join8(dir, name);
6377
7465
  try {
6378
- if (!statSync3(child).isDirectory()) continue;
7466
+ if (!statSync5(child).isDirectory()) continue;
6379
7467
  } catch {
6380
7468
  continue;
6381
7469
  }
@@ -6395,13 +7483,13 @@ function defaultScanRoots(home, knownPaths = []) {
6395
7483
  if (parent && parent !== p) add(parent);
6396
7484
  }
6397
7485
  for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
6398
- add(join5(home, name));
7486
+ add(join8(home, name));
6399
7487
  }
6400
7488
  return out;
6401
7489
  }
6402
7490
 
6403
7491
  // src/repoDiscovery.ts
6404
- function resolveScanRoots(raw, home = homedir2(), knownPaths = []) {
7492
+ function resolveScanRoots(raw, home = homedir3(), knownPaths = []) {
6405
7493
  const v = (raw ?? "").trim();
6406
7494
  if (!v) return defaultScanRoots(home, knownPaths);
6407
7495
  if (["off", "none", "0"].includes(v.toLowerCase())) return [];
@@ -6409,7 +7497,7 @@ function resolveScanRoots(raw, home = homedir2(), knownPaths = []) {
6409
7497
  for (const part of v.split(",")) {
6410
7498
  const p = part.trim();
6411
7499
  if (!p) continue;
6412
- const abs = p === "~" ? home : p.startsWith("~/") ? join6(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
7500
+ const abs = p === "~" ? home : p.startsWith("~/") ? join9(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
6413
7501
  if (!out.includes(abs)) out.push(abs);
6414
7502
  }
6415
7503
  return out;
@@ -6547,7 +7635,7 @@ function readRepoMapCache(path) {
6547
7635
  function writeRepoMapCache(path, entries) {
6548
7636
  try {
6549
7637
  const tmp = `${path}.tmp`;
6550
- writeFileSync6(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
7638
+ writeFileSync7(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
6551
7639
  renameSync5(tmp, path);
6552
7640
  } catch {
6553
7641
  }
@@ -6555,17 +7643,17 @@ function writeRepoMapCache(path, entries) {
6555
7643
 
6556
7644
  // src/hostIdentity.ts
6557
7645
  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";
7646
+ import { existsSync as existsSync7, readFileSync as readFileSync9, renameSync as renameSync6, writeFileSync as writeFileSync8 } from "fs";
7647
+ import { join as join10 } from "path";
6560
7648
  import { randomUUID as randomUUID2 } from "crypto";
6561
7649
  function hostIdentityPath(home) {
6562
- return join7(home, "host.json");
7650
+ return join10(home, "host.json");
6563
7651
  }
6564
7652
  function loadHostIdentity(deps) {
6565
- const exists = deps.exists ?? existsSync5;
7653
+ const exists = deps.exists ?? existsSync7;
6566
7654
  const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
6567
7655
  const writeFile2 = deps.writeFile ?? ((p, data) => {
6568
- writeFileSync7(`${p}.tmp`, data);
7656
+ writeFileSync8(`${p}.tmp`, data);
6569
7657
  renameSync6(`${p}.tmp`, p);
6570
7658
  });
6571
7659
  const host = deps.hostname ?? hostname;
@@ -6748,87 +7836,15 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
6748
7836
 
6749
7837
  // src/launcher.ts
6750
7838
  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";
7839
+ import { closeSync as closeSync2, existsSync as existsSync8, mkdirSync as mkdirSync6, openSync as openSync2, readdirSync as readdirSync7 } from "fs";
6752
7840
  import { createRequire } from "module";
6753
- import { homedir as homedir3 } from "os";
6754
- import { dirname as dirname2, join as join9 } from "path";
7841
+ import { homedir as homedir4 } from "os";
7842
+ import { dirname as dirname2, join as join12 } from "path";
6755
7843
  import { fileURLToPath } from "url";
6756
7844
 
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;
6805
- }
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."
6824
- );
6825
- }
6826
- return CLAUDE_FALLBACK;
6827
- }
6828
-
6829
7845
  // 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";
7846
+ import { lstatSync, mkdirSync as mkdirSync5, readdirSync as readdirSync6, rmSync, statSync as statSync6, symlinkSync as symlinkSync2 } from "fs";
7847
+ import { join as join11, relative, sep } from "path";
6832
7848
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
6833
7849
  function droneHomeDeny(env = process.env) {
6834
7850
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -6846,34 +7862,34 @@ function buildDroneHome(opts) {
6846
7862
  const { realHome, droneHome } = opts;
6847
7863
  const deny = opts.deny ?? droneHomeDeny();
6848
7864
  const platform = opts.platform ?? process.platform;
6849
- mkdirSync3(droneHome, { recursive: true, mode: 448 });
6850
- const wanted = new Set(mirroredEntries(readdirSync4(realHome), deny, platform));
7865
+ mkdirSync5(droneHome, { recursive: true, mode: 448 });
7866
+ const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
6851
7867
  const rel = relative(realHome, droneHome);
6852
7868
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
6853
- for (const existing of readdirSync4(droneHome)) {
7869
+ for (const existing of readdirSync6(droneHome)) {
6854
7870
  if (wanted.has(existing)) continue;
6855
- const p = join8(droneHome, existing);
7871
+ const p = join11(droneHome, existing);
6856
7872
  try {
6857
7873
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
6858
7874
  } catch {
6859
7875
  }
6860
7876
  }
6861
7877
  for (const name of wanted) {
6862
- const link = join8(droneHome, name);
7878
+ const link = join11(droneHome, name);
6863
7879
  try {
6864
7880
  lstatSync(link);
6865
7881
  continue;
6866
7882
  } catch {
6867
7883
  }
6868
- const target = join8(realHome, name);
7884
+ const target = join11(realHome, name);
6869
7885
  let isDir;
6870
7886
  try {
6871
- isDir = statSync4(target).isDirectory();
7887
+ isDir = statSync6(target).isDirectory();
6872
7888
  } catch {
6873
7889
  continue;
6874
7890
  }
6875
7891
  try {
6876
- symlinkSync(target, link, isDir ? "junction" : "file");
7892
+ symlinkSync2(target, link, isDir ? "junction" : "file");
6877
7893
  } catch (e) {
6878
7894
  if (e.code !== "EEXIST") throw e;
6879
7895
  }
@@ -6886,10 +7902,10 @@ var REAL_PATH_ENV_PINS = [
6886
7902
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
6887
7903
  ];
6888
7904
  function realPathEnvPins(realHome) {
6889
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join8(realHome, ...segments)]));
7905
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join11(realHome, ...segments)]));
6890
7906
  }
6891
- function droneDenyRules(realHome, swarmHome) {
6892
- const paths = [
7907
+ function dronePrivatePaths(realHome, swarmHome) {
7908
+ return [
6893
7909
  // НЕ `${swarmHome}/**`. Worktree дрона живёт ВНУТРИ swarmHome
6894
7910
  // (`<swarmHome>/worktrees.noindex/<repo>/<slug>`), и правило на весь каталог закрывало
6895
7911
  // дрону его собственный рабочий каталог: Read/Write/Edit отвечали «File is in a directory
@@ -6910,11 +7926,6 @@ function droneDenyRules(realHome, swarmHome) {
6910
7926
  `${realHome}/.claude/.credentials.json`,
6911
7927
  `${realHome}/.npmrc`
6912
7928
  ];
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
7929
  }
6919
7930
 
6920
7931
  // src/testForks.ts
@@ -6946,6 +7957,7 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
6946
7957
  }
6947
7958
 
6948
7959
  // src/launcher.ts
7960
+ init_executors();
6949
7961
  var requireFromHere = createRequire(import.meta.url);
6950
7962
  function resolveTsxCli() {
6951
7963
  try {
@@ -6956,29 +7968,12 @@ function resolveTsxCli() {
6956
7968
  );
6957
7969
  }
6958
7970
  }
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
- }
7971
+ var MCP_SERVER_NAME = "overmind";
6977
7972
  function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
6978
7973
  const env = { ...base3 };
6979
7974
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
6980
7975
  if (isolate && !base3.TURBO_CACHE_DIR) {
6981
- env.TURBO_CACHE_DIR = join9(worktree, ".turbo", "cache");
7976
+ env.TURBO_CACHE_DIR = join12(worktree, ".turbo", "cache");
6982
7977
  }
6983
7978
  Object.assign(env, droneTestEnv(droneCap, base3));
6984
7979
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -7006,49 +8001,20 @@ var DAEMON_STATE_ENV = [
7006
8001
  "TASKLESS_TOKEN"
7007
8002
  ];
7008
8003
  function bundledRoleDir(kind) {
7009
- return join9(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
8004
+ return join12(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
7010
8005
  }
7011
8006
  function roleDir(worktree, kind) {
7012
- return join9(worktree, ".claude", "roles", kind);
8007
+ return join12(worktree, ".claude", "roles", kind);
7013
8008
  }
7014
8009
  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;
8010
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync8(dir));
7045
8011
  }
7046
8012
  function nextRunLogPath(runLogsDir, runId) {
7047
- const dir = join9(runLogsDir, String(runId));
7048
- mkdirSync4(dir, { recursive: true });
8013
+ const dir = join12(runLogsDir, String(runId));
8014
+ mkdirSync6(dir, { recursive: true });
7049
8015
  let files = [];
7050
8016
  try {
7051
- files = readdirSync5(dir);
8017
+ files = readdirSync7(dir);
7052
8018
  } catch {
7053
8019
  }
7054
8020
  let next = 0;
@@ -7056,7 +8022,7 @@ function nextRunLogPath(runLogsDir, runId) {
7056
8022
  const m = /^(\d+)\.json/.exec(f);
7057
8023
  if (m) next = Math.max(next, Number(m[1]) + 1);
7058
8024
  }
7059
- return join9(dir, `${next}.json`);
8025
+ return join12(dir, `${next}.json`);
7060
8026
  }
7061
8027
  function openNextRunLog(runLogsDir, runId) {
7062
8028
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -7091,57 +8057,63 @@ function makeLauncher(cfg) {
7091
8057
  );
7092
8058
  }
7093
8059
  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
- }
8060
+ const mcp = {
8061
+ name: MCP_SERVER_NAME,
8062
+ command: process.execPath,
8063
+ args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["--conditions=development", resolveTsxCli(), cfg.overmindEntry, "--drone", "--run", String(run.id)],
8064
+ // SWARM_DRONE_CHANNEL_URL/TOKEN (TASWM-295) ИЛИ SWARM_DB (откат) — см. channelEnv выше.
8065
+ // Канал: дрон говорит только с демоном, файл базы ему не отдаётся вовсе. Токен подписан
8066
+ // на СТАБИЛЬНЫЙ ключ прогона, поэтому переживает рестарт демона.
8067
+ // SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — доверенный
8068
+ // источник «чья это сессия» (read_task и отчёт о сдаче доверяют ему, а не строке,
8069
+ // найденной по номеру прогона).
8070
+ // SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID (TASWM-263) и SWARM_RUN_NAME
8071
+ // (TASWM-265): роль, связи и имя прогона той же спавн-фиксацией. Нужны РЕЖИМУ ОТКАТА —
8072
+ // ensureRunHydrated заводит ими стаб-строку в sqlite, если демон в client-режиме её там
8073
+ // не заводил. На канале прогон отдаёт демон целиком, и стаб не нужен.
8074
+ // SWARM_WORKSPACE_ID (TASWM-288): ВОРКСПЕЙС прогона — той же спавн-фиксацией: задачные
8075
+ // вызовы (read_task, отчёт, тулы Героя) дрон делает НЕ через канал, а прямо в Taskless,
8076
+ // и воркспейс им нужен верно сразу. Без него read_task/отчёт дрона
8077
+ // ушли бы с глобальным пином демона — ровно то, что чинит эта задача. Пусто
8078
+ // (прогон до фичи / sqlite-бэкенд) → ключ не кладём, дрон берёт глобальный пин.
8079
+ // SWARM_HOME (TASWM-289): дрон едет с ТЕНЕВЫМ HOME, в котором `.taskless-swarm`
8080
+ // нет вовсе, — а дрон-MCP из этого же дерева процессов читает oauth.json
8081
+ // (connectTaskless в runDroneServer), пульс овермайнда и workspace.json. Наследовать
8082
+ // ему нечего, поэтому реальный SWARM_HOME кладём явно. Дрон этот конфиг прочитать
8083
+ // может, но нового ему это не даёт: его собственный worktree лежит ВНУТРИ
8084
+ // SWARM_HOME, путь он и так знает (см. «чего этот модуль не делает» в droneHome.ts).
8085
+ env: {
8086
+ ...channelEnv,
8087
+ ...cfg.droneIsolation ? { SWARM_HOME: cfg.droneIsolation.swarmHome } : {},
8088
+ SWARM_TASK_SLUG: run.task_slug,
8089
+ SWARM_RUN_KIND: run.kind,
8090
+ SWARM_RUN_NAME: run.name,
8091
+ ...run.workspace_id ? { SWARM_WORKSPACE_ID: run.workspace_id } : {},
8092
+ ...run.reviews_run_id != null ? { SWARM_REVIEWS_RUN_ID: String(run.reviews_run_id) } : {},
8093
+ ...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
7131
8094
  }
7132
8095
  };
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();
8096
+ const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8097
+ const realHome = cfg.droneIsolation?.realHome ?? homedir4();
7138
8098
  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"))) {
8099
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join12(cfg.droneIsolation.swarmHome, "token"))) {
7141
8100
  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.`
8101
+ `[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
8102
  );
7144
8103
  }
8104
+ const executor = cfg.executor ?? resolveExecutor();
8105
+ const plan = executor.plan({
8106
+ worktree,
8107
+ prompt,
8108
+ continueSession: !!continueSession,
8109
+ model,
8110
+ mcp,
8111
+ skillDirs: roleDirs(worktree, run.kind),
8112
+ // Закрытые дрону пути имеют смысл только при теневом HOME: без него дрон и так
8113
+ // сидит в HOME демона, и запирать половину дверей — самообман.
8114
+ denyPaths: cfg.droneIsolation ? dronePrivatePaths(realHome, cfg.droneIsolation.swarmHome) : [],
8115
+ realHome
8116
+ });
7145
8117
  let stdio = "ignore";
7146
8118
  let outFd;
7147
8119
  let errFd;
@@ -7151,111 +8123,79 @@ function makeLauncher(cfg) {
7151
8123
  errFd = fds.err ?? void 0;
7152
8124
  stdio = ["ignore", outFd, errFd ?? "ignore"];
7153
8125
  }
7154
- const child = spawn(cfg.claudeBin ?? resolveClaudeBin(), claudeArgs(cfgPath, prompt, continueSession, model, pluginDirs, settings), {
8126
+ const child = spawn(cfg.bin ?? plan.bin, plan.args, {
7155
8127
  cwd: worktree,
7156
8128
  detached: true,
7157
8129
  stdio,
7158
8130
  // Пер-worktree TURBO_CACHE_DIR (TASWM-232): дрон не отыгрывает кеш соседнего
7159
- // worktree, проверки исполняются реально. Теневой HOME + чистка env от состояния
7160
- // демона (TASWM-289). Доля тестовых форков от числа дронов на хосте (TASWM-246).
7161
- // Остальной env демона наследуется, как раньше.
7162
- env: droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null)
7163
- });
7164
- if (outFd !== void 0) closeSync2(outFd);
7165
- if (errFd !== void 0) closeSync2(errFd);
7166
- child.unref();
7167
- if (child.pid == null) throw new Error("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043D\u0435\u0442 pid)");
7168
- return child.pid;
7169
- };
7170
- }
7171
-
7172
- // src/overmind.ts
7173
- init_procCleanup();
7174
-
7175
- // 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);
8131
+ // worktree, проверки исполняются реально. Теневой HOME + чистка env от состояния
8132
+ // демона (TASWM-289). Доля тестовых форков от числа дронов на хосте (TASWM-246).
8133
+ // Остальной env демона наследуется, как раньше. Добавки адаптера — последними:
8134
+ // они про его собственное состояние, и перебивать их ройным нечем.
8135
+ env: { ...droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null), ...plan.env }
8136
+ });
8137
+ if (outFd !== void 0) closeSync2(outFd);
8138
+ if (errFd !== void 0) closeSync2(errFd);
8139
+ child.unref();
8140
+ if (child.pid == null) throw new Error("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043D\u0435\u0442 pid)");
8141
+ return child.pid;
8142
+ };
7222
8143
  }
7223
8144
 
8145
+ // src/overmind.ts
8146
+ init_procCleanup();
8147
+
7224
8148
  // src/cost.ts
8149
+ init_executors();
8150
+ import { readFileSync as readFileSync10, readdirSync as readdirSync8, renameSync as renameSync7 } from "fs";
8151
+ import { join as join13 } from "path";
7225
8152
  var UNPROCESSED_LOG_RE = /^\d+\.json$/;
7226
8153
  function unprocessedLogFiles(dir) {
7227
8154
  try {
7228
- return readdirSync6(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
8155
+ return readdirSync8(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
7229
8156
  } catch {
7230
8157
  return [];
7231
8158
  }
7232
8159
  }
7233
8160
  function accountRunCost(deps, run) {
7234
- const dir = join10(deps.runLogsDir, String(run.id));
8161
+ const dir = join13(deps.runLogsDir, String(run.id));
7235
8162
  const files = unprocessedLogFiles(dir);
7236
8163
  if (files.length === 0) return;
7237
8164
  const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8165
+ const executor = deps.executor ?? resolveExecutor();
7238
8166
  let added = 0;
8167
+ let tokensIn = 0;
8168
+ let tokensOut = 0;
8169
+ let durationMs = 0;
7239
8170
  const processed = [];
7240
8171
  for (const f of files) {
7241
- const full = join10(dir, f);
8172
+ const full = join13(dir, f);
7242
8173
  let raw = null;
7243
8174
  try {
7244
8175
  raw = readFileSync10(full, "utf8");
7245
8176
  } catch {
7246
8177
  continue;
7247
8178
  }
7248
- const result = parseClaudeResultJson(raw);
8179
+ const result = executor.parseResult(raw, model);
7249
8180
  if (!result) {
7250
8181
  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
8182
  } 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;
8183
+ if (result.warning) console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${result.warning}`);
8184
+ added += result.costUsd;
8185
+ tokensIn += result.tokensIn ?? 0;
8186
+ tokensOut += result.tokensOut ?? 0;
8187
+ durationMs += result.durationMs ?? 0;
7255
8188
  }
7256
8189
  processed.push(full);
7257
8190
  }
7258
- if (added > 0) deps.db.updateRun(run.id, { cost_usd: run.cost_usd + added });
8191
+ if (added > 0 || tokensIn > 0 || tokensOut > 0 || durationMs > 0) {
8192
+ deps.db.updateRun(run.id, {
8193
+ cost_usd: run.cost_usd + added,
8194
+ tokens_in: run.tokens_in + tokensIn,
8195
+ tokens_out: run.tokens_out + tokensOut,
8196
+ duration_ms: run.duration_ms + durationMs
8197
+ });
8198
+ }
7259
8199
  for (const full of processed) {
7260
8200
  try {
7261
8201
  renameSync7(full, `${full}.done`);
@@ -7268,14 +8208,15 @@ function makeCostAccountant(deps) {
7268
8208
  }
7269
8209
 
7270
8210
  // src/runOutcome.ts
7271
- import { readdirSync as readdirSync7, readFileSync as readFileSync11 } from "fs";
7272
- import { join as join11 } from "path";
8211
+ init_executors();
8212
+ import { readdirSync as readdirSync9, readFileSync as readFileSync11 } from "fs";
8213
+ import { join as join14 } from "path";
7273
8214
  var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
7274
- function classifyExit(runLogsDir, runId) {
7275
- const dir = join11(runLogsDir, String(runId));
8215
+ function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
8216
+ const dir = join14(runLogsDir, String(runId));
7276
8217
  let files;
7277
8218
  try {
7278
- files = readdirSync7(dir);
8219
+ files = readdirSync9(dir);
7279
8220
  } catch {
7280
8221
  return "unknown";
7281
8222
  }
@@ -7289,14 +8230,15 @@ function classifyExit(runLogsDir, runId) {
7289
8230
  if (!latest) return "unknown";
7290
8231
  let raw;
7291
8232
  try {
7292
- raw = readFileSync11(join11(dir, latest.name), "utf8");
8233
+ raw = readFileSync11(join14(dir, latest.name), "utf8");
7293
8234
  } catch {
7294
8235
  return "unknown";
7295
8236
  }
7296
- return parseClaudeResultJson(raw) ? "graceful" : "crash";
8237
+ return executor.parseResult(raw, null) ? "graceful" : "crash";
7297
8238
  }
7298
- function makeExitClassifier(runLogsDir) {
7299
- return (run) => classifyExit(runLogsDir, run.id);
8239
+ function makeExitClassifier(runLogsDir, executor) {
8240
+ const exec = executor ?? resolveExecutor();
8241
+ return (run) => classifyExit(runLogsDir, run.id, exec);
7300
8242
  }
7301
8243
 
7302
8244
  // src/orchestrator.ts
@@ -7304,20 +8246,90 @@ init_src();
7304
8246
  init_git();
7305
8247
 
7306
8248
  // src/deps.ts
7307
- import { existsSync as existsSync9, readdirSync as readdirSync9, readFileSync as readFileSync12 } from "fs";
7308
- import { join as join13 } from "path";
8249
+ import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync12 } from "fs";
8250
+ import { join as join17 } from "path";
7309
8251
  import { execa } from "execa";
8252
+
8253
+ // src/prepareLock.ts
8254
+ import { createHash } from "crypto";
8255
+ import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as statSync8, writeSync } from "fs";
8256
+ import { join as join16 } from "path";
8257
+ import { tmpdir } from "os";
8258
+ var WAIT_TIMEOUT_MS = 5 * 6e4;
8259
+ var POLL_MS = 300;
8260
+ var STALE_MS = 20 * 6e4;
8261
+ function machineLockPath(key, dir = tmpdir()) {
8262
+ return join16(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
8263
+ }
8264
+ function defaultTryCreate(path, content) {
8265
+ try {
8266
+ const fd = openSync3(path, "wx");
8267
+ try {
8268
+ writeSync(fd, content);
8269
+ } finally {
8270
+ closeSync3(fd);
8271
+ }
8272
+ return true;
8273
+ } catch {
8274
+ return false;
8275
+ }
8276
+ }
8277
+ function defaultAgeMs(path) {
8278
+ try {
8279
+ return Date.now() - statSync8(path).mtimeMs;
8280
+ } catch {
8281
+ return null;
8282
+ }
8283
+ }
8284
+ async function withMachineLock(key, fn, io = {}) {
8285
+ const tryCreate = io.tryCreate ?? defaultTryCreate;
8286
+ const remove = io.remove ?? ((p) => {
8287
+ try {
8288
+ rmSync2(p, { force: true });
8289
+ } catch {
8290
+ }
8291
+ });
8292
+ const ageMs2 = io.ageMs ?? defaultAgeMs;
8293
+ const wait = io.wait ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
8294
+ const now = io.now ?? Date.now;
8295
+ const path = machineLockPath(key, io.dir ?? tmpdir());
8296
+ const deadline = now() + WAIT_TIMEOUT_MS;
8297
+ let locked = false;
8298
+ for (; ; ) {
8299
+ if (tryCreate(path, `${process.pid}
8300
+ `)) {
8301
+ locked = true;
8302
+ break;
8303
+ }
8304
+ const age = ageMs2(path);
8305
+ if (age !== null && age > STALE_MS) remove(path);
8306
+ if (now() >= deadline) break;
8307
+ await wait(POLL_MS);
8308
+ }
8309
+ try {
8310
+ return await fn(locked);
8311
+ } finally {
8312
+ if (locked) remove(path);
8313
+ }
8314
+ }
8315
+
8316
+ // src/deps.ts
7310
8317
  var PREPARE_TIMEOUT_MS = 15 * 6e4;
7311
8318
  var MAX_PLAN_COMMANDS = 12;
7312
8319
  async function runCommand(cmd, worktree) {
7313
- const res = await execa(cmd.run, {
7314
- cwd: cmd.cwd ?? worktree,
7315
- shell: true,
7316
- // команда — строка целиком (у настройки/CI иначе никак), поэтому именно шелл
7317
- reject: false,
7318
- timeout: PREPARE_TIMEOUT_MS
7319
- });
7320
- return res.exitCode === 0;
8320
+ const exec = async () => {
8321
+ const res = await execa(cmd.run, {
8322
+ cwd: cmd.cwd ?? worktree,
8323
+ shell: true,
8324
+ // команда — строка целиком (у настройки/CI иначе никак), поэтому именно шелл
8325
+ reject: false,
8326
+ timeout: PREPARE_TIMEOUT_MS,
8327
+ ...cmd.env ? { env: cmd.env } : {}
8328
+ // execa мержит с окружением процесса, не заменяет его
8329
+ });
8330
+ return res.exitCode === 0;
8331
+ };
8332
+ return cmd.lock ? withMachineLock(cmd.lock, exec) : exec();
7321
8333
  }
7322
8334
  function safeReadText(p) {
7323
8335
  try {
@@ -7328,14 +8340,14 @@ function safeReadText(p) {
7328
8340
  }
7329
8341
  function safeListDirs(p) {
7330
8342
  try {
7331
- return readdirSync9(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
8343
+ return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
7332
8344
  } catch {
7333
8345
  return [];
7334
8346
  }
7335
8347
  }
7336
8348
  function safeListFiles(p) {
7337
8349
  try {
7338
- return readdirSync9(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
8350
+ return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
7339
8351
  } catch {
7340
8352
  return [];
7341
8353
  }
@@ -7361,21 +8373,21 @@ function overridePlan(override) {
7361
8373
  };
7362
8374
  }
7363
8375
  function detectPackageManager(worktree, io = {}) {
7364
- const exists = io.exists ?? existsSync9;
8376
+ const exists = io.exists ?? existsSync10;
7365
8377
  const readText = io.readText ?? safeReadText;
7366
- const hasPkgJson = exists(join13(worktree, "package.json"));
8378
+ const hasPkgJson = exists(join17(worktree, "package.json"));
7367
8379
  if (!hasPkgJson) {
7368
- if (exists(join13(worktree, "deno.json")) || exists(join13(worktree, "deno.jsonc")) || exists(join13(worktree, "deno.lock"))) return "deno";
8380
+ if (exists(join17(worktree, "deno.json")) || exists(join17(worktree, "deno.jsonc")) || exists(join17(worktree, "deno.lock"))) return "deno";
7369
8381
  return null;
7370
8382
  }
7371
- const pkg = readJson(readText, join13(worktree, "package.json"));
8383
+ const pkg = readJson(readText, join17(worktree, "package.json"));
7372
8384
  const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
7373
8385
  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";
8386
+ if (exists(join17(worktree, "bun.lock")) || exists(join17(worktree, "bun.lockb"))) return "bun";
8387
+ if (exists(join17(worktree, "pnpm-lock.yaml"))) return "pnpm";
8388
+ if (exists(join17(worktree, "yarn.lock"))) return "yarn";
8389
+ if (exists(join17(worktree, "package-lock.json"))) return "npm";
8390
+ if (exists(join17(worktree, "deno.lock"))) return "deno";
7379
8391
  return "npm";
7380
8392
  }
7381
8393
  function jsInstallCommand(pm) {
@@ -7428,7 +8440,7 @@ var ECOSYSTEM_MARKERS = [
7428
8440
  // конфигурация — дело сборки, не подготовки
7429
8441
  ];
7430
8442
  function detectPlan(worktree, io) {
7431
- const exists = io.exists ?? existsSync9;
8443
+ const exists = io.exists ?? existsSync10;
7432
8444
  const listFiles = io.listFiles ?? safeListFiles;
7433
8445
  const pm = detectPackageManager(worktree, io);
7434
8446
  if (pm) {
@@ -7436,20 +8448,21 @@ function detectPlan(worktree, io) {
7436
8448
  // node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
7437
8449
  // У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
7438
8450
  // заново, но он дешёвый: тянет из того же общего кэша.
7439
- { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join13(worktree, "node_modules"), source: "detect" }
8451
+ { label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join17(worktree, "node_modules"), source: "detect" }
7440
8452
  ];
7441
8453
  for (const target of workspaceBuildTargets(worktree, io)) {
7442
8454
  commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
7443
8455
  }
8456
+ commands.push(...migrateCommands(worktree, pm, io));
7444
8457
  return { source: "detect", ecosystem: `node/${pm}`, commands };
7445
8458
  }
7446
8459
  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));
8460
+ const hit = marker.files.some((f) => exists(join17(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
7448
8461
  if (!hit) continue;
7449
8462
  let ecosystem = marker.ecosystem;
7450
8463
  let command = marker.command;
7451
8464
  for (const refine of marker.refine ?? []) {
7452
- if (!exists(join13(worktree, refine.file))) continue;
8465
+ if (!exists(join17(worktree, refine.file))) continue;
7453
8466
  ecosystem = refine.ecosystem;
7454
8467
  command = refine.command;
7455
8468
  break;
@@ -7506,12 +8519,12 @@ var CI_INSTALL_PATTERNS = [
7506
8519
  function ciWorkflowPlan(worktree, io) {
7507
8520
  const readText = io.readText ?? safeReadText;
7508
8521
  const listFiles = io.listFiles ?? safeListFiles;
7509
- const dir = join13(worktree, ".github", "workflows");
8522
+ const dir = join17(worktree, ".github", "workflows");
7510
8523
  const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
7511
8524
  const seen = /* @__PURE__ */ new Set();
7512
8525
  const commands = [];
7513
8526
  for (const name of files.sort()) {
7514
- const yaml = readText(join13(dir, name));
8527
+ const yaml = readText(join17(dir, name));
7515
8528
  if (yaml === null) continue;
7516
8529
  for (const raw of workflowRunLines(yaml)) {
7517
8530
  const line = raw.replace(/\s+/g, " ").trim();
@@ -7526,7 +8539,7 @@ function ciWorkflowPlan(worktree, io) {
7526
8539
  return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
7527
8540
  }
7528
8541
  function workspaceGlobs(worktree, readText) {
7529
- const yaml = readText(join13(worktree, "pnpm-workspace.yaml"));
8542
+ const yaml = readText(join17(worktree, "pnpm-workspace.yaml"));
7530
8543
  if (yaml !== null) {
7531
8544
  const globs = [];
7532
8545
  let inPackages = false;
@@ -7546,7 +8559,7 @@ function workspaceGlobs(worktree, readText) {
7546
8559
  }
7547
8560
  if (globs.length) return globs;
7548
8561
  }
7549
- const pkg = readJson(readText, join13(worktree, "package.json"));
8562
+ const pkg = readJson(readText, join17(worktree, "package.json"));
7550
8563
  const ws = pkg?.workspaces;
7551
8564
  const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
7552
8565
  return list.filter((g) => typeof g === "string");
@@ -7557,14 +8570,14 @@ function workspaceDirs(worktree, globs, io) {
7557
8570
  if (glob.startsWith("!")) continue;
7558
8571
  const star = glob.indexOf("*");
7559
8572
  if (star === -1) {
7560
- const dir = join13(worktree, glob);
7561
- if (io.exists(join13(dir, "package.json"))) dirs.add(dir);
8573
+ const dir = join17(worktree, glob);
8574
+ if (io.exists(join17(dir, "package.json"))) dirs.add(dir);
7562
8575
  continue;
7563
8576
  }
7564
- const base3 = join13(worktree, glob.slice(0, star).replace(/\/$/, ""));
8577
+ const base3 = join17(worktree, glob.slice(0, star).replace(/\/$/, ""));
7565
8578
  for (const name of io.listDirs(base3)) {
7566
- const dir = join13(base3, name);
7567
- if (io.exists(join13(dir, "package.json"))) dirs.add(dir);
8579
+ const dir = join17(base3, name);
8580
+ if (io.exists(join17(dir, "package.json"))) dirs.add(dir);
7568
8581
  }
7569
8582
  }
7570
8583
  return [...dirs];
@@ -7585,18 +8598,18 @@ function entryPaths(pkg) {
7585
8598
  return out;
7586
8599
  }
7587
8600
  function workspaceBuildTargets(worktree, io = {}) {
7588
- const exists = io.exists ?? existsSync9;
8601
+ const exists = io.exists ?? existsSync10;
7589
8602
  const readText = io.readText ?? safeReadText;
7590
8603
  const listDirs = io.listDirs ?? safeListDirs;
7591
8604
  const globs = workspaceGlobs(worktree, readText);
7592
8605
  if (!globs.length) return [];
7593
8606
  const pkgs = [];
7594
8607
  for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
7595
- const pkg = readJson(readText, join13(dir, "package.json"));
8608
+ const pkg = readJson(readText, join17(dir, "package.json"));
7596
8609
  if (!pkg || typeof pkg.name !== "string") continue;
7597
8610
  const scripts = pkg.scripts ?? {};
7598
8611
  const entries = entryPaths(pkg);
7599
- const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join13(dir, e)));
8612
+ const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join17(dir, e)));
7600
8613
  const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
7601
8614
  pkgs.push({ name: pkg.name, dir, deps, needsBuild });
7602
8615
  }
@@ -7618,13 +8631,66 @@ function workspaceBuildTargets(worktree, io = {}) {
7618
8631
  for (const p of targets.values()) visit(p);
7619
8632
  return ordered;
7620
8633
  }
8634
+ var LOCAL_DB_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]", "host.docker.internal"]);
8635
+ function devDatabaseUrl(worktree, io = {}) {
8636
+ const readText = io.readText ?? safeReadText;
8637
+ const fromEnv = (io.env ?? process.env).DATABASE_URL;
8638
+ if (fromEnv && fromEnv.trim()) return fromEnv.trim();
8639
+ for (const file of [".env", ".env.example"]) {
8640
+ const raw = readText(join17(worktree, file));
8641
+ if (raw === null) continue;
8642
+ const m = /^\s*(?:export\s+)?DATABASE_URL\s*=\s*(.*)$/m.exec(raw);
8643
+ const value = (m?.[1] ?? "").trim().replace(/^['"]|['"]$/g, "");
8644
+ if (value) return value;
8645
+ }
8646
+ return null;
8647
+ }
8648
+ function isLocalDatabase(url) {
8649
+ try {
8650
+ return LOCAL_DB_HOSTS.has(new URL(url).hostname);
8651
+ } catch {
8652
+ return false;
8653
+ }
8654
+ }
8655
+ function prismaPackages(worktree, io) {
8656
+ const exists = io.exists ?? existsSync10;
8657
+ const readText = io.readText ?? safeReadText;
8658
+ const listDirs = io.listDirs ?? safeListDirs;
8659
+ const dirs = [worktree, ...workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs })];
8660
+ return dirs.filter((dir) => exists(join17(dir, "prisma", "schema.prisma")) && listDirs(join17(dir, "prisma", "migrations")).length > 0);
8661
+ }
8662
+ function jsExecBin(pm, command) {
8663
+ switch (pm) {
8664
+ case "npm":
8665
+ return `npm exec -- ${command}`;
8666
+ case "bun":
8667
+ return `bun x ${command}`;
8668
+ default:
8669
+ return `${pm} exec ${command}`;
8670
+ }
8671
+ }
8672
+ function migrateCommands(worktree, pm, io) {
8673
+ if (pm === "deno") return [];
8674
+ const url = devDatabaseUrl(worktree, io);
8675
+ if (!url || !isLocalDatabase(url)) return [];
8676
+ return prismaPackages(worktree, io).map((dir) => ({
8677
+ label: `\u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0411\u0414${dir === worktree ? "" : ` (${dir.slice(worktree.length + 1)})`}`,
8678
+ run: jsExecBin(pm, "prisma migrate deploy"),
8679
+ cwd: dir,
8680
+ env: { DATABASE_URL: url },
8681
+ // Ключ мьютекса — сама база: дроны разных репозиториев с разными базами друг друга не ждут.
8682
+ lock: `migrate:${url}`,
8683
+ optional: true,
8684
+ source: "detect"
8685
+ }));
8686
+ }
7621
8687
  function planPrepare(worktree, io = {}) {
7622
8688
  const plan = overridePlan(io.override) ?? detectPlan(worktree, io) ?? ciWorkflowPlan(worktree, io);
7623
8689
  if (!plan) return { source: "none", ecosystem: null, commands: [] };
7624
8690
  return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
7625
8691
  }
7626
8692
  async function ensureWorktreeDeps(worktree, io = {}) {
7627
- const exists = io.exists ?? existsSync9;
8693
+ const exists = io.exists ?? existsSync10;
7628
8694
  const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
7629
8695
  const approve = io.approve ?? (() => true);
7630
8696
  const plan = planPrepare(worktree, io);
@@ -7635,7 +8701,8 @@ async function ensureWorktreeDeps(worktree, io = {}) {
7635
8701
  ran: [],
7636
8702
  failed: [],
7637
8703
  skipped: [],
7638
- blocked: []
8704
+ blocked: [],
8705
+ warned: []
7639
8706
  };
7640
8707
  for (const cmd of plan.commands) {
7641
8708
  if (cmd.skipIfExists && exists(cmd.skipIfExists)) {
@@ -7650,6 +8717,8 @@ async function ensureWorktreeDeps(worktree, io = {}) {
7650
8717
  if (await exec(cmd)) {
7651
8718
  report.ran.push(cmd.label);
7652
8719
  report.outcome = "prepared";
8720
+ } else if (cmd.optional) {
8721
+ report.warned.push(cmd.label);
7653
8722
  } else {
7654
8723
  report.failed.push(cmd.label);
7655
8724
  report.outcome = "failed";
@@ -7666,13 +8735,17 @@ function depsEvents(report) {
7666
8735
  events.push(
7667
8736
  `\u26A0 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 worktree${where} \u0423\u041F\u0410\u041B\u0410 \u043D\u0430 \xAB${report.failed.join(", ")}\xBB \u2014 \u0434\u0440\u043E\u043D \u0441\u0442\u0430\u0440\u0442\u0443\u0435\u0442 \u0411\u0415\u0417 \u0433\u043E\u0442\u043E\u0432\u043E\u0433\u043E \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F: pre-commit \u0445\u0443\u043A \u043C\u043E\u0436\u0435\u0442 \u043C\u043E\u043B\u0447\u0430 \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C, typecheck/\u0442\u0435\u0441\u0442\u044B \u0431\u0443\u0434\u0443\u0442 \u043A\u0440\u0430\u0441\u043D\u044B\u043C\u0438 \u043D\u0435 \u043F\u043E \u0434\u0435\u043B\u0443 (TASWM-312)`
7668
8737
  );
8738
+ if (report.warned.length)
8739
+ events.push(
8740
+ `\u26A0 \u0448\u0430\u0433 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0438 \xAB${report.warned.join(", ")}\xBB \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B \u2014 worktree \u0433\u043E\u0442\u043E\u0432, \u043D\u043E \u0411\u0410\u0417\u0410 \u041C\u041E\u0413\u041B\u0410 \u041E\u0421\u0422\u0410\u0422\u042C\u0421\u042F \u041D\u0410 \u0421\u0422\u0410\u0420\u041E\u0419 \u0421\u0425\u0415\u041C\u0415 (\u0447\u0430\u0449\u0435 \u0432\u0441\u0435\u0433\u043E \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0430\u044F \u0431\u0430\u0437\u0430 \u043F\u0440\u043E\u0441\u0442\u043E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u0430) \u2014 TASWM-362`
8741
+ );
7669
8742
  if (report.blocked.length)
7670
8743
  events.push(`\u26A0 \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0438 \u043D\u0435 \u043F\u0443\u0449\u0435\u043D\u0430 \u0433\u0435\u0439\u0442\u043E\u043C \u0434\u043E\u0432\u0435\u0440\u0438\u044F: ${report.blocked.join(", ")} \u2014 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u043D\u0435 \u0433\u043E\u0442\u043E\u0432\u0438\u043B\u0438 (TASWM-312)`);
7671
8744
  if (report.outcome === "not-detected")
7672
8745
  events.push(
7673
8746
  `\u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u0430: \u043F\u0440\u043E\u0435\u043A\u0442 \u043D\u0435 \u043E\u043F\u043E\u0437\u043D\u0430\u043D (\u043D\u0435\u0442 \u0437\u043D\u0430\u043A\u043E\u043C\u044B\u0445 \u043C\u0430\u0440\u043A\u0435\u0440\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432, \u0432 .github/workflows \u043A\u043E\u043C\u0430\u043D\u0434 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438 \u043D\u0435 \u043D\u0430\u0448\u043B\u0438) \u2014 \u0434\u0435\u043C\u043E\u043D \u041D\u0418\u0427\u0415\u0413\u041E \u043D\u0435 \u0441\u0442\u0430\u0432\u0438\u043B; \u0437\u0430\u0434\u0430\u0442\u044C \u043A\u043E\u043C\u0430\u043D\u0434\u0443 \u043C\u043E\u0436\u043D\u043E \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (swarmConfig.prepare) \u2014 TASWM-312`
7674
8747
  );
7675
- if (report.outcome === "nothing-to-do" && !report.skipped.length) {
8748
+ if (report.outcome === "nothing-to-do" && !report.skipped.length && !report.warned.length) {
7676
8749
  if (report.source === "project-config") events.push(`\u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0430 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u043E\u0439 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (swarmConfig.prepare \u043F\u0443\u0441\u0442) \u2014 TASWM-312`);
7677
8750
  else if (report.ecosystem) events.push(`\u043F\u0440\u043E\u0435\u043A\u0442${where}: \u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043F\u0435\u0440\u0435\u0434 \u0441\u0442\u0430\u0440\u0442\u043E\u043C \u043D\u0435\u0447\u0435\u0433\u043E, \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F \u2014 TASWM-312`);
7678
8751
  }
@@ -7691,6 +8764,9 @@ function depsPromptNotice(report) {
7691
8764
  if (problems.length) {
7692
8765
  return `\u26A0 \u041E\u041A\u0420\u0423\u0416\u0415\u041D\u0418\u0415 WORKTREE \u041D\u0415 \u041F\u041E\u0414\u0413\u041E\u0422\u041E\u0412\u041B\u0415\u041D\u041E (TASWM-312): ${problems.join("; ")}. \u041F\u043E\u043A\u0430 \u044D\u0442\u043E \u0442\u0430\u043A, \u0413\u0415\u0419\u0422 \u041C\u041E\u0416\u0415\u0422 \u041D\u0415 \u0420\u0410\u0411\u041E\u0422\u0410\u0422\u042C: pre-commit \u0445\u0443\u043A \u043C\u043E\u043B\u0447\u0430 \u043D\u0435 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0441\u044F (\u043F\u0435\u0440\u0432\u044B\u0439 \u043A\u043E\u043C\u043C\u0438\u0442 \u0443\u0439\u0434\u0451\u0442 \u0431\u0435\u0437 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A), \u0430 typecheck/\u0442\u0435\u0441\u0442\u044B \u0431\u0443\u0434\u0443\u0442 \u043A\u0440\u0430\u0441\u043D\u044B\u043C\u0438 \u043D\u0435 \u043F\u043E \u0434\u0435\u043B\u0443. \u041F\u0435\u0440\u0432\u044B\u043C \u0434\u0435\u043B\u043E\u043C \u043F\u043E\u0447\u0438\u043D\u0438 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u0441\u0430\u043C (\u043F\u043E\u0441\u0442\u0430\u0432\u044C \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u0442\u0430\u043A, \u043A\u0430\u043A \u044D\u0442\u043E \u0434\u0435\u043B\u0430\u0435\u0442 README/CI \u043F\u0440\u043E\u0435\u043A\u0442\u0430) \u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0442\u043E\u043C \u0432\u0435\u0440\u044C \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430\u043C \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A; \u043D\u0435 \u0441\u0434\u0430\u0432\u0430\u0439 \u0440\u0430\u0431\u043E\u0442\u0443, \u043E\u0431\u044A\u044F\u0441\u043D\u044F\u044F \u043F\u0430\u0434\u0435\u043D\u0438\u044F \xAB\u0441\u043B\u043E\u043C\u0430\u043D\u043D\u044B\u043C \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435\u043C\xBB, \u043D\u0435 \u043F\u043E\u043F\u0440\u043E\u0431\u043E\u0432\u0430\u0432 \u044D\u0442\u043E.`;
7693
8766
  }
8767
+ if (report.warned.length) {
8768
+ return `\u2139 \u0411\u0410\u0417\u0410 \u041C\u041E\u0413\u041B\u0410 \u041E\u0421\u0422\u0410\u0422\u042C\u0421\u042F \u041D\u0410 \u0421\u0422\u0410\u0420\u041E\u0419 \u0421\u0425\u0415\u041C\u0415 (TASWM-362): \u0448\u0430\u0433 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0438 \xAB${report.warned.join(", ")}\xBB \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B \u2014 \u043E\u0431\u044B\u0447\u043D\u043E \u043F\u043E\u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0430\u044F \u0431\u0430\u0437\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u0430. \u0421\u0430\u043C worktree \u0433\u043E\u0442\u043E\u0432, typecheck \u0438 \u044E\u043D\u0438\u0442-\u0442\u0435\u0441\u0442\u044B \u0434\u043E\u0441\u0442\u043E\u0432\u0435\u0440\u043D\u044B. \u0415\u0441\u043B\u0438 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0448\u044C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0438 \u0443\u0432\u0438\u0434\u0438\u0448\u044C \u043E\u0448\u0438\u0431\u043A\u0443 \u043F\u0440\u043E \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0449\u0443\u044E \u043A\u043E\u043B\u043E\u043D\u043A\u0443/\u0442\u0430\u0431\u043B\u0438\u0446\u0443 \u2014 \u044D\u0442\u043E \u043E\u043D\u0430: \u043F\u043E\u0434\u043D\u0438\u043C\u0438 \u0431\u0430\u0437\u0443 \u0438 \u043D\u0430\u043A\u0430\u0442\u0438 \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0442\u0430\u043A, \u043A\u0430\u043A \u044D\u0442\u043E \u0434\u0435\u043B\u0430\u0435\u0442 dev-\u0441\u043A\u0440\u0438\u043F\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.`;
8769
+ }
7694
8770
  if (report.outcome === "not-detected") {
7695
8771
  return `\u2139 \u0414\u0415\u041C\u041E\u041D \u041D\u0415 \u0413\u041E\u0422\u041E\u0412\u0418\u041B \u041E\u041A\u0420\u0423\u0416\u0415\u041D\u0418\u0415 (TASWM-312): \u043F\u0440\u043E\u0435\u043A\u0442 \u043D\u0435 \u043E\u043F\u043E\u0437\u043D\u0430\u043D \u043F\u043E \u043C\u0430\u0440\u043A\u0435\u0440\u043D\u044B\u043C \u0444\u0430\u0439\u043B\u0430\u043C, \u043A\u043E\u043C\u0430\u043D\u0434 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438 \u0432 .github/workflows \u0442\u043E\u0436\u0435 \u043D\u0435\u0442. \u0417\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438, \u0435\u0441\u043B\u0438 \u043E\u043D\u0438 \u043D\u0443\u0436\u043D\u044B, \u043D\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u044B \u2014 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 README/CI \u0438 \u043F\u043E\u0441\u0442\u0430\u0432\u044C \u0438\u0445 \u0441\u0430\u043C \u041F\u0415\u0420\u0415\u0414 \u0442\u0435\u043C, \u043A\u0430\u043A \u0432\u0435\u0440\u0438\u0442\u044C typecheck/\u0442\u0435\u0441\u0442\u0430\u043C.`;
7696
8772
  }
@@ -7715,7 +8791,9 @@ var BaseBranchMissingError = class extends Error {
7715
8791
  branch;
7716
8792
  repo;
7717
8793
  };
7718
- var AreaBusyError = class extends Error {
8794
+ var SpawnDeferredError = class extends Error {
8795
+ };
8796
+ var AreaBusyError = class extends SpawnDeferredError {
7719
8797
  constructor(area) {
7720
8798
  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
8799
  this.area = area;
@@ -7723,6 +8801,14 @@ var AreaBusyError = class extends Error {
7723
8801
  }
7724
8802
  area;
7725
8803
  };
8804
+ var FileOverlapError = class extends SpawnDeferredError {
8805
+ constructor(message, blockedByRunId) {
8806
+ super(message);
8807
+ this.blockedByRunId = blockedByRunId;
8808
+ this.name = "FileOverlapError";
8809
+ }
8810
+ blockedByRunId;
8811
+ };
7726
8812
  function healTaskSlug(db, run, task) {
7727
8813
  const slug = task?.slug;
7728
8814
  if (!slug || slug === run.task_slug || !isTaskSlug(slug) || isTaskSlug(run.task_slug)) return run;
@@ -7732,7 +8818,9 @@ function healTaskSlug(db, run, task) {
7732
8818
  async function spawnDrone(deps, run) {
7733
8819
  const repo = normalizeRepo(run.repo || deps.repo || "");
7734
8820
  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)) {
8821
+ const conflict = deps.conflicts ? await deps.conflicts(run, repo) : { kind: "unknown" };
8822
+ if (conflict.kind === "wait") throw new FileOverlapError(conflict.message, conflict.blockedBy.id);
8823
+ if (conflict.kind === "unknown" && run.area && deps.db.runningAreas(repo).includes(run.area)) {
7736
8824
  throw new AreaBusyError(run.area);
7737
8825
  }
7738
8826
  const branch = run.task_slug;
@@ -7783,13 +8871,13 @@ function isGithubPrUrl(url) {
7783
8871
  return GH_PR_RE.test(url.trim());
7784
8872
  }
7785
8873
  function softStatus() {
7786
- return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [] };
8874
+ return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [], headBranch: null };
7787
8875
  }
7788
8876
  async function prStatus(prUrl, run = realRunner) {
7789
8877
  if (!isGithubPrUrl(prUrl)) return softStatus();
7790
8878
  let out;
7791
8879
  try {
7792
- out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files"]);
8880
+ out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files,headRefName"]);
7793
8881
  } catch {
7794
8882
  return softStatus();
7795
8883
  }
@@ -7810,9 +8898,20 @@ async function prStatus(prUrl, run = realRunner) {
7810
8898
  // не смержен, открыт и GitHub уверенно говорит CONFLICTING
7811
8899
  conflicting: !merged && state === "OPEN" && mergeable === "CONFLICTING",
7812
8900
  isDraft: j.isDraft === true,
7813
- files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : []
8901
+ files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : [],
8902
+ headBranch: typeof j.headRefName === "string" && j.headRefName ? j.headRefName : null
7814
8903
  };
7815
8904
  }
8905
+ async function prHeadBranch(prUrl, run = realRunner) {
8906
+ if (!isGithubPrUrl(prUrl)) return null;
8907
+ try {
8908
+ const out = await run(["pr", "view", prUrl, "--json", "headRefName"]);
8909
+ const j = JSON.parse(out);
8910
+ return typeof j.headRefName === "string" && j.headRefName ? j.headRefName : null;
8911
+ } catch {
8912
+ return null;
8913
+ }
8914
+ }
7816
8915
  async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
7817
8916
  if (!branch || !repoDir) return [];
7818
8917
  try {
@@ -8020,6 +9119,22 @@ function reviewTransitionPatch(run, prUrl) {
8020
9119
  }
8021
9120
  return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
8022
9121
  }
9122
+ function branchFromPrPatch(currentBranch, headBranch) {
9123
+ if (!headBranch || headBranch === currentBranch) return null;
9124
+ return {
9125
+ patch: { branch: headBranch },
9126
+ 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)`
9127
+ };
9128
+ }
9129
+ function prLiveness(st) {
9130
+ if (!st) return "unknown";
9131
+ if (st.merged) return "merged";
9132
+ const state = (st.state ?? "").toUpperCase();
9133
+ if (state === "MERGED") return "merged";
9134
+ if (state === "CLOSED") return "closed";
9135
+ if (state === "UNKNOWN") return "unknown";
9136
+ return "open";
9137
+ }
8023
9138
  function applyReviewTransition(db, run, prUrl) {
8024
9139
  const { patch, ...t } = reviewTransitionPatch(run, prUrl);
8025
9140
  db.updateRun(run.id, patch);
@@ -8028,14 +9143,14 @@ function applyReviewTransition(db, run, prUrl) {
8028
9143
 
8029
9144
  // src/droneServer.ts
8030
9145
  init_src();
8031
- function freezeInstruction(slug, reason = "\u0435\u0433\u043E \u043F\u0443\u043B\u044C\u0441 (heartbeat) \u0443\u0441\u0442\u0430\u0440\u0435\u043B") {
9146
+ function freezeInstruction(branch, reason = "\u0435\u0433\u043E \u043F\u0443\u043B\u044C\u0441 (heartbeat) \u0443\u0441\u0442\u0430\u0440\u0435\u043B") {
8032
9147
  return [
8033
9148
  `\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
9149
  "\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
9150
  "\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
9151
  "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
9152
  " \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.`,
9153
+ `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
9154
  "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
9155
  "\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
9156
  "\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 +9183,12 @@ async function buildDroneServer(store, opts = {}) {
8068
9183
  const slugForInstruction = (fallback) => opts.expectedTaskSlug ?? fallback ?? "<slug>";
8069
9184
  const freeze = async (withdraw = false) => {
8070
9185
  let slug;
9186
+ let branch;
8071
9187
  try {
8072
9188
  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
9189
  const run = await store.myRun();
8074
9190
  slug = run?.task_slug;
9191
+ branch = !opts.expectedTaskSlug || opts.expectedTaskSlug === run?.task_slug ? run?.branch : null;
8075
9192
  if (run && run.status !== "paused") {
8076
9193
  await store.updateRun(null, { status: "paused" });
8077
9194
  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 +9196,7 @@ async function buildDroneServer(store, opts = {}) {
8079
9196
  } catch (e) {
8080
9197
  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
9198
  }
8082
- return { content: [{ type: "text", text: freezeInstruction(slugForInstruction(slug), opts.deadReason) }] };
9199
+ return { content: [{ type: "text", text: freezeInstruction(branch || slugForInstruction(slug), opts.deadReason) }] };
8083
9200
  };
8084
9201
  const guard = (fn) => async (a) => {
8085
9202
  try {
@@ -8103,7 +9220,7 @@ async function buildDroneServer(store, opts = {}) {
8103
9220
  );
8104
9221
  server.tool(
8105
9222
  "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).",
9223
+ "\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
9224
  {},
8108
9225
  async () => {
8109
9226
  if (!opts.taskless) {
@@ -8316,9 +9433,12 @@ ${report}${shots.warning}`,
8316
9433
  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
9434
  }
8318
9435
  };
9436
+ const headBranch = opts.prHeadBranch ? await opts.prHeadBranch(pr_url).catch(() => null) : null;
9437
+ const branchFix = branchFromPrPatch(run?.branch, headBranch);
9438
+ if (branchFix) await store.addEvent(null, "step", branchFix.event);
8319
9439
  if (run) {
8320
9440
  const t = reviewTransitionPatch(run, pr_url);
8321
- await store.updateRun(null, t.patch);
9441
+ await store.updateRun(null, { ...t.patch, ...branchFix?.patch ?? {} });
8322
9442
  await store.addEvent(null, "pr_opened", pr_url);
8323
9443
  await postReport(reportSlug(run.task_slug), t.cycle ?? run.review_cycle ?? 0);
8324
9444
  if (t.isFixIteration) {
@@ -8331,7 +9451,7 @@ ${report}${shots.warning}`,
8331
9451
  }
8332
9452
  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
9453
  }
8334
- await store.updateRun(null, { status: "review", pr_url });
9454
+ await store.updateRun(null, { status: "review", pr_url, ...branchFix?.patch ?? {} });
8335
9455
  await store.addEvent(null, "pr_opened", pr_url);
8336
9456
  const fallbackSlug = reportSlug(void 0);
8337
9457
  if (fallbackSlug) await postReport(fallbackSlug, void 0);
@@ -8496,7 +9616,7 @@ async function runDroneServer(runId) {
8496
9616
  );
8497
9617
  const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
8498
9618
  const expectedKind = toRunKind(process.env.SWARM_RUN_KIND);
8499
- const common = { taskless, runKind: expectedKind, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
9619
+ const common = { taskless, runKind: expectedKind, prHeadBranch, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
8500
9620
  const channelUrl = process.env.SWARM_DRONE_CHANNEL_URL;
8501
9621
  const channelToken = process.env.SWARM_DRONE_CHANNEL_TOKEN;
8502
9622
  if (channelUrl && channelToken) {
@@ -8525,7 +9645,7 @@ async function runDroneServer(runId) {
8525
9645
 
8526
9646
  // src/droneBridge.ts
8527
9647
  init_src();
8528
- import { createHash as createHash2 } from "crypto";
9648
+ import { createHash as createHash3 } from "crypto";
8529
9649
 
8530
9650
  // src/anomalies.ts
8531
9651
  init_src();
@@ -8589,12 +9709,15 @@ var AnomalyTracker = class {
8589
9709
  for (const [k, v] of Object.entries(restored.since)) this.since.set(k, v);
8590
9710
  for (const [k, v] of Object.entries(restored.lastFiredAt)) this.lastFiredAt.set(k, v);
8591
9711
  for (const [k, v] of Object.entries(restored.fires)) this.fires.set(k, v);
9712
+ for (const k of restored.resolved ?? []) this.resolved.add(k);
8592
9713
  }
8593
9714
  }
8594
9715
  renotifyCooldownMs;
8595
9716
  since = /* @__PURE__ */ new Map();
8596
9717
  lastFiredAt = /* @__PURE__ */ new Map();
8597
9718
  fires = /* @__PURE__ */ new Map();
9719
+ /** TAS-485: очередь «уже звонили, а условие ушло» — к гашению в инбоксе. */
9720
+ resolved = /* @__PURE__ */ new Set();
8598
9721
  maxRepeats;
8599
9722
  io;
8600
9723
  /** Кандидаты, готовые к сигналу (отлежали minDurationMs, не в кулдауне) — НИЧЕГО не коммитит. */
@@ -8615,6 +9738,7 @@ var AnomalyTracker = class {
8615
9738
  for (const map of [this.since, this.lastFiredAt, this.fires]) {
8616
9739
  for (const k of [...map.keys()]) {
8617
9740
  if (!seen.has(k)) {
9741
+ if (map === this.lastFiredAt) this.resolved.add(k);
8618
9742
  map.delete(k);
8619
9743
  pruned = true;
8620
9744
  }
@@ -8627,8 +9751,25 @@ var AnomalyTracker = class {
8627
9751
  commit(key, now) {
8628
9752
  this.lastFiredAt.set(key, now);
8629
9753
  this.fires.set(key, (this.fires.get(key) ?? 0) + 1);
9754
+ this.resolved.delete(key);
8630
9755
  this.persist();
8631
9756
  }
9757
+ /**
9758
+ * TAS-485: ключи, чьё условие ушло ПОСЛЕ того, как о нём звонили, — их записи
9759
+ * в инбоксе пора погасить. Список только ЧИТАЕТСЯ: вычёркивает доставленное
9760
+ * вызывающий (`forgetResolved`) и только после успеха — та же дисциплина, что
9761
+ * у eligible/commit. Проглоти мы сбой доставки как успех, тревога о том, чего
9762
+ * нет, осталась бы висеть навсегда и молча.
9763
+ */
9764
+ pendingResolved() {
9765
+ return [...this.resolved];
9766
+ }
9767
+ /** Гашение по этим ключам доставлено — забываем их. */
9768
+ forgetResolved(keys) {
9769
+ let changed = false;
9770
+ for (const k of keys) changed = this.resolved.delete(k) || changed;
9771
+ if (changed) this.persist();
9772
+ }
8632
9773
  /**
8633
9774
  * Следующий сигнал по этому ключу — ПОСЛЕДНИЙ разрешённый в эпизоде: вызывающий меняет
8634
9775
  * формулировку (LAST_REMINDER_SUFFIX), чтобы затихание не выглядело как «рой забыл».
@@ -8644,7 +9785,8 @@ var AnomalyTracker = class {
8644
9785
  this.io.save({
8645
9786
  since: Object.fromEntries(this.since),
8646
9787
  lastFiredAt: Object.fromEntries(this.lastFiredAt),
8647
- fires: Object.fromEntries(this.fires)
9788
+ fires: Object.fromEntries(this.fires),
9789
+ resolved: [...this.resolved]
8648
9790
  });
8649
9791
  } catch (e) {
8650
9792
  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 +9840,9 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8698
9840
  const changedAt = sqliteMs(run.updated_at);
8699
9841
  return changedAt >= q.from && (q.to == null || changedAt <= q.to);
8700
9842
  };
9843
+ const closedBeforeStart = (run) => opts.daemonStartedAt != null && sqliteMs(run.updated_at) < opts.daemonStartedAt;
8701
9844
  const recentFailed = allRuns.filter(
8702
- (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r)
9845
+ (r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r)
8703
9846
  );
8704
9847
  if (recentFailed.length >= thresholds.failedSpikeCount) {
8705
9848
  candidates.push({
@@ -8736,6 +9879,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8736
9879
  if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
8737
9880
  if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
8738
9881
  if (killedByOperator(run)) continue;
9882
+ if (closedBeforeStart(run)) continue;
8739
9883
  const repeat = isRepeatDown(run);
8740
9884
  candidates.push({
8741
9885
  key: `run_stalled:${run.id}`,
@@ -8756,6 +9900,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
8756
9900
  if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
8757
9901
  if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
8758
9902
  if (killedByOperator(run)) continue;
9903
+ if (closedBeforeStart(run)) continue;
8759
9904
  const repeat = isRepeatDown(run);
8760
9905
  candidates.push({
8761
9906
  key: `run_failed:${run.id}`,
@@ -8895,7 +10040,7 @@ function questionKey(runId, text) {
8895
10040
  return `${runId} ${text.trim().replace(/\s+/g, " ")}`;
8896
10041
  }
8897
10042
  function noteKey(runId, text) {
8898
- return `note:${runId}:${createHash2("sha1").update(questionKey(runId, text)).digest("hex").slice(0, 16)}`;
10043
+ return `note:${runId}:${createHash3("sha1").update(questionKey(runId, text)).digest("hex").slice(0, 16)}`;
8899
10044
  }
8900
10045
  async function syncDroneChannel(opts) {
8901
10046
  const result = { imported: [], answered: [], notes: [] };
@@ -9175,22 +10320,22 @@ function makeExclusiveReader(opts) {
9175
10320
  }
9176
10321
 
9177
10322
  // src/envGate.ts
9178
- import { existsSync as existsSync12 } from "fs";
9179
- import { join as join20 } from "path";
10323
+ import { existsSync as existsSync13 } from "fs";
10324
+ import { join as join24 } from "path";
9180
10325
  import { createConnection } from "net";
9181
10326
  import { execa as execa2 } from "execa";
9182
10327
 
9183
10328
  // src/platform.ts
9184
- import { existsSync as existsSync11, readFileSync as readFileSync17 } from "fs";
9185
- import { delimiter, join as join19 } from "path";
10329
+ import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
10330
+ import { delimiter, join as join23 } from "path";
9186
10331
  function resolveCommandPath(name, env = process.env, platform = process.platform) {
9187
- if (name.includes("/") || name.includes("\\")) return existsSync11(name) ? name : null;
10332
+ if (name.includes("/") || name.includes("\\")) return existsSync12(name) ? name : null;
9188
10333
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
9189
10334
  const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
9190
10335
  for (const dir of dirs) {
9191
10336
  for (const ext of exts) {
9192
- const candidate = join19(dir, name + ext);
9193
- if (existsSync11(candidate)) return candidate;
10337
+ const candidate = join23(dir, name + ext);
10338
+ if (existsSync12(candidate)) return candidate;
9194
10339
  }
9195
10340
  }
9196
10341
  return null;
@@ -9198,6 +10343,31 @@ function resolveCommandPath(name, env = process.env, platform = process.platform
9198
10343
  function hasCommandInPath(name, env = process.env, platform = process.platform) {
9199
10344
  return resolveCommandPath(name, env, platform) !== null;
9200
10345
  }
10346
+ function detectGlobalPm(entry, io = {}) {
10347
+ const p = (entry ?? "").replace(/\\/g, "/");
10348
+ if (!p) return "unknown";
10349
+ if (p.endsWith(".ts") || /\/packages\/swarm-daemon\/(src|dist)\//.test(p)) return "sources";
10350
+ if (/\/\.bun\/install\/global\//.test(p) || /\/\.bun\/bin\//.test(p)) return "bun";
10351
+ if (/\/pnpm\//.test(p) || /\/\.pnpm-global\//.test(p) || /\/Library\/pnpm\//.test(p)) return "pnpm";
10352
+ if (/\/\.yarn\//.test(p) || /\/Yarn\/Data\/global\//.test(p) || /\/yarn\/global\//.test(p)) return "yarn";
10353
+ if (/\/node_modules\//.test(p)) return "npm";
10354
+ const env = io.env ?? process.env;
10355
+ const platform = io.platform ?? process.platform;
10356
+ const has = io.hasCommand ?? ((n) => hasCommandInPath(n, env, platform));
10357
+ return has("npm") ? "npm" : "unknown";
10358
+ }
10359
+ function globalInstallCommand(pm, spec) {
10360
+ switch (pm) {
10361
+ case "pnpm":
10362
+ return `pnpm add -g ${spec}`;
10363
+ case "bun":
10364
+ return `bun add -g ${spec}`;
10365
+ case "yarn":
10366
+ return `yarn global add ${spec}`;
10367
+ default:
10368
+ return `npm i -g ${spec}`;
10369
+ }
10370
+ }
9201
10371
 
9202
10372
  // src/envGate.ts
9203
10373
  var PROBE_TIMEOUT_MS = 2500;
@@ -9248,8 +10418,8 @@ var COMPOSE_MARKERS = [
9248
10418
  "compose.yaml"
9249
10419
  ];
9250
10420
  function detectRequirements(repo, io = {}) {
9251
- const exists = io.exists ?? existsSync12;
9252
- const marker = COMPOSE_MARKERS.find((f) => exists(join20(repo, f)));
10421
+ const exists = io.exists ?? existsSync13;
10422
+ const marker = COMPOSE_MARKERS.find((f) => exists(join24(repo, f)));
9253
10423
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
9254
10424
  }
9255
10425
  function resolveRequirements(repo, declared, io = {}) {
@@ -9385,8 +10555,8 @@ import { basename as basename3 } from "path";
9385
10555
  import { serve as serve2 } from "@hono/node-server";
9386
10556
 
9387
10557
  // src/diff.ts
9388
- import { statSync as statSync7 } from "fs";
9389
- import { join as join21 } from "path";
10558
+ import { statSync as statSync9 } from "fs";
10559
+ import { join as join25 } from "path";
9390
10560
  import { $ as $4 } from "execa";
9391
10561
  var MAX_FILES = 200;
9392
10562
  var MAX_LINES_PER_FILE = 1500;
@@ -9529,7 +10699,7 @@ async function resolveBase(wt, baseBranch) {
9529
10699
  }
9530
10700
  function mtime(path) {
9531
10701
  try {
9532
- return statSync7(path).mtimeMs;
10702
+ return statSync9(path).mtimeMs;
9533
10703
  } catch {
9534
10704
  return 0;
9535
10705
  }
@@ -9554,7 +10724,7 @@ async function collectRunDiff(worktree, opts = {}) {
9554
10724
  files.push(pf);
9555
10725
  }
9556
10726
  }
9557
- files.sort((a, b) => mtime(join21(worktree, b.path)) - mtime(join21(worktree, a.path)));
10727
+ files.sort((a, b) => mtime(join25(worktree, b.path)) - mtime(join25(worktree, a.path)));
9558
10728
  const truncatedFiles = files.length > MAX_FILES;
9559
10729
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
9560
10730
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -9569,11 +10739,25 @@ async function collectRunDiff(worktree, opts = {}) {
9569
10739
  ...truncatedFiles ? { truncated: true } : {}
9570
10740
  };
9571
10741
  }
10742
+ async function changedPaths(worktree, opts = {}) {
10743
+ if (!worktree) return [];
10744
+ try {
10745
+ const isWt = (await $4({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
10746
+ if (isWt !== "true") return [];
10747
+ const from = await resolveBase(worktree, opts.baseBranch) ?? "HEAD";
10748
+ const tracked = (await $4({ reject: false })`git -C ${worktree} diff --name-only --find-renames ${from} --`).stdout;
10749
+ const untracked = (await $4({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
10750
+ const all = [...tracked.split("\n"), ...untracked.split("\0")].map((s) => s.trim()).filter(Boolean);
10751
+ return [...new Set(all)].sort();
10752
+ } catch {
10753
+ return [];
10754
+ }
10755
+ }
9572
10756
 
9573
10757
  // package.json
9574
10758
  var package_default = {
9575
10759
  name: "@taskless-app/swarm",
9576
- version: "0.5.0",
10760
+ version: "0.6.1",
9577
10761
  type: "module",
9578
10762
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
9579
10763
  keywords: [
@@ -9622,7 +10806,8 @@ var package_default = {
9622
10806
  "@modelcontextprotocol/sdk": "^1.29.0",
9623
10807
  execa: "^10.0.0",
9624
10808
  zod: "3.25.76"
9625
- }
10809
+ },
10810
+ gitHead: "415ad928e18430894ae24ac2e4267073f5c6da14"
9626
10811
  };
9627
10812
 
9628
10813
  // src/version.ts
@@ -9690,6 +10875,10 @@ function roySnapshot(db, now = Date.now(), overmindLog = [], paused = false, dae
9690
10875
  stopReason: r.stop_reason,
9691
10876
  reason,
9692
10877
  updated: r.updated_at,
10878
+ startedAt: r.created_at,
10879
+ // TASWM-351: возраст прогона на карточке
10880
+ costUsd: r.cost_usd || 0,
10881
+ // TASWM-351: то же поле, что уходит в прод (swarm_run_upsert{costUsd})
9693
10882
  step,
9694
10883
  hasDecision: withQ.has(r.id),
9695
10884
  rescued: evs.some((e) => e.kind === "rescued"),
@@ -11197,47 +12386,10 @@ function closeWebServer(server) {
11197
12386
  });
11198
12387
  }
11199
12388
 
11200
- // src/daemonVersion.ts
11201
- init_git();
11202
- import { dirname as dirname4 } from "path";
11203
- import { $ as $5 } from "execa";
11204
- async function captureDaemonStart(entryFile, now = () => /* @__PURE__ */ new Date()) {
11205
- try {
11206
- const repo = await repoRoot(dirname4(entryFile));
11207
- const sha = (await $5`git -C ${repo} rev-parse HEAD`).stdout.trim();
11208
- if (!sha) return null;
11209
- return { repo, startedAt: now().toISOString(), startCommit: sha };
11210
- } catch {
11211
- return null;
11212
- }
11213
- }
11214
- var DAEMON_CODE_PATHS = ["packages/swarm-daemon", "packages/swarm-core"];
11215
- async function checkDaemonVersion(start, now = () => /* @__PURE__ */ new Date()) {
11216
- const base3 = { startedAt: start.startedAt, startCommit: start.startCommit, checkedAt: now().toISOString() };
11217
- try {
11218
- await $5({ reject: false })`git -C ${start.repo} fetch --quiet origin`;
11219
- const mainRef = await remoteMainRef(start.repo);
11220
- const mainCommit = (await $5`git -C ${start.repo} rev-parse ${mainRef}`).stdout.trim();
11221
- if (!mainCommit || mainCommit === start.startCommit) {
11222
- return { ...base3, mainCommit: mainCommit || null, stale: false, staleFiles: [] };
11223
- }
11224
- const ancestor = await $5({ reject: false })`git -C ${start.repo} merge-base --is-ancestor ${start.startCommit} ${mainCommit}`;
11225
- if (ancestor.exitCode !== 0) {
11226
- return { ...base3, mainCommit, stale: false, staleFiles: [] };
11227
- }
11228
- const diff = await $5({ reject: false })(
11229
- "git",
11230
- ["-C", start.repo, "diff", "--name-only", start.startCommit, mainCommit, "--", ...DAEMON_CODE_PATHS]
11231
- );
11232
- const staleFiles = diff.stdout.trim() ? diff.stdout.trim().split("\n") : [];
11233
- return { ...base3, mainCommit, stale: staleFiles.length > 0, staleFiles: staleFiles.slice(0, 20) };
11234
- } catch {
11235
- return { ...base3, mainCommit: null, stale: false, staleFiles: [] };
11236
- }
11237
- }
11238
-
11239
12389
  // src/release.ts
11240
- var SWARM_PACKAGE = "@taskless-app/swarm";
12390
+ init_src();
12391
+ init_claudeBin();
12392
+ var SWARM_PACKAGE = SWARM_PACKAGE_NAME;
11241
12393
  var LATEST_TIMEOUT_MS = 4e3;
11242
12394
  async function fetchLatestVersion(opts = {}) {
11243
12395
  const fetchFn = opts.fetchFn ?? fetch;
@@ -11268,12 +12420,30 @@ function outdatedLine(current, latest, updateCommand = "swarm update") {
11268
12420
  return `\u26A0 \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u043E\u044F \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0430: \u0443 \u0442\u0435\u0431\u044F v${current}, \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest} \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: ${updateCommand}`;
11269
12421
  }
11270
12422
 
12423
+ // src/daemonVersion.ts
12424
+ function captureDaemonStart(now = () => /* @__PURE__ */ new Date()) {
12425
+ return { startedAt: now().toISOString(), version: PACKAGE_VERSION };
12426
+ }
12427
+ var DAEMON_CODE_PATHS = ["packages/swarm-daemon", "packages/swarm-core"];
12428
+ async function checkDaemonVersion(start, opts = {}) {
12429
+ const now = opts.now ?? (() => /* @__PURE__ */ new Date());
12430
+ const fetchLatest = opts.latest ?? (() => fetchLatestVersion());
12431
+ const latestVersion = await fetchLatest().catch(() => null);
12432
+ return {
12433
+ startedAt: start.startedAt,
12434
+ version: start.version,
12435
+ latestVersion,
12436
+ stale: isOutdated(start.version, latestVersion),
12437
+ checkedAt: now().toISOString()
12438
+ };
12439
+ }
12440
+
11271
12441
  // src/hostCommands.ts
11272
12442
  init_src();
11273
12443
  import { readFileSync as readFileSync18, renameSync as renameSync8, writeFileSync as writeFileSync13 } from "fs";
11274
- import { join as join22 } from "path";
12444
+ import { join as join26 } from "path";
11275
12445
  function hostCommandStatePath(home) {
11276
- return join22(home, "host-command.json");
12446
+ return join26(home, "host-command.json");
11277
12447
  }
11278
12448
  function readState(path) {
11279
12449
  try {
@@ -11383,6 +12553,28 @@ async function executeOne(cmd, opts) {
11383
12553
  await ack(cmd.id, "done", { result: `\u043F\u0440\u043E\u0433\u043E\u043D ${slug} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D` });
11384
12554
  return false;
11385
12555
  }
12556
+ // TASWM-351: «перезапустить с того же места». Отказ побудки (дрон уже работает,
12557
+ // продолжать нечем) уезжает как `rejected` с ЕЁ формулировкой — повторять такую
12558
+ // команду бессмысленно, и это ровно та разница между rejected и failed, ради
12559
+ // которой они разделены (см. HOST_COMMAND_STATUSES).
12560
+ case "wake_run": {
12561
+ const slug = cmd.runSlug?.trim();
12562
+ if (!slug) {
12563
+ 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" });
12564
+ return false;
12565
+ }
12566
+ const woken = await effects.wakeRun(slug);
12567
+ if (!woken) {
12568
+ 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` });
12569
+ return false;
12570
+ }
12571
+ if (!woken.ok) {
12572
+ await ack(cmd.id, "rejected", { result: woken.why });
12573
+ return false;
12574
+ }
12575
+ await ack(cmd.id, "done", { result: woken.why });
12576
+ return false;
12577
+ }
11386
12578
  case "restart":
11387
12579
  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
12580
  case "update": {
@@ -11394,6 +12586,11 @@ async function executeOne(cmd, opts) {
11394
12586
  effects.narrate(`\u26A0\uFE0F \u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0448\u0430\u0433\u0435 \xAB${UPDATE_STEP_LABELS[res.step]}\xBB: ${res.message}`);
11395
12587
  return false;
11396
12588
  }
12589
+ if (res.restart === false) {
12590
+ await ack(cmd.id, "done", { step: res.step, result: res.message });
12591
+ effects.narrate(`\u2705 ${res.message}`);
12592
+ return false;
12593
+ }
11397
12594
  return await doRestart(cmd, opts, res.message);
11398
12595
  }
11399
12596
  }
@@ -11420,12 +12617,12 @@ async function doRestart(cmd, opts, note) {
11420
12617
 
11421
12618
  // src/selfUpdate.ts
11422
12619
  init_git();
11423
- import { existsSync as existsSync13 } from "fs";
11424
- import { join as join23 } from "path";
11425
- import { $ as $6 } from "execa";
12620
+ import { existsSync as existsSync14 } from "fs";
12621
+ import { join as join27 } from "path";
12622
+ import { $ as $5 } from "execa";
11426
12623
  var DAEMON_PACKAGE = "@taskless-app/swarm";
11427
12624
  var defaultRun = async (cmd, args, cwd) => {
11428
- const res = await $6({ reject: false, cwd })(cmd, args);
12625
+ const res = await $5({ reject: false, cwd })(cmd, args);
11429
12626
  return { exitCode: res.exitCode ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
11430
12627
  };
11431
12628
  function tail(text, max = 400) {
@@ -11434,7 +12631,7 @@ function tail(text, max = 400) {
11434
12631
  }
11435
12632
  async function runSelfUpdate(io) {
11436
12633
  const run = io.run ?? defaultRun;
11437
- const exists = io.exists ?? existsSync13;
12634
+ const exists = io.exists ?? existsSync14;
11438
12635
  const repo = io.repo;
11439
12636
  await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
11440
12637
  const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
@@ -11463,7 +12660,7 @@ async function runSelfUpdate(io) {
11463
12660
  };
11464
12661
  }
11465
12662
  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"))) {
12663
+ if (!exists(join27(repo, "pnpm-lock.yaml"))) {
11467
12664
  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
12665
  }
11469
12666
  const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
@@ -11482,6 +12679,31 @@ async function runSelfUpdate(io) {
11482
12679
  function defaultSelfUpdateIO(repo, onStep) {
11483
12680
  return { repo, run: defaultRun, mainRef: remoteMainRef, onStep };
11484
12681
  }
12682
+ async function runPackageSelfUpdate(io) {
12683
+ await io.onStep("fetch", "\u0441\u043C\u043E\u0442\u0440\u044E, \u0447\u0442\u043E \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043E");
12684
+ const latest = await io.latest();
12685
+ if (!latest) {
12686
+ return { ok: false, step: "fetch", message: "\u0434\u043E registry.npmjs.org \u043D\u0435 \u0434\u043E\u0437\u0432\u043E\u043D\u0438\u043B\u0438\u0441\u044C \u2014 \u043A\u0430\u043A\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430, \u0443\u0437\u043D\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u043C" };
12687
+ }
12688
+ if (!isOutdated(io.currentVersion, latest)) {
12689
+ return { ok: true, restart: false, step: "install", message: `\u0443\u0436\u0435 \u0441\u0432\u0435\u0436\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F: v${io.currentVersion} (\u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest})` };
12690
+ }
12691
+ await io.onStep("install", `\u0441\u0442\u0430\u0432\u043B\u044E v${latest}`);
12692
+ const command = io.installCommand(`${SWARM_PACKAGE}@${latest}`);
12693
+ const code = await io.exec(command);
12694
+ if (code !== 0) {
12695
+ return { ok: false, step: "install", message: `\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0448\u043B\u0430 (\u043A\u043E\u0434 ${code}): ${command} \u2014 \u0432\u044B\u043F\u043E\u043B\u043D\u0438 \u044D\u0442\u0443 \u0441\u0442\u0440\u043E\u043A\u0443 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438, \u043D\u0430 \u0447\u0451\u043C \u0441\u043F\u043E\u0442\u044B\u043A\u0430\u0435\u0442\u0441\u044F` };
12696
+ }
12697
+ return { ok: true, step: "install", message: `\u043E\u0431\u043D\u043E\u0432\u0438\u043B\u0441\u044F \u0434\u043E v${latest} (\u0431\u044B\u043B\u043E v${io.currentVersion})` };
12698
+ }
12699
+
12700
+ // src/cliIo.ts
12701
+ import { createInterface } from "readline/promises";
12702
+ import { execa as execa3 } from "execa";
12703
+ async function execLoud(command) {
12704
+ const res = await execa3(command, { shell: true, reject: false, stdio: "inherit" });
12705
+ return res.exitCode ?? 1;
12706
+ }
11485
12707
 
11486
12708
  // src/narration.ts
11487
12709
  var RING_LIMIT = 20;
@@ -11497,11 +12719,12 @@ function makeOvermindNarrator() {
11497
12719
  }
11498
12720
 
11499
12721
  // src/probe.ts
12722
+ init_executors();
11500
12723
  import { spawn as spawn2 } from "child_process";
11501
- var LIMIT_RE = /(weekly limit|usage limit|hit your .*limit)/i;
11502
12724
  var RESET_RE = /resets[^\n]*/i;
11503
- function anthropicHealthy(opts = {}) {
11504
- const bin = opts.claudeBin ?? resolveClaudeBin();
12725
+ function executorHealthy(opts = {}) {
12726
+ const executor = opts.executor ?? resolveExecutor();
12727
+ const bin = opts.bin ?? executor.resolveBin();
11505
12728
  const timeoutMs = opts.timeoutMs ?? 2e4;
11506
12729
  return new Promise((resolve4) => {
11507
12730
  let done = false;
@@ -11516,7 +12739,7 @@ function anthropicHealthy(opts = {}) {
11516
12739
  }
11517
12740
  resolve4(r);
11518
12741
  };
11519
- const child = spawn2(bin, [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"], { stdio: ["ignore", "pipe", "pipe"] });
12742
+ const child = spawn2(bin, [...executor.probe.args], { stdio: ["ignore", "pipe", "pipe"] });
11520
12743
  child.stdout?.on("data", (d) => {
11521
12744
  out += d.toString();
11522
12745
  });
@@ -11527,7 +12750,9 @@ function anthropicHealthy(opts = {}) {
11527
12750
  child.on("error", () => finish({ ok: false, reason: "exec", resetHint: null }));
11528
12751
  child.on("exit", (code) => {
11529
12752
  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 });
12753
+ if (executor.probe.limitPattern.test(out)) {
12754
+ return finish({ ok: false, reason: "limit", resetHint: out.match(RESET_RE)?.[0]?.trim() ?? null });
12755
+ }
11531
12756
  finish({ ok: false, reason: "provider", resetHint: null });
11532
12757
  });
11533
12758
  });
@@ -11553,18 +12778,21 @@ async function fetchExternalStorm(url, opts = {}) {
11553
12778
  }
11554
12779
 
11555
12780
  // 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";
12781
+ import { existsSync as existsSync15, readFileSync as readFileSync19, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
12782
+ import { join as join28 } from "path";
11558
12783
  var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
11559
- var EMPTY = { since: {}, lastFiredAt: {}, fires: {}, stopQuiet: null };
12784
+ var MAX_RESOLVED = 200;
12785
+ function emptyState() {
12786
+ return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
12787
+ }
11560
12788
  function anomalyStatePath(home) {
11561
- return join24(home, "anomaly-state.json");
12789
+ return join28(home, "anomaly-state.json");
11562
12790
  }
11563
12791
  function loadAnomalyState(home, now = Date.now(), io = {}) {
11564
- const exists = io.exists ?? existsSync14;
12792
+ const exists = io.exists ?? existsSync15;
11565
12793
  const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
11566
12794
  const path = anomalyStatePath(home);
11567
- if (!exists(path)) return { ...EMPTY };
12795
+ if (!exists(path)) return emptyState();
11568
12796
  try {
11569
12797
  const raw = JSON.parse(readFile2(path));
11570
12798
  const since = numbers(raw.since);
@@ -11579,10 +12807,14 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
11579
12807
  since: pick(since, fresh),
11580
12808
  lastFiredAt: pick(lastFiredAt, fresh),
11581
12809
  fires: pick(fires, fresh),
12810
+ // TAS-485: очередь гашения НЕ фильтруем тем же TTL, что ключи. Она про
12811
+ // уведомление, которое УЖЕ висит у человека в инбоксе: пролежав неделю
12812
+ // недоставленным, гашение не становится лишним — оно становится нужнее.
12813
+ resolved: strings(raw.resolved).slice(0, MAX_RESOLVED),
11582
12814
  stopQuiet: parseQuiet(raw.stopQuiet)
11583
12815
  };
11584
12816
  } catch {
11585
- return { ...EMPTY };
12817
+ return emptyState();
11586
12818
  }
11587
12819
  }
11588
12820
  function saveAnomalyState(home, state, io = {}) {
@@ -11605,9 +12837,14 @@ var AnomalyStateStore = class {
11605
12837
  /** Инъекция в AnomalyTracker (см. AnomalyTrackerOpts.io). */
11606
12838
  trackerIo() {
11607
12839
  return {
11608
- load: () => ({ since: this.state.since, lastFiredAt: this.state.lastFiredAt, fires: this.state.fires }),
12840
+ load: () => ({
12841
+ since: this.state.since,
12842
+ lastFiredAt: this.state.lastFiredAt,
12843
+ fires: this.state.fires,
12844
+ resolved: this.state.resolved
12845
+ }),
11609
12846
  save: (snapshot) => {
11610
- this.state = { ...snapshot, stopQuiet: this.state.stopQuiet };
12847
+ this.state = { ...snapshot, resolved: (snapshot.resolved ?? []).slice(0, MAX_RESOLVED), stopQuiet: this.state.stopQuiet };
11611
12848
  this.flush();
11612
12849
  }
11613
12850
  };
@@ -11649,6 +12886,10 @@ function numbers(raw) {
11649
12886
  }
11650
12887
  return out;
11651
12888
  }
12889
+ function strings(raw) {
12890
+ if (!Array.isArray(raw)) return [];
12891
+ return raw.filter((v) => typeof v === "string" && v.length > 0);
12892
+ }
11652
12893
  function pick(src, keys) {
11653
12894
  const out = {};
11654
12895
  for (const k of keys) if (src[k] != null) out[k] = src[k];
@@ -11681,7 +12922,7 @@ function isRepeatEvent(prev, cur) {
11681
12922
  }
11682
12923
  function swarmLastActivityMs(db) {
11683
12924
  let latest = null;
11684
- const bump = (iso) => {
12925
+ const bump2 = (iso) => {
11685
12926
  const ms = sqliteMs(iso);
11686
12927
  if (latest == null || ms > latest) latest = ms;
11687
12928
  };
@@ -11689,12 +12930,12 @@ function swarmLastActivityMs(db) {
11689
12930
  if (isTerminalStatus(run.status)) continue;
11690
12931
  const evs = db.events(run.id);
11691
12932
  if (!evs.length) {
11692
- bump(run.updated_at);
12933
+ bump2(run.updated_at);
11693
12934
  continue;
11694
12935
  }
11695
12936
  let i = evs.length - 1;
11696
12937
  while (i > 0 && isRepeatEvent(evs[i - 1], evs[i])) i--;
11697
- bump(evs[i].at);
12938
+ bump2(evs[i].at);
11698
12939
  }
11699
12940
  return latest;
11700
12941
  }
@@ -11820,7 +13061,193 @@ function decideWatchdogAction(db, now, state, thresholds, droneCap, wt, exclusiv
11820
13061
  // src/overmind.ts
11821
13062
  init_storm();
11822
13063
  init_src();
13064
+
13065
+ // src/zones.ts
13066
+ init_src();
13067
+
13068
+ // src/gitHistory.ts
13069
+ import { $ as $6 } from "execa";
13070
+ var REC = "";
13071
+ var FIELD = "";
13072
+ var LOG_FORMAT = `${REC}%H${FIELD}%s`;
13073
+ var SLUG_IN_SUBJECT = /\b([A-Z][A-Z0-9]{0,7}-\d+)\b/;
13074
+ function slugFromSubject(subject) {
13075
+ return SLUG_IN_SUBJECT.exec(subject)?.[1] ?? null;
13076
+ }
13077
+ function unquotePath(raw) {
13078
+ const p = raw.trim();
13079
+ return p.length >= 2 && p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
13080
+ }
13081
+ function parseGitLog(out) {
13082
+ const commits = [];
13083
+ for (const chunk of out.split(REC)) {
13084
+ if (!chunk.trim()) continue;
13085
+ const [head, ...rest] = chunk.split("\n");
13086
+ const [sha, subject] = (head ?? "").split(FIELD);
13087
+ if (!sha) continue;
13088
+ const files = rest.map(unquotePath).filter(Boolean);
13089
+ commits.push({ sha, subject: subject ?? "", files });
13090
+ }
13091
+ return commits;
13092
+ }
13093
+ var NOISE_RE = /^(journal\/|changelog\/|docs\/|design\/|scratch\/|\.claude\/|.*\.lock$|pnpm-lock\.yaml$)/;
13094
+ function isNoisePath(path) {
13095
+ return NOISE_RE.test(path);
13096
+ }
13097
+ var GROUP_DEFAULTS = { maxFilesPerCommit: 120, ignore: isNoisePath };
13098
+ function groupTaskHistory(commits, opts = {}) {
13099
+ const o = { ...GROUP_DEFAULTS, ...opts };
13100
+ const bySlug = /* @__PURE__ */ new Map();
13101
+ for (const c of commits) {
13102
+ const slug = slugFromSubject(c.subject);
13103
+ if (!slug) continue;
13104
+ if (c.files.length > o.maxFilesPerCommit) continue;
13105
+ const files = c.files.filter((f) => !o.ignore(f));
13106
+ if (!files.length) continue;
13107
+ let acc = bySlug.get(slug);
13108
+ if (!acc) bySlug.set(slug, acc = { files: /* @__PURE__ */ new Set(), texts: [] });
13109
+ for (const f of files) acc.files.add(f);
13110
+ acc.texts.push(c.subject);
13111
+ }
13112
+ return [...bySlug.entries()].map(([slug, acc]) => ({
13113
+ slug,
13114
+ files: [...acc.files],
13115
+ text: acc.texts.join(" \xB7 ")
13116
+ }));
13117
+ }
13118
+ async function readCommitLog(repo, opts = {}) {
13119
+ const max = opts.maxCommits ?? 1500;
13120
+ try {
13121
+ const { stdout } = await $6({ cwd: repo, reject: false })`git log --no-merges --name-only --pretty=format:${LOG_FORMAT} -n ${String(max)} ${opts.ref ?? "HEAD"}`;
13122
+ return parseGitLog(stdout ?? "");
13123
+ } catch {
13124
+ return [];
13125
+ }
13126
+ }
13127
+ async function readTaskHistory(repo, opts = {}) {
13128
+ return groupTaskHistory(await readCommitLog(repo, opts), opts);
13129
+ }
13130
+ async function headSha(repo) {
13131
+ try {
13132
+ const { stdout, exitCode } = await $6({ cwd: repo, reject: false })`git rev-parse HEAD`;
13133
+ return exitCode === 0 ? stdout.trim() : null;
13134
+ } catch {
13135
+ return null;
13136
+ }
13137
+ }
13138
+
13139
+ // src/zones.ts
13140
+ function runText(run) {
13141
+ return [run.task_title ?? "", run.task_desc ?? ""].join(" ").trim();
13142
+ }
13143
+ function predictForRun(model, run) {
13144
+ return predictFiles(model, { zone: run.area, text: runText(run) });
13145
+ }
13146
+ function short(files, n = 3) {
13147
+ return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
13148
+ }
13149
+ var SPINE_PREDICTION_WEIGHT = 0.7;
13150
+ function decideConflict({ model, candidate, active }) {
13151
+ if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
13152
+ if (!hasZoneModel(model)) return { kind: "unknown" };
13153
+ const predicted = predictForRun(model, candidate);
13154
+ if (!predicted.length) return { kind: "unknown" };
13155
+ if (!active.length) return { kind: "clear" };
13156
+ const spine = new Set(model.spine);
13157
+ const candidateSpine = predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
13158
+ if (candidateSpine.length) {
13159
+ const blocker = active[0];
13160
+ return {
13161
+ kind: "wait",
13162
+ blockedBy: blocker.run,
13163
+ 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})`
13164
+ };
13165
+ }
13166
+ for (const w of active) {
13167
+ if (!w.actual) continue;
13168
+ const theirSpine = w.files.filter((f) => spine.has(f));
13169
+ if (theirSpine.length && zoneForFiles(model, w.files)?.zone === SPINE_ZONE) {
13170
+ return {
13171
+ kind: "wait",
13172
+ blockedBy: w.run,
13173
+ 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`
13174
+ };
13175
+ }
13176
+ }
13177
+ for (const w of active) {
13178
+ const risk = overlapRisk(predicted, w.files);
13179
+ if (!risk.overlap) continue;
13180
+ 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)})`;
13181
+ return {
13182
+ kind: "wait",
13183
+ blockedBy: w.run,
13184
+ 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)")
13185
+ };
13186
+ }
13187
+ return { kind: "clear" };
13188
+ }
13189
+ function suggestAreaForRun(model, run) {
13190
+ if (!hasZoneModel(model)) return null;
13191
+ const text = runText(run);
13192
+ if (!text) return null;
13193
+ const top = suggestZonesFromText(model, text)[0];
13194
+ return top && top.confidence >= 0.3 ? { zone: top.zone, confidence: top.confidence, why: top.why } : null;
13195
+ }
13196
+ var DEFAULT_TTL_MS = 10 * 6e4;
13197
+ var ZoneModels = class {
13198
+ constructor(opts = {}) {
13199
+ this.opts = opts;
13200
+ }
13201
+ opts;
13202
+ cache = /* @__PURE__ */ new Map();
13203
+ /** Модель репо. Истории нет / это не git — EMPTY_ZONE_MODEL (раздача на старом локе). */
13204
+ async forRepo(repo, now = Date.now()) {
13205
+ if (!repo) return EMPTY_ZONE_MODEL;
13206
+ const head = await headSha(repo);
13207
+ const hit = this.cache.get(repo);
13208
+ if (hit && hit.head === head && now - hit.builtAt < (this.opts.ttlMs ?? DEFAULT_TTL_MS)) return hit.model;
13209
+ const model = await this.build(repo);
13210
+ this.cache.set(repo, { head, builtAt: now, model });
13211
+ return model;
13212
+ }
13213
+ /** Сбросить кэш (тесты, `swarm zones --refresh`). */
13214
+ clear() {
13215
+ this.cache.clear();
13216
+ }
13217
+ async build(repo) {
13218
+ const history = await readTaskHistory(repo, { ...this.opts.maxCommits ? { maxCommits: this.opts.maxCommits } : {} });
13219
+ return buildZoneModel(withManualAreas(history, this.opts.manualAreas?.() ?? /* @__PURE__ */ new Map()));
13220
+ }
13221
+ };
13222
+ var ActiveFilesCache = class {
13223
+ constructor(read, ttlMs = 3e4) {
13224
+ this.read = read;
13225
+ this.ttlMs = ttlMs;
13226
+ }
13227
+ read;
13228
+ ttlMs;
13229
+ cache = /* @__PURE__ */ new Map();
13230
+ async filesOf(run, now = Date.now()) {
13231
+ const hit = this.cache.get(run.id);
13232
+ if (hit && now - hit.at < this.ttlMs) return hit.files;
13233
+ const files = await this.read(run);
13234
+ this.cache.set(run.id, { at: now, files });
13235
+ return files;
13236
+ }
13237
+ /** Забыть всех, кого нет среди живых (дрон закончил — его файлы больше не заняты). */
13238
+ prune(liveIds) {
13239
+ const live = new Set(liveIds);
13240
+ for (const id of [...this.cache.keys()]) if (!live.has(id)) this.cache.delete(id);
13241
+ }
13242
+ };
13243
+ function withManualAreas(history, areas) {
13244
+ return history.map((e) => ({ ...e, area: normalizeZoneName(areas.get(e.slug) ?? null) }));
13245
+ }
13246
+
13247
+ // src/overmind.ts
13248
+ var ANOMALY_RESOLVE_BATCH = 50;
11823
13249
  var THIS_FILE = fileURLToPath2(import.meta.url);
13250
+ var executorHasSession = (p) => EXECUTOR.session.exists(p);
11824
13251
  var OVERMIND_ENTRY = THIS_FILE;
11825
13252
  var STALL_MS = Number(process.env.SWARM_STALL_MS) || 10 * 60 * 1e3;
11826
13253
  var STALL_FLOOR_MS = Number(process.env.SWARM_STALL_FLOOR_MS) || 30 * 60 * 1e3;
@@ -11834,6 +13261,10 @@ var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
11834
13261
  var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
11835
13262
  var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
11836
13263
  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";
13264
+ var REVIEW_UNRECOVERABLE_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u044C";
13265
+ function reviewUnrecoverableMark(branch) {
13266
+ return `${REVIEW_UNRECOVERABLE_MARK} (\u0432\u0435\u0442\u043A\u0430 ${branch})`;
13267
+ }
11837
13268
  var REVIEW_CYCLE_LIMIT = Number(process.env.SWARM_REVIEW_CYCLE_LIMIT) || 3;
11838
13269
  var REVIEW_CYCLE_ESCALATION_MARK = "\u0440\u0435-\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0446\u0438\u043A\u043B\u0438\u043B\u043E\u0441\u044C";
11839
13270
  var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
@@ -11860,6 +13291,7 @@ var CHANGES_WAKE_ESCALATION_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\
11860
13291
  var NEEDS_HUMAN_MARKS = [
11861
13292
  RESOLVE_ESCALATION_MARK,
11862
13293
  REVIEW_ESCALATION_MARK,
13294
+ REVIEW_UNRECOVERABLE_MARK,
11863
13295
  REVIEW_CYCLE_ESCALATION_MARK,
11864
13296
  CI_WAKE_ESCALATION_MARK,
11865
13297
  CHANGES_WAKE_ESCALATION_MARK
@@ -11980,20 +13412,20 @@ function countActiveDrones(db) {
11980
13412
  function countActiveReviewers(db) {
11981
13413
  return db.listRuns().filter((r) => r.kind === "reviewer" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
11982
13414
  }
11983
- async function closeReviewersOnMerge(deps, reviewRunId) {
11984
- const stale = deps.db.activeReviewersFor(reviewRunId);
11985
- for (const rv of stale) {
11986
- if (rv.pid != null && deps.kill && deps.isAlive(rv.pid)) {
11987
- try {
11988
- deps.kill(rv.pid);
11989
- } catch {
11990
- }
13415
+ async function closeReviewerRun(deps, rv, reason) {
13416
+ if (rv.pid != null && deps.kill && deps.isAlive(rv.pid)) {
13417
+ try {
13418
+ deps.kill(rv.pid);
13419
+ } catch {
11991
13420
  }
11992
- if (rv.worktree_path) await deps.removeWorktree(rv.repo, rv.worktree_path).catch(() => {
11993
- });
11994
- deps.db.updateRun(rv.id, { status: "done" });
11995
- deps.db.addEvent(rv.id, "done", "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E \u2014 \u0440\u0435\u0432\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E");
11996
13421
  }
13422
+ if (rv.worktree_path) await deps.removeWorktree(rv.repo, rv.worktree_path).catch(() => {
13423
+ });
13424
+ deps.db.updateRun(rv.id, { status: "done", stop_reason: null });
13425
+ deps.db.addEvent(rv.id, "done", reason);
13426
+ }
13427
+ async function closeReviewersOnDeadPr(deps, reviewRunId, reason) {
13428
+ for (const rv of deps.db.activeReviewersFor(reviewRunId)) await closeReviewerRun(deps, rv, reason);
11997
13429
  }
11998
13430
  function ciWaitStartedMs(db, runId) {
11999
13431
  const evs = db.events(runId);
@@ -12067,7 +13499,39 @@ async function finalizeMergedReview(deps, run) {
12067
13499
  deps.db.addEvent(run.id, "merged", run.pr_url ?? "");
12068
13500
  deps.db.updateRun(run.id, { status: "done" });
12069
13501
  deps.db.addEvent(run.id, "done");
12070
- await closeReviewersOnMerge(deps, run.id);
13502
+ await closeReviewersOnDeadPr(deps, run.id, "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E \u2014 \u0440\u0435\u0432\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E");
13503
+ }
13504
+ async function finalizeClosedPrReview(deps, run, reason) {
13505
+ await closeReviewersOnDeadPr(deps, run.id, "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E");
13506
+ if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
13507
+ });
13508
+ deps.db.updateRun(run.id, { status: "done" });
13509
+ deps.db.addEvent(run.id, "done", reason);
13510
+ }
13511
+ var REVIEW_PR_UNKNOWN_MARK = "\u0441\u0442\u0430\u0442\u0443\u0441 PR \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D";
13512
+ async function prGateForReviewer(deps, run, prUrl, when) {
13513
+ const st = await deps.prStatus(prUrl).catch(() => null);
13514
+ switch (prLiveness(st)) {
13515
+ case "merged":
13516
+ await finalizeMergedReview(deps, run);
13517
+ narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D, ${when} \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D, \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D.`);
13518
+ return { kind: "finalized" };
13519
+ case "closed":
13520
+ await finalizeClosedPrReview(deps, run, `PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 (${when}) \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E`);
13521
+ narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, ${when} \u2014 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E, \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442.`);
13522
+ return { kind: "finalized" };
13523
+ case "unknown":
13524
+ if (!deps.db.events(run.id).some((e) => (e.text ?? "").includes(REVIEW_PR_UNKNOWN_MARK))) {
13525
+ deps.db.addEvent(run.id, "step", `${REVIEW_PR_UNKNOWN_MARK} (gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0438\u043B\u0438 \u0432 pr_url \u043D\u0435 PR) \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u043C, \u0441\u043F\u0440\u043E\u0441\u0438\u043C \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C \u0442\u0438\u043A\u0435`);
13526
+ }
13527
+ return { kind: "wait" };
13528
+ default:
13529
+ if (st.conflicting || st.isDraft) {
13530
+ 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, ${when} \u2014 \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D \u0434\u043E \u0441\u043B\u0435\u0434. \u0442\u0438\u043A\u0430`);
13531
+ return { kind: "wait" };
13532
+ }
13533
+ return { kind: "spawn", st };
13534
+ }
12071
13535
  }
12072
13536
  function doneMergedPrForSlug(db, slug) {
12073
13537
  const done = db.listRuns("done").find((r) => r.task_slug === slug && r.pr_url && r.kind !== "hero" && r.resolves_run_id == null && r.reviews_run_id == null);
@@ -12130,6 +13594,22 @@ async function closeMergedTaskRuns(deps) {
12130
13594
  }
12131
13595
  }
12132
13596
  }
13597
+ async function closeOrphanedReviewers(deps) {
13598
+ for (const rv of deps.db.listRuns()) {
13599
+ if (rv.kind !== "reviewer" || rv.reviews_run_id == null) continue;
13600
+ if (!REVIEWER_ALIVE_STATUSES.has(rv.status)) continue;
13601
+ const parent = deps.db.getRun(rv.reviews_run_id);
13602
+ if (parent && !isTerminalStatus(parent.status)) continue;
13603
+ try {
13604
+ const why = parent ? `\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442 (${STATUS_META[parent.status]?.label ?? parent.status})` : "\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u0438\u0441\u0447\u0435\u0437";
13605
+ await closeReviewerRun(deps, rv, `${why} \u2014 PR \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u0440\u0435\u0432\u044C\u044E\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E`);
13606
+ if (parent) deps.db.addEvent(parent.id, "step", `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${rv.name} (#${rv.id}) \u0437\u0430\u043A\u0440\u044B\u0442: ${why}`);
13607
+ narrate(deps, `${droneLabel(rv)} \u043E\u0441\u0442\u0430\u043B\u0441\u044F \u043D\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u043E\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u0435 \u2014 \u0433\u0430\u0448\u0443 \u0441\u0430\u043C, \u0441\u043B\u043E\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0451\u043D.`);
13608
+ } catch (e) {
13609
+ console.error(`[overmind] \u0437\u0430\u043A\u0440\u044B\u0442\u0438\u0435 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 #${rv.id} \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:`, e);
13610
+ }
13611
+ }
13612
+ }
12133
13613
  var CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C (\u0441\u0435\u0442\u044C/\u043F\u0440\u043E\u0446\u0435\u0441\u0441) \u2014 \u0442\u044B headless-\u0434\u0440\u043E\u043D, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u0423 \u0416\u0415 \u0437\u0430\u0434\u0430\u0447\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 `git status`/`git log` \u0438 \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0441\u0432\u043E\u0438\u0445 \u0448\u0430\u0433\u043E\u0432: \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E \u2014 \u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438 \u0441 \u043C\u0435\u0441\u0442\u0430 \u043E\u0431\u0440\u044B\u0432\u0430. \u0414\u043E\u0432\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u043E \u043A\u043E\u043D\u0446\u0430, \u041A\u041E\u041C\u041C\u0418\u0422\u042C \u0418 \u041F\u0423\u0428\u042C \u043F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0448\u0430\u0433\u0430, \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url). \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 MCP-\u0442\u0443\u043B ask.";
12134
13614
  function buildCiWakeNudge(summary) {
12135
13615
  return [
@@ -12188,8 +13668,12 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
12188
13668
  // авторские подписи внутри него уже приведены к одной строке (renderAttachmentsBlock).
12189
13669
  ...attachmentsBlock ? ["", attachmentsBlock] : [],
12190
13670
  "",
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).",
13671
+ // TASWM-356: ветка worktree — НЕ обязательно слаг. У продолжения чужого PR она своя
13672
+ // (её завёл прошлый дрон), и `git push -u origin <slug>` оттуда либо откажет, либо заведёт
13673
+ // вторую ветку мимо PR. Пишем ту, что реально записана на прогоне; пусто (первый спавн —
13674
+ // ветку заводит spawnDrone из слага) — прежняя формулировка.
13675
+ `\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"}.`,
13676
+ "\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
13677
  "\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
13678
  // TASWM-293: живой случай SCE-5 — ревьюер заблокировал PR за отсутствие CI (у проекта
12195
13679
  // его нет и не предполагается), автора разбудили «закрывать замечания», замечаний по коду
@@ -12206,7 +13690,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
12206
13690
  `\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
13691
  "",
12208
13692
  "\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.`,
13693
+ `\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
13694
  "\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
13695
  '\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
13696
  "",
@@ -12231,7 +13715,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
12231
13715
  }
12232
13716
  return parts.join("\n");
12233
13717
  }
12234
- var HERO_CHARTER_PATH = join25(dirname5(THIS_FILE), "..", "docs", "HERO.md");
13718
+ var HERO_CHARTER_PATH = join29(dirname4(THIS_FILE), "..", "docs", "HERO.md");
12235
13719
  function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
12236
13720
  const name = run.name || `#${run.id}`;
12237
13721
  const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
@@ -12485,8 +13969,8 @@ async function wakeRun(deps, runId) {
12485
13969
  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
13970
  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
13971
  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;
13972
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
13973
+ const hasTranscript = deps.hasTranscript ?? executorHasSession;
12490
13974
  const wt = run.worktree_path;
12491
13975
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
12492
13976
  const isService = run.reviews_run_id != null || run.resolves_run_id != null;
@@ -12570,9 +14054,9 @@ function repoHasCi(deps, run) {
12570
14054
  function repoHasCiWorkflows(worktreePath, repo) {
12571
14055
  for (const base3 of [worktreePath, repo]) {
12572
14056
  if (!base3) continue;
12573
- const dir = join25(base3, ".github", "workflows");
14057
+ const dir = join29(base3, ".github", "workflows");
12574
14058
  try {
12575
- if (readdirSync10(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
14059
+ if (readdirSync12(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
12576
14060
  } catch {
12577
14061
  }
12578
14062
  }
@@ -12719,8 +14203,8 @@ async function wakeReviewerOnCiReady(deps, run, over) {
12719
14203
  return true;
12720
14204
  }
12721
14205
  function worktreeAndTranscriptAlive(deps, worktree) {
12722
- const exists = deps.worktreeExists ?? existsSync15;
12723
- const transcript = deps.hasTranscript ?? sessionTranscriptExists;
14206
+ const exists = deps.worktreeExists ?? existsSync16;
14207
+ const transcript = deps.hasTranscript ?? executorHasSession;
12724
14208
  return exists(worktree) && transcript(worktree);
12725
14209
  }
12726
14210
  async function spawnFreshReviewerAfterStall(deps, run, why) {
@@ -12733,10 +14217,9 @@ async function spawnFreshReviewerAfterStall(deps, run, why) {
12733
14217
  if (run.review_cycle >= REVIEW_CYCLE_LIMIT || deps.db.reviewerAttemptCountFor(run.id) >= REVIEW_SPAWN_LIMIT) {
12734
14218
  return;
12735
14219
  }
12736
- const st = await deps.prStatus(run.pr_url).catch(() => null);
12737
- if (!st || st.merged || st.state === "CLOSED" || st.conflicting || st.isDraft) {
12738
- return;
12739
- }
14220
+ const gate = await prGateForReviewer(deps, run, run.pr_url, "\u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B CI");
14221
+ if (gate.kind !== "spawn") return;
14222
+ const st = gate.st;
12740
14223
  try {
12741
14224
  await deps.spawnReviewer(run, st.files ?? []);
12742
14225
  narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u043F\u043E\u0442\u0435\u0440\u044F\u043B\u043E \u0441\u0435\u0441\u0441\u0438\u044E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430, \u0436\u0434\u0430\u0432 CI (${why}) \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0441\u0440\u0430\u0437\u0443, \u043D\u0435 \u0436\u0434\u0443 \u043A\u0430\u0440\u0443\u0441\u0435\u043B\u044C.`);
@@ -12823,6 +14306,22 @@ async function envGateAllows(deps, run) {
12823
14306
  function effectiveRepo(deps, run) {
12824
14307
  return normalizeRepo(run.repo || deps.repoFallback || "");
12825
14308
  }
14309
+ async function zoneConflictFor(db, models, activeFiles, run, repo, repoFallback) {
14310
+ const model = await models.forRepo(repo);
14311
+ if (!hasZoneModel(model)) return { kind: "unknown" };
14312
+ const live = db.listRuns().filter((r) => (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status));
14313
+ activeFiles.prune(live.map((r) => r.id));
14314
+ const active = [];
14315
+ for (const r of live) {
14316
+ if (r.id === run.id) continue;
14317
+ if (normalizeRepo(r.repo || repoFallback || "") !== repo) continue;
14318
+ const files = await activeFiles.filesOf(r);
14319
+ active.push(
14320
+ files.length ? { run: r, files, actual: true } : { run: r, files: predictForRun(model, r).map((p) => p.path), actual: false }
14321
+ );
14322
+ }
14323
+ return decideConflict({ model, candidate: run, active });
14324
+ }
12826
14325
  function activeDronesOnRepo(deps, repo, exceptRunId) {
12827
14326
  return deps.db.listRuns().filter(
12828
14327
  (r) => r.id !== exceptRunId && (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status) && effectiveRepo(deps, r) === repo
@@ -12863,10 +14362,10 @@ async function exclusiveGateAllows(deps, run) {
12863
14362
  return true;
12864
14363
  }
12865
14364
  function pruneGateLogs(deps) {
12866
- if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size) return;
14365
+ if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size && !deps.overlapLogged?.size) return;
12867
14366
  const open = /* @__PURE__ */ new Set();
12868
14367
  for (const run of deps.db.listRuns()) if (!isTerminalStatus(run.status)) open.add(run.id);
12869
- for (const map of [deps.envGateLogged, deps.exclusiveLogged]) {
14368
+ for (const map of [deps.envGateLogged, deps.exclusiveLogged, deps.overlapLogged]) {
12870
14369
  if (!map) continue;
12871
14370
  for (const id of [...map.keys()]) if (!open.has(id)) map.delete(id);
12872
14371
  }
@@ -12887,6 +14386,18 @@ async function exclusivityBlockedRunIds(deps) {
12887
14386
  }
12888
14387
  return blocked;
12889
14388
  }
14389
+ async function fillSuggestedArea(deps, run) {
14390
+ if (run.area || !deps.suggestArea) return;
14391
+ const guess = await deps.suggestArea(run).catch(() => null);
14392
+ if (!guess) return;
14393
+ deps.db.updateRun(run.id, { area: guess.zone });
14394
+ run.area = guess.zone;
14395
+ deps.db.addEvent(
14396
+ run.id,
14397
+ "step",
14398
+ `\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`
14399
+ );
14400
+ }
12890
14401
  async function trySpawnQueuedRun(deps, run) {
12891
14402
  if (deps.control?.paused) return "skipped";
12892
14403
  const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
@@ -12902,11 +14413,21 @@ async function trySpawnQueuedRun(deps, run) {
12902
14413
  if (!await exclusiveGateAllows(deps, run)) return "skipped";
12903
14414
  if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
12904
14415
  if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
14416
+ await fillSuggestedArea(deps, run);
12905
14417
  try {
12906
14418
  await deps.spawn(run);
12907
14419
  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
14420
  return "spawned";
12909
14421
  } catch (e) {
14422
+ if (e instanceof FileOverlapError) {
14423
+ const logged = deps.overlapLogged ??= /* @__PURE__ */ new Map();
14424
+ if (logged.get(run.id) !== e.message) {
14425
+ logged.set(run.id, e.message);
14426
+ 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)`);
14427
+ 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.`);
14428
+ }
14429
+ return "skipped";
14430
+ }
12910
14431
  if (e instanceof AreaBusyError) return "skipped";
12911
14432
  deps.db.updateRun(run.id, { status: "failed" });
12912
14433
  deps.db.addEvent(run.id, "failed", String(e));
@@ -12933,8 +14454,8 @@ async function resumeWatchdogParked(deps, droneCap) {
12933
14454
  }
12934
14455
  if (!candidate) return;
12935
14456
  if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
12936
- const worktreeExists = deps.worktreeExists ?? existsSync15;
12937
- const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
14457
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
14458
+ const hasTranscript = deps.hasTranscript ?? executorHasSession;
12938
14459
  const wt = candidate.worktree_path;
12939
14460
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
12940
14461
  const mode = canContinue ? "continue" : "fresh";
@@ -13102,8 +14623,8 @@ async function manageRelief(deps, now, droneCap, stormActive) {
13102
14623
  if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
13103
14624
  if (!deps.respawn) return;
13104
14625
  const wt = resumeCandidate.worktree_path;
13105
- const worktreeExists = deps.worktreeExists ?? existsSync15;
13106
- const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
14626
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
14627
+ const hasTranscript = deps.hasTranscript ?? executorHasSession;
13107
14628
  const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
13108
14629
  const mode = canContinue ? "continue" : "fresh";
13109
14630
  if (mode === "continue" && wt && deps.reclaimWorktree) {
@@ -13226,15 +14747,28 @@ ${journal}`);
13226
14747
  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
14748
  });
13228
14749
  }
14750
+ function syncBranchFromPr(db, run, headBranch) {
14751
+ const fix = branchFromPrPatch(run.branch, headBranch);
14752
+ if (!fix) return run;
14753
+ const updated = db.updateRun(run.id, fix.patch) ?? run;
14754
+ db.addEvent(run.id, "step", fix.event);
14755
+ return updated;
14756
+ }
14757
+ async function worktreeBranchFor(run, prHead) {
14758
+ if (run.branch) return run.branch;
14759
+ const fromPr = run.pr_url && prHead ? await prHead(run.pr_url).catch(() => null) : null;
14760
+ return fromPr || run.task_slug;
14761
+ }
13229
14762
  async function adoptDroneToReview(deps, run, prUrl) {
13230
14763
  const t = applyReviewTransition(deps.db, run, prUrl);
14764
+ const adopted = syncBranchFromPr(deps.db, run, deps.prHeadBranch ? await deps.prHeadBranch(prUrl).catch(() => null) : null);
13231
14765
  deps.db.addEvent(run.id, "pr_opened", prUrl);
13232
14766
  deps.db.addEvent(
13233
14767
  run.id,
13234
14768
  "step",
13235
14769
  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
14770
  );
13237
- await postAdoptionReport(deps, run, prUrl, t.cycle ?? run.review_cycle ?? 0);
14771
+ await postAdoptionReport(deps, adopted, prUrl, t.cycle ?? run.review_cycle ?? 0);
13238
14772
  }
13239
14773
  async function postScrapedVerdictReport(deps, runId, taskSlug, verdict, droneName, workspaceId, cycle) {
13240
14774
  if (!deps.reportComment) return;
@@ -13264,8 +14798,8 @@ function buildEscalationCandidates(db) {
13264
14798
  }
13265
14799
  async function tickOnce(deps) {
13266
14800
  const now = deps.now ? deps.now() : Date.now();
13267
- const worktreeExists = deps.worktreeExists ?? existsSync15;
13268
- const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
14801
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
14802
+ const hasTranscript = deps.hasTranscript ?? executorHasSession;
13269
14803
  if (deps.rehydrate) {
13270
14804
  const rh = deps.rehydrate;
13271
14805
  const sinceLast = rh.lastAt == null ? Infinity : now - rh.lastAt;
@@ -13391,7 +14925,7 @@ async function tickOnce(deps) {
13391
14925
  if (!stormActive) {
13392
14926
  await resumeWatchdogParked(deps, droneCap);
13393
14927
  }
13394
- for (const run of deps.db.listRuns("review")) {
14928
+ for (let run of deps.db.listRuns("review")) {
13395
14929
  try {
13396
14930
  if (run.resolves_run_id != null) {
13397
14931
  if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
@@ -13410,12 +14944,19 @@ async function tickOnce(deps) {
13410
14944
  continue;
13411
14945
  }
13412
14946
  if (!run.pr_url) continue;
13413
- const st = await deps.prStatus(run.pr_url);
14947
+ const prUrl = run.pr_url;
14948
+ const st = await deps.prStatus(prUrl);
14949
+ run = syncBranchFromPr(deps.db, run, st.headBranch);
13414
14950
  if (st.merged) {
13415
14951
  await finalizeMergedReview(deps, run);
13416
14952
  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.`);
13417
14953
  continue;
13418
14954
  }
14955
+ if (prLiveness(st) === "closed") {
14956
+ await finalizeClosedPrReview(deps, run, "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E, \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442");
14957
+ narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 ${run.task_slug} \u0441\u043D\u0438\u043C\u0430\u0435\u0442\u0441\u044F \u0441 \u0440\u0435\u0432\u044C\u044E, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
14958
+ continue;
14959
+ }
13419
14960
  if (touchesDaemonCode(st.files ?? [])) {
13420
14961
  const already = deps.db.events(run.id).some((e) => (e.text ?? "").includes(SELF_REFERENCE_MARK));
13421
14962
  if (!already) {
@@ -13479,11 +15020,13 @@ async function tickOnce(deps) {
13479
15020
  continue;
13480
15021
  }
13481
15022
  const attempts = deps.db.reviewerAttemptCountFor(run.id);
15023
+ const runEvents = deps.db.events(run.id);
15024
+ const unrecoverableMark = reviewUnrecoverableMark(run.branch || run.task_slug);
13482
15025
  let verdictFromComment = null;
13483
15026
  let lastAttempt = null;
13484
15027
  if (attempts > 0 && deps.verdictComment) {
13485
15028
  lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
13486
- verdictFromComment = await deps.verdictComment(run.pr_url, lastAttempt?.created_at ?? run.created_at).catch(() => null);
15029
+ verdictFromComment = await deps.verdictComment(prUrl, lastAttempt?.created_at ?? run.created_at).catch(() => null);
13487
15030
  }
13488
15031
  if (verdictFromComment) {
13489
15032
  const mark = verdictCommentMark(verdictFromComment.createdAt);
@@ -13498,32 +15041,32 @@ async function tickOnce(deps) {
13498
15041
  await wakeAuthorOnChanges(deps, run, now, droneCap);
13499
15042
  }
13500
15043
  }
15044
+ } else if (runEvents.some((e) => (e.text ?? "").includes(unrecoverableMark))) {
13501
15045
  } else if (attempts >= REVIEW_SPAWN_LIMIT) {
13502
- if (!deps.db.events(run.id).some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
15046
+ if (!runEvents.some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
13503
15047
  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
15048
  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
15049
  }
13506
15050
  } else {
13507
15051
  const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
13508
15052
  if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk) {
13509
- const freshSt = await deps.prStatus(run.pr_url);
13510
- if (freshSt.merged) {
13511
- await finalizeMergedReview(deps, run);
13512
- 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.`);
13513
- } else if (freshSt.state === "CLOSED") {
13514
- if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
13515
- });
13516
- deps.db.updateRun(run.id, { status: "done" });
13517
- deps.db.addEvent(run.id, "done", "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E");
13518
- narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
13519
- } else if (freshSt.conflicting || freshSt.isDraft) {
13520
- 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
- } else {
15053
+ const gate = await prGateForReviewer(deps, run, prUrl, "\u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430");
15054
+ if (gate.kind === "spawn") {
15055
+ run = syncBranchFromPr(deps.db, run, gate.st.headBranch);
13522
15056
  try {
13523
15057
  await deps.spawnReviewer(run, st.files ?? []);
13524
15058
  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
15059
  } 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)}`);
15060
+ if (isUnrecoverableRefError(e)) {
15061
+ deps.db.addEvent(
15062
+ run.id,
15063
+ "step",
15064
+ `${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`
15065
+ );
15066
+ 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.`);
15067
+ } else {
15068
+ 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)}`);
15069
+ }
13527
15070
  }
13528
15071
  }
13529
15072
  }
@@ -13534,6 +15077,7 @@ async function tickOnce(deps) {
13534
15077
  }
13535
15078
  }
13536
15079
  await closeMergedTaskRuns(deps);
15080
+ await closeOrphanedReviewers(deps);
13537
15081
  refreshReviewPhases(deps);
13538
15082
  let liveChildPpids;
13539
15083
  try {
@@ -13790,7 +15334,9 @@ async function tickOnce(deps) {
13790
15334
  // resource-gate-stall (TASWM-212): та же причина, что видит лог manageRelief — один
13791
15335
  // deps.gateReason() на тик (кэш hostReading() внутри resources.ts, лишнего спавна нет).
13792
15336
  ...deps.gateReason ? { gateReason: deps.gateReason() } : {},
13793
- ...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {}
15337
+ ...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {},
15338
+ // Рестарт не переигрывает падения (TAS-485) — см. closedBeforeStart в anomalies.ts.
15339
+ ...az.startedAt != null ? { daemonStartedAt: az.startedAt } : {}
13794
15340
  }),
13795
15341
  ...buildEscalationCandidates(deps.db),
13796
15342
  // ЦЕНА РАЗГРУЗКИ (TASWM-316): «машина не тянет» с числами и рекомендацией — та же
@@ -13820,6 +15366,20 @@ async function tickOnce(deps) {
13820
15366
  }
13821
15367
  narrate(deps, `\u26A0\uFE0F ${signal.title}`);
13822
15368
  }
15369
+ const stale = az.tracker.pendingResolved();
15370
+ if (stale.length > 0) {
15371
+ if (!az.resolve) {
15372
+ az.tracker.forgetResolved(stale);
15373
+ } else {
15374
+ const batch = stale.slice(0, ANOMALY_RESOLVE_BATCH);
15375
+ try {
15376
+ await az.resolve(batch);
15377
+ az.tracker.forgetResolved(batch);
15378
+ } catch (e) {
15379
+ 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);
15380
+ }
15381
+ }
15382
+ }
13823
15383
  }
13824
15384
  if (deps.watchdog) {
13825
15385
  try {
@@ -13885,7 +15445,7 @@ async function cancelRun(deps, slug, reason) {
13885
15445
  }
13886
15446
  async function resolveConflict(deps, reviewRun) {
13887
15447
  const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
13888
- const branch = reviewRun.branch || reviewRun.task_slug;
15448
+ const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
13889
15449
  const resolver = deps.db.createRun({
13890
15450
  task_slug: reviewRun.task_slug,
13891
15451
  repo,
@@ -13895,7 +15455,7 @@ async function resolveConflict(deps, reviewRun) {
13895
15455
  workspace_id: reviewRun.workspace_id
13896
15456
  // та же задача, тот же воркспейс (TASWM-288)
13897
15457
  });
13898
- const wt = join25(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
15458
+ const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
13899
15459
  try {
13900
15460
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
13901
15461
  await deps.git.setWorktreeIdentity(wt, resolver.name);
@@ -13925,14 +15485,14 @@ async function resolveConflict(deps, reviewRun) {
13925
15485
  await deps.git.removeWorktree(repo, wt).catch(() => {
13926
15486
  });
13927
15487
  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";
15488
+ 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
15489
  deps.db.addEvent(resolver.id, "failed", `${marker}: ${String(e)}`);
13930
15490
  throw e;
13931
15491
  }
13932
15492
  }
13933
15493
  async function spawnReviewer(deps, reviewRun, files) {
13934
15494
  const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
13935
- const branch = reviewRun.branch || reviewRun.task_slug;
15495
+ const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
13936
15496
  const priorReview = reviewRun.review_cycle > 0 ? lastChangesSummary(deps.db, reviewRun.id) : null;
13937
15497
  const verifyMode = deps.verifyMode ? await deps.verifyMode(repo).catch(() => void 0) : void 0;
13938
15498
  const checklist = buildReviewChecklist(reviewRun, files, priorReview, verifyMode);
@@ -13947,7 +15507,7 @@ async function spawnReviewer(deps, reviewRun, files) {
13947
15507
  // (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
13948
15508
  workspace_id: reviewRun.workspace_id
13949
15509
  });
13950
- const wt = join25(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
15510
+ const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
13951
15511
  try {
13952
15512
  await deps.git.prepareDetachedWorktree(repo, branch, wt);
13953
15513
  await deps.git.setWorktreeIdentity(wt, reviewer.name);
@@ -13982,7 +15542,7 @@ async function respawnRun(deps, run, mode, nudge) {
13982
15542
  ]);
13983
15543
  const base3 = effectiveBaseBranch(run.base_branch, projectBase);
13984
15544
  const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
13985
- if (!existsSync15(wt)) {
15545
+ if (!existsSync16(wt)) {
13986
15546
  const branch = runToSpawn.branch || runToSpawn.task_slug;
13987
15547
  const source = await addWorktree(repo, wt, branch, {
13988
15548
  ...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
@@ -14011,7 +15571,7 @@ ${notice}` : freshPrompt;
14011
15571
  deps.db.updateRun(run.id, { status: "running", pid });
14012
15572
  }
14013
15573
  async function syncCrashedOnStartup(deps) {
14014
- const worktreeExists = deps.worktreeExists ?? existsSync15;
15574
+ const worktreeExists = deps.worktreeExists ?? existsSync16;
14015
15575
  let orphaned = 0;
14016
15576
  let merged = 0;
14017
15577
  let requeued = 0;
@@ -14074,7 +15634,7 @@ async function makeStore(repo, worktreesDir, repoFlag) {
14074
15634
  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
15635
  }
14076
15636
  }
14077
- const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW, homedir4(), repoPaths);
15637
+ const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW, homedir5(), repoPaths);
14078
15638
  const repoMap = emptyRepoMapState(scanRootsList);
14079
15639
  if (SWARM_BACKEND !== "client") {
14080
15640
  const { SwarmDb: SwarmDb2 } = await loadSqlite();
@@ -14148,10 +15708,10 @@ async function migrateWorktreesAtStartup() {
14148
15708
  legacyDir: WORKTREES_LEGACY_DIR,
14149
15709
  targetDir: WORKTREES_DIR,
14150
15710
  envOverride: process.env.SWARM_WORKTREES || "",
14151
- exists: (d) => existsSync15(d),
15711
+ exists: (d) => existsSync16(d),
14152
15712
  isNonEmptyDir: (d) => {
14153
15713
  try {
14154
- return readdirSync10(d).length > 0;
15714
+ return readdirSync12(d).length > 0;
14155
15715
  } catch {
14156
15716
  return false;
14157
15717
  }
@@ -14163,7 +15723,7 @@ async function migrateWorktreesAtStartup() {
14163
15723
  });
14164
15724
  } catch (e) {
14165
15725
  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;
15726
+ return existsSync16(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
14167
15727
  }
14168
15728
  }
14169
15729
  async function reportWorkspaceAccess(tl, db) {
@@ -14180,7 +15740,7 @@ async function reportWorkspaceAccess(tl, db) {
14180
15740
  }
14181
15741
  async function runDaemon(opts) {
14182
15742
  installCrashGuards();
14183
- const daemonStart = await captureDaemonStart(THIS_FILE);
15743
+ const daemonStart = captureDaemonStart();
14184
15744
  const latestRelease = fetchLatestVersion();
14185
15745
  const worktreesDir = await migrateWorktreesAtStartup();
14186
15746
  if (!opts.repo) {
@@ -14233,18 +15793,23 @@ async function runDaemon(opts) {
14233
15793
  overmindEntry: THIS_FILE,
14234
15794
  dbPath: DB_PATH,
14235
15795
  runLogsDir: RUN_LOGS_DIR,
15796
+ // Кем исполнять (TASWM-349) — настройка SWARM_EXECUTOR, один резолв на весь демон.
15797
+ executor: EXECUTOR,
14236
15798
  droneIsolation: DRONE_ISOLATION,
14237
15799
  channel: droneChannelLaunch,
14238
15800
  droneCap: () => lastDroneCap
14239
15801
  });
14240
15802
  let prepareReader;
15803
+ const zoneModels = new ZoneModels({ manualAreas: () => new Map(db.listRuns().map((r) => [r.task_slug, r.area])) });
15804
+ const activeFiles = new ActiveFilesCache((r) => changedPaths(r.worktree_path, { baseBranch: r.base_branch ?? null }));
14241
15805
  const baseSpawn = {
14242
15806
  db,
14243
15807
  repo: opts.repo,
14244
15808
  // фолбэк, если у прогона repo пуст
14245
15809
  worktreesDir,
14246
15810
  launch,
14247
- prepare: (repo) => prepareReader ? prepareReader(repo) : Promise.resolve(null)
15811
+ prepare: (repo) => prepareReader ? prepareReader(repo) : Promise.resolve(null),
15812
+ conflicts: (run, repo) => zoneConflictFor(db, zoneModels, activeFiles, run, repo, opts.repo)
14248
15813
  };
14249
15814
  let intake;
14250
15815
  if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_MAX_IN_FLIGHT > 0) {
@@ -14261,7 +15826,7 @@ async function runDaemon(opts) {
14261
15826
  repoMap = { run: rebuildRepoMap, intervalMs: SWARM_REPO_MAP_MS, lastAt: Date.now() };
14262
15827
  }
14263
15828
  let daemonVersion;
14264
- if (daemonStart && SWARM_VERSION_CHECK_MS > 0) {
15829
+ if (SWARM_VERSION_CHECK_MS > 0) {
14265
15830
  let cached = null;
14266
15831
  daemonVersion = {
14267
15832
  run: async () => {
@@ -14278,7 +15843,7 @@ async function runDaemon(opts) {
14278
15843
  const client = db;
14279
15844
  heartbeat = {
14280
15845
  push: async () => {
14281
- const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null);
15846
+ const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastDroneCap);
14282
15847
  if (reply.commands.length) onHostCommands(reply.commands);
14283
15848
  if (reply.logRequests.length) {
14284
15849
  await serveLogRequests(
@@ -14348,7 +15913,14 @@ async function runDaemon(opts) {
14348
15913
  autoReviewEnabled: SWARM_AUTO_REVIEW,
14349
15914
  outboxPendingCount: () => client.pendingCount(),
14350
15915
  notify: (signal) => client.notifyAnomaly(signal),
14351
- stopQuiet: () => anomalyState.stopQuiet()
15916
+ // TAS-485: условие ушло → гасим свою же тревогу в инбоксе.
15917
+ resolve: (keys) => client.resolveAnomalies(keys),
15918
+ stopQuiet: () => anomalyState.stopQuiet(),
15919
+ // TAS-485: этот демон отвечает за то, что случилось ПРИ НЁМ. Прогон, закрытый до
15920
+ // старта, он не переоткрывает — иначе каждый перезапуск начинает волну заново
15921
+ // (id прогонов в зеркале свои у каждой жизни процесса, durable-кулдаун по ним не
15922
+ // совпадает — см. closedBeforeStart в anomalies.ts).
15923
+ startedAt: Date.now()
14352
15924
  };
14353
15925
  }
14354
15926
  const watchdog = {
@@ -14421,6 +15993,9 @@ async function runDaemon(opts) {
14421
15993
  worktreesDir,
14422
15994
  launch,
14423
15995
  git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
15996
+ // TASWM-356: ветка ревьюера — фактическая ветка PR. Штатно она уже в поле прогона
15997
+ // (реконсайл синхронит её из headBranch), этот ридер — для прогонов без неё.
15998
+ prHeadBranch,
14424
15999
  // TASWM-293: ТОТ ЖЕ живой ридер verifyMode, что у исполнителя (spawnDrone) —
14425
16000
  // иначе ревьюер судит проект с verifyMode=local по правилам ci и блокирует PR
14426
16001
  // за отсутствие CI, которого у проекта нет и не предполагается.
@@ -14513,7 +16088,7 @@ async function runDaemon(opts) {
14513
16088
  if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
14514
16089
  if (taskClient) await taskClient.startTask(runToSpawn.task_slug);
14515
16090
  },
14516
- prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state })),
16091
+ 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
16092
  // Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
14518
16093
  // (фолбэк — дефолт-репо роя). Реальный gh; логика усыновления — в tickOnce (тестируется).
14519
16094
  openPrsForBranch: (branch, repoDir) => openPrsForBranch(branch, repoDir || opts.repo),
@@ -14537,6 +16112,8 @@ async function runDaemon(opts) {
14537
16112
  reportComment: (slug, body, label, ws) => tl.forWorkspace(ws).addComment(slug, body, label)
14538
16113
  } : {},
14539
16114
  prCommitTitles: (prUrl) => prCommitTitles(prUrl),
16115
+ prHeadBranch: (prUrl) => prHeadBranch(prUrl),
16116
+ // TASWM-356: ветка PR на усыновлении, узким запросом
14540
16117
  journalForBranch: (repo, branch, slug) => journalForBranch(repo || opts.repo, branch, slug),
14541
16118
  removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p),
14542
16119
  // TASWM-303: тот же фолбэк repo, что у removeWorktree/journalForBranch — run.repo пуст
@@ -14546,15 +16123,17 @@ async function runDaemon(opts) {
14546
16123
  repoHasCi: (run) => repoHasCiWorkflows(run.worktree_path, run.repo || opts.repo),
14547
16124
  isAlive: pidAlive,
14548
16125
  // Учёт cost/usage (TASWM-188): реальный аккаунтант поверх RUN_LOGS_DIR + db.
14549
- accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db }),
16126
+ accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR }),
14550
16127
  // Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
14551
16128
  // (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
14552
- exitOutcome: makeExitClassifier(RUN_LOGS_DIR),
16129
+ exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
14553
16130
  // Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
14554
16131
  livePpids,
14555
16132
  // Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
14556
16133
  // POST /swarm/kill → stopped в проде → overmind гасит живой pid на реконсайле).
14557
- kill: (pid) => killProcessGroup(pid, "SIGTERM"),
16134
+ // TASWM-349: свой способ остановки — за адаптером; не задан (оба сегодняшних) → общий
16135
+ // групповой SIGTERM.
16136
+ kill: (pid) => EXECUTOR.stop ? EXECUTOR.stop(pid, "SIGTERM") : killProcessGroup(pid, "SIGTERM"),
14558
16137
  // Реклейм worktree паузнутого/возвращаемого дрона (TASWM-228): тот же cwd-скан, что и
14559
16138
  // очистка перед сносом worktree, но БЕЗ сноса каталога — worktree остаётся под
14560
16139
  // --continue. Гасит тяжёлую работу дрона, ушедшую из его process group (Bash-тул
@@ -14593,7 +16172,7 @@ async function runDaemon(opts) {
14593
16172
  lastCommitMs: (p) => worktreeLastCommitMs(p),
14594
16173
  // mtime транскрипта claude-сессии как liveness (TASWM-136): точнее коммитов — обновляется
14595
16174
  // на каждое действие дрона (Edit/Read/tool-use), не только на коммит.
14596
- transcriptMtimeMs: (p) => transcriptMtimeMs(p),
16175
+ transcriptMtimeMs: (p) => EXECUTOR.session.mtimeMs(p),
14597
16176
  // Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
14598
16177
  resourcesOk: hostResourcesOk,
14599
16178
  // Ресурс-гейт v2 (TASWM-151): жёсткий порог активной разгрузки — те же сырые показания
@@ -14624,6 +16203,13 @@ async function runDaemon(opts) {
14624
16203
  ...exclusiveProject ? { exclusiveProject } : {},
14625
16204
  repoFallback: opts.repo,
14626
16205
  exclusiveLogged: /* @__PURE__ */ new Map(),
16206
+ // Зона задачи из истории репо (TASWM-348) — предлагаем сами, если постановщик
16207
+ // её не проставил. Дедуп событий отдельной мапой, как у гейтов выше.
16208
+ suggestArea: async (run) => {
16209
+ const repo = normalizeRepo(run.repo || opts.repo || "");
16210
+ return repo ? suggestAreaForRun(await zoneModels.forRepo(repo), run) : null;
16211
+ },
16212
+ overlapLogged: /* @__PURE__ */ new Map(),
14627
16213
  // Ротация своего лога (TASWM-330): демон пишет в него неделями, а до этого ротация была
14628
16214
  // только при старте. Итог ротации печатаем в сам лог — первой строкой нового поколения
14629
16215
  // видно, что история не потерялась, а уехала в `.1`.
@@ -14648,7 +16234,9 @@ async function runDaemon(opts) {
14648
16234
  repoFallback: opts.repo,
14649
16235
  worktreesDir,
14650
16236
  launch,
14651
- git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree }
16237
+ git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree },
16238
+ prHeadBranch
16239
+ // TASWM-356: ветка PR для прогонов без записанного поля branch
14652
16240
  },
14653
16241
  reviewRun
14654
16242
  ).then(() => {
@@ -14679,8 +16267,8 @@ async function runDaemon(opts) {
14679
16267
  mode,
14680
16268
  nudge
14681
16269
  ),
14682
- worktreeExists: (p) => existsSync15(p),
14683
- hasTranscript: (p) => sessionTranscriptExists(p),
16270
+ worktreeExists: (p) => existsSync16(p),
16271
+ hasTranscript: executorHasSession,
14684
16272
  // CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
14685
16273
  // (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
14686
16274
  // строится над failing/failingRunIds ИЗ checks — без повторного `gh pr checks`.
@@ -14691,7 +16279,7 @@ async function runDaemon(opts) {
14691
16279
  // Штормовой режим (TASWM-23): health-probe на отбой (реальный claude) + внешний
14692
16280
  // сигнал SWARM_STORM_URL (2-й слой, если задан). Логика/пороги — в manageStorm/storm.ts.
14693
16281
  storm: {
14694
- probe: () => anthropicHealthy({ timeoutMs: resolveStormConfig().probeTimeoutMs }),
16282
+ probe: () => executorHealthy({ executor: EXECUTOR, timeoutMs: resolveStormConfig().probeTimeoutMs }),
14695
16283
  // externalDown опционален (exactOptionalPropertyTypes): ключ есть только при
14696
16284
  // заданном SWARM_STORM_URL, иначе опускаем — не передаём undefined.
14697
16285
  ...SWARM_STORM_URL ? { externalDown: () => fetchExternalStorm(SWARM_STORM_URL) } : {}
@@ -14837,11 +16425,33 @@ async function runDaemon(opts) {
14837
16425
  );
14838
16426
  return run ? { id: run.id } : null;
14839
16427
  },
16428
+ // TASWM-351: «перезапустить с того же места» из продукта. Тот же вход, что у
16429
+ // `swarm wake <id>` и у POST /wake локальной морды, — сознательно, чтобы у
16430
+ // побудки была ОДНА реализация: она умеет отличать «сессию можно продолжить»
16431
+ // от «продолжать нечем» и не будит служебные прогоны вслепую (см. wakeRun).
16432
+ // Слаг → прогон резолвим тем же activeRunForSlug, что и остановка: у команды
16433
+ // из веба нет и не будет локального id прогона (он живёт только в этом процессе).
16434
+ wakeRun: async (slug) => {
16435
+ const run = db.activeRunForSlug(slug);
16436
+ if (!run) return null;
16437
+ return wakeRun({ db, isAlive: pidAlive, respawn: deps.respawn, narrate: narrator.narrate }, run.id);
16438
+ },
14840
16439
  update: async (onStep) => {
14841
- if (!daemonStart) {
14842
- 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" };
16440
+ const pm = detectGlobalPm(process.argv[1]);
16441
+ if (pm !== "sources") {
16442
+ return runPackageSelfUpdate({
16443
+ currentVersion: PACKAGE_VERSION,
16444
+ installCommand: (spec) => globalInstallCommand(pm, spec),
16445
+ latest: () => fetchLatestVersion(),
16446
+ exec: execLoud,
16447
+ onStep
16448
+ });
16449
+ }
16450
+ const repo = await repoRoot(dirname4(THIS_FILE)).catch(() => null);
16451
+ if (!repo) {
16452
+ return { ok: false, step: "fetch", message: "\u0431\u0435\u0433\u0443 \u0438\u0437 \u0438\u0441\u0445\u043E\u0434\u043D\u0438\u043A\u043E\u0432, \u043D\u043E git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u043D\u0430\u0434 \u043D\u0438\u043C\u0438 \u043D\u0435\u0442 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E" };
14843
16453
  }
14844
- return runSelfUpdate(defaultSelfUpdateIO(daemonStart.repo, onStep));
16454
+ return runSelfUpdate(defaultSelfUpdateIO(repo, onStep));
14845
16455
  },
14846
16456
  narrate: narrator.narrate
14847
16457
  };
@@ -15047,7 +16657,9 @@ export {
15047
16657
  REVIEW_CYCLE_ESCALATION_MARK,
15048
16658
  REVIEW_CYCLE_LIMIT,
15049
16659
  REVIEW_ESCALATION_MARK,
16660
+ REVIEW_PR_UNKNOWN_MARK,
15050
16661
  REVIEW_SPAWN_LIMIT,
16662
+ REVIEW_UNRECOVERABLE_MARK,
15051
16663
  SELF_REFERENCE_MARK,
15052
16664
  STALL_FLOOR_MS,
15053
16665
  STALL_MS,
@@ -15070,6 +16682,7 @@ export {
15070
16682
  repoHasCiWorkflows,
15071
16683
  resolveConflict,
15072
16684
  respawnRun,
16685
+ reviewUnrecoverableMark,
15073
16686
  reviveStuck,
15074
16687
  runDaemon,
15075
16688
  spawnReviewer,