@taskless-app/swarm 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +38 -0
  2. package/dist/cli.js +1427 -307
  3. package/dist/overmind.js +1912 -548
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -578,6 +578,7 @@ function isHostOffline(lastSeenAt, nowMs, staleMs = HOST_HEARTBEAT_STALE_MS) {
578
578
  return nowMs - at > staleMs;
579
579
  }
580
580
  function hostStateText(host, nowMs = Date.now(), staleMs = HOST_HEARTBEAT_STALE_MS) {
581
+ if (host.draining) return host.drainReason ? `\u0432\u044B\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u043D\u0430 \u043E\u0431\u0441\u043B\u0443\u0436\u0438\u0432\u0430\u043D\u0438\u0435 \u2014 ${host.drainReason}` : "\u0432\u044B\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u043D\u0430 \u043E\u0431\u0441\u043B\u0443\u0436\u0438\u0432\u0430\u043D\u0438\u0435";
581
582
  if (!host.lastSeenAt) return "\u043F\u0443\u043B\u044C\u0441\u0430 \u043D\u0435 \u0431\u044B\u043B\u043E";
582
583
  const at = Date.parse(host.lastSeenAt);
583
584
  if (Number.isNaN(at)) return "\u043F\u0443\u043B\u044C\u0441\u0430 \u043D\u0435 \u0431\u044B\u043B\u043E";
@@ -603,8 +604,16 @@ var init_hosts = __esm({
603
604
  }
604
605
  });
605
606
 
607
+ // ../swarm-core/src/hostPool.ts
608
+ var init_hostPool = __esm({
609
+ "../swarm-core/src/hostPool.ts"() {
610
+ "use strict";
611
+ init_hosts();
612
+ }
613
+ });
614
+
606
615
  // ../swarm-core/src/hostCommands.ts
607
- var HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS;
616
+ var HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, RUN_SCOPED_COMMAND_KINDS, RUN_SCOPED;
608
617
  var init_hostCommands = __esm({
609
618
  "../swarm-core/src/hostCommands.ts"() {
610
619
  "use strict";
@@ -613,6 +622,8 @@ var init_hostCommands = __esm({
613
622
  HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
614
623
  HOST_COMMAND_TTL_MS = 30 * 6e4;
615
624
  HOST_COMMAND_KEEPALIVE_MS = 10 * 6e4;
625
+ RUN_SCOPED_COMMAND_KINDS = ["stop_run", "wake_run"];
626
+ RUN_SCOPED = new Set(RUN_SCOPED_COMMAND_KINDS);
616
627
  }
617
628
  });
618
629
 
@@ -624,6 +635,420 @@ var init_relief = __esm({
624
635
  }
625
636
  });
626
637
 
638
+ // ../swarm-core/src/cost.ts
639
+ var EMPTY_TASK_COST;
640
+ var init_cost = __esm({
641
+ "../swarm-core/src/cost.ts"() {
642
+ "use strict";
643
+ EMPTY_TASK_COST = Object.freeze({
644
+ costUsd: 0,
645
+ tokensIn: 0,
646
+ tokensOut: 0,
647
+ durationMs: 0,
648
+ runs: 0,
649
+ respawns: 0
650
+ });
651
+ }
652
+ });
653
+
654
+ // ../swarm-core/src/runGuards.ts
655
+ var DEFAULT_RUN_LIMITS;
656
+ var init_runGuards = __esm({
657
+ "../swarm-core/src/runGuards.ts"() {
658
+ "use strict";
659
+ DEFAULT_RUN_LIMITS = {
660
+ maxCostUsd: 20,
661
+ runTimeoutMs: 8 * 60 * 6e4,
662
+ stepTimeoutMs: 90 * 6e4,
663
+ lostMs: 30 * 6e4
664
+ };
665
+ }
666
+ });
667
+
668
+ // ../swarm-core/src/codeZones.ts
669
+ function hasZoneModel(m) {
670
+ return m.tasks > 0 && m.zones.length > 0;
671
+ }
672
+ function testFileFeature(basename5) {
673
+ const head = basename5.replace(/\.(test|spec)\.[a-z]+$/i, "").split(".")[0].toLowerCase();
674
+ return head && !GENERIC_DIRS.has(head) ? head : null;
675
+ }
676
+ function pathZoneCandidate(path) {
677
+ const parts = path.split("/").filter(Boolean);
678
+ if (parts.length <= 1) return "root";
679
+ if (WORKSPACE_DIRS.has(parts[0]) && parts.length >= 3) {
680
+ const pkg = parts[1];
681
+ if (parts[2] === "src" && parts.length >= 5) {
682
+ const feature = parts[3];
683
+ if (!GENERIC_DIRS.has(feature)) return feature;
684
+ }
685
+ if (GENERIC_TEST_DIRS.has(parts[2])) {
686
+ const feature = testFileFeature(parts[parts.length - 1]);
687
+ if (feature) return feature;
688
+ }
689
+ return pkg;
690
+ }
691
+ if (parts[0] === "src" && parts.length >= 3 && !GENERIC_DIRS.has(parts[1])) return parts[1];
692
+ return parts[0];
693
+ }
694
+ function tokenize(text) {
695
+ const out = [];
696
+ for (const raw of text.split(/[^\p{L}\p{N}]+/u)) {
697
+ if (!raw) continue;
698
+ for (const piece of raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")) {
699
+ const t = piece.toLowerCase();
700
+ if (t.length < 3 || STOP_TERMS.has(t) || /^\d+$/.test(t)) continue;
701
+ out.push(t);
702
+ }
703
+ }
704
+ return out;
705
+ }
706
+ function normalizeZoneName(raw) {
707
+ if (!raw) return null;
708
+ const s = raw.trim().toLowerCase().replace(/^area:/, "");
709
+ return s || null;
710
+ }
711
+ function bump(m, k, by = 1) {
712
+ m.set(k, (m.get(k) ?? 0) + by);
713
+ }
714
+ function topKey(m) {
715
+ let best = null;
716
+ let bestN = 0;
717
+ let total = 0;
718
+ for (const [k, n] of m) {
719
+ total += n;
720
+ if (n > bestN) {
721
+ best = k;
722
+ bestN = n;
723
+ }
724
+ }
725
+ return best === null || total === 0 ? null : { key: best, count: bestN, share: bestN / total };
726
+ }
727
+ function buildZoneModel(entries, opts = {}) {
728
+ const o = { ...DEFAULTS, ...opts };
729
+ const tasks = entries.filter((e) => e.files.length > 0);
730
+ if (tasks.length < o.minTasks) return EMPTY_ZONE_MODEL;
731
+ const fileTasks = /* @__PURE__ */ new Map();
732
+ const candidate = /* @__PURE__ */ new Map();
733
+ for (const t of tasks) {
734
+ for (const f of new Set(t.files)) {
735
+ bump(fileTasks, f);
736
+ if (!candidate.has(f)) candidate.set(f, pathZoneCandidate(f));
737
+ }
738
+ }
739
+ const zoneOf = new Map(candidate);
740
+ for (const [file, seen] of fileTasks) {
741
+ if (seen < 2) continue;
742
+ const votes = /* @__PURE__ */ new Map();
743
+ for (const t of tasks) {
744
+ const files = [...new Set(t.files)];
745
+ if (!files.includes(file)) continue;
746
+ const others = files.filter((f) => f !== file);
747
+ if (!others.length) continue;
748
+ for (const f of others) bump(votes, candidate.get(f), 1 / others.length);
749
+ }
750
+ const top = topKey(votes);
751
+ if (top && top.share >= 0.6 && top.key !== candidate.get(file)) zoneOf.set(file, top.key);
752
+ }
753
+ const voteWeight = (file) => 1 / Math.log2(2 + (fileTasks.get(file) ?? 1));
754
+ const taskZonePre = /* @__PURE__ */ new Map();
755
+ for (const t of tasks) {
756
+ const votes = /* @__PURE__ */ new Map();
757
+ for (const f of new Set(t.files)) bump(votes, zoneOf.get(f), voteWeight(f));
758
+ const top = topKey(votes);
759
+ if (top) taskZonePre.set(t.slug, top.key);
760
+ }
761
+ const fileZones = /* @__PURE__ */ new Map();
762
+ for (const t of tasks) {
763
+ const tz = taskZonePre.get(t.slug);
764
+ if (!tz) continue;
765
+ for (const f of new Set(t.files)) {
766
+ let m = fileZones.get(f);
767
+ if (!m) fileZones.set(f, m = /* @__PURE__ */ new Map());
768
+ bump(m, tz);
769
+ }
770
+ }
771
+ const spineSet = /* @__PURE__ */ new Set();
772
+ for (const [file, seen] of fileTasks) {
773
+ if (seen < o.spineMinTasks) continue;
774
+ const zonesOfFile = fileZones.get(file);
775
+ const dominant = zonesOfFile ? topKey(zonesOfFile) : null;
776
+ if ((zonesOfFile?.size ?? 0) < o.spineMinZones) continue;
777
+ if (dominant && dominant.share >= o.spineDominantShare) continue;
778
+ spineSet.add(file);
779
+ }
780
+ const fileZone = /* @__PURE__ */ new Map();
781
+ for (const [f, z] of zoneOf) if (!spineSet.has(f)) fileZone.set(f, z);
782
+ const taskZone = /* @__PURE__ */ new Map();
783
+ for (const t of tasks) {
784
+ const votes = /* @__PURE__ */ new Map();
785
+ for (const f of new Set(t.files)) {
786
+ const z = fileZone.get(f);
787
+ if (z) bump(votes, z, voteWeight(f));
788
+ }
789
+ const top = topKey(votes);
790
+ taskZone.set(t.slug, top ? top.key : SPINE_ZONE);
791
+ }
792
+ const labelVotes = /* @__PURE__ */ new Map();
793
+ for (const t of tasks) {
794
+ const manual = normalizeZoneName(t.area);
795
+ const z = taskZone.get(t.slug);
796
+ if (!manual || !z || z === SPINE_ZONE || manual === SPINE_ZONE) continue;
797
+ let m = labelVotes.get(z);
798
+ if (!m) labelVotes.set(z, m = /* @__PURE__ */ new Map());
799
+ bump(m, manual);
800
+ }
801
+ const rename = /* @__PURE__ */ new Map();
802
+ const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
803
+ const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
804
+ const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
805
+ return sum(b[1]) - sum(a[1]) || a[0].localeCompare(b[0]);
806
+ });
807
+ for (const [zone, votes] of clustersByEvidence) {
808
+ const top = topKey(votes);
809
+ if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
810
+ rename.set(zone, top.key);
811
+ takenNames.add(top.key);
812
+ }
813
+ const renamed = (z) => rename.get(z) ?? z;
814
+ if (rename.size) {
815
+ for (const [f, z] of fileZone) fileZone.set(f, renamed(z));
816
+ for (const [s, z] of taskZone) taskZone.set(s, renamed(z));
817
+ }
818
+ const zoneTerms = /* @__PURE__ */ new Map();
819
+ const addTerm = (zone, term, w) => {
820
+ let m = zoneTerms.get(zone);
821
+ if (!m) zoneTerms.set(zone, m = /* @__PURE__ */ new Map());
822
+ bump(m, term, w);
823
+ };
824
+ for (const [f, z] of fileZone) for (const term of tokenize(f)) addTerm(z, term, 1);
825
+ for (const f of spineSet) for (const term of tokenize(f)) addTerm(SPINE_ZONE, term, 1);
826
+ for (const t of tasks) {
827
+ const z = taskZone.get(t.slug);
828
+ if (!z || !t.text) continue;
829
+ for (const term of tokenize(t.text)) addTerm(z, term, 2);
830
+ }
831
+ const termZones = /* @__PURE__ */ new Map();
832
+ for (const terms of zoneTerms.values()) for (const term of terms.keys()) bump(termZones, term);
833
+ const zoneFiles = /* @__PURE__ */ new Map();
834
+ for (const [f, z] of fileZone) {
835
+ const arr = zoneFiles.get(z) ?? [];
836
+ arr.push(f);
837
+ zoneFiles.set(z, arr);
838
+ }
839
+ const zoneTaskCount = /* @__PURE__ */ new Map();
840
+ for (const z of taskZone.values()) bump(zoneTaskCount, z);
841
+ const byFreq = (a, b) => (fileTasks.get(b) ?? 0) - (fileTasks.get(a) ?? 0) || a.localeCompare(b);
842
+ 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));
843
+ const spine = [...spineSet].sort(byFreq);
844
+ if (spineSet.size) zones.push({ zone: SPINE_ZONE, tasks: zoneTaskCount.get(SPINE_ZONE) ?? 0, files: spine });
845
+ const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
846
+ return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
847
+ }
848
+ function zoneForFiles(model, files) {
849
+ if (!hasZoneModel(model) || !files.length) return null;
850
+ const spineSet = new Set(model.spine);
851
+ const touchedSpine = files.filter((f) => spineSet.has(f));
852
+ const votes = /* @__PURE__ */ new Map();
853
+ for (const f of new Set(files)) {
854
+ const z = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
855
+ if (z) bump(votes, z);
856
+ }
857
+ const top = topKey(votes);
858
+ if (!top) {
859
+ return {
860
+ zone: SPINE_ZONE,
861
+ confidence: 1,
862
+ 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(", ")})`,
863
+ spine: true
864
+ };
865
+ }
866
+ return {
867
+ zone: top.key,
868
+ confidence: top.share,
869
+ 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}`,
870
+ spine: touchedSpine.length > 0
871
+ };
872
+ }
873
+ function suggestZonesFromText(model, text, limit = 3) {
874
+ if (!hasZoneModel(model)) return [];
875
+ const terms = tokenize(text);
876
+ if (!terms.length) return [];
877
+ const zoneCount = model.zoneTerms.size || 1;
878
+ const idf = (term) => Math.log(1 + zoneCount / (model.termZones.get(term) ?? 1));
879
+ const scores = /* @__PURE__ */ new Map();
880
+ const hits = /* @__PURE__ */ new Map();
881
+ const tasksOfZone = new Map(model.zones.map((z) => [z.zone, z.tasks]));
882
+ for (const [zone, dict] of model.zoneTerms) {
883
+ if ((tasksOfZone.get(zone) ?? 0) < 2) continue;
884
+ let dot = 0;
885
+ let norm = 0;
886
+ const matched = [];
887
+ for (const [term, w] of dict) {
888
+ const v = Math.log(1 + w) * idf(term);
889
+ norm += v * v;
890
+ }
891
+ if (!norm) continue;
892
+ for (const term of new Set(terms)) {
893
+ const w = dict.get(term);
894
+ if (!w) continue;
895
+ dot += Math.log(1 + w) * idf(term) * idf(term);
896
+ matched.push(term);
897
+ }
898
+ const score = dot / Math.sqrt(norm) * Math.log(1 + (tasksOfZone.get(zone) ?? 1));
899
+ if (score > 0) {
900
+ scores.set(zone, score);
901
+ hits.set(zone, matched);
902
+ }
903
+ }
904
+ const total = [...scores.values()].reduce((a, b) => a + b, 0);
905
+ if (!total) return [];
906
+ return [...scores.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([zone, s]) => ({
907
+ zone,
908
+ confidence: s / total,
909
+ why: `\u0441\u043E\u0432\u043F\u0430\u043B\u043E \u043F\u043E \u0441\u043B\u043E\u0432\u0430\u043C: ${(hits.get(zone) ?? []).slice(0, 5).join(", ")}`,
910
+ spine: zone === SPINE_ZONE
911
+ }));
912
+ }
913
+ function similarTasks(model, text, limit = 5) {
914
+ const terms = new Set(tokenize(text));
915
+ if (!terms.size) return [];
916
+ const scored = [];
917
+ for (const [i, e] of model.entries.entries()) {
918
+ const other = model.entryTerms[i];
919
+ if (!other?.size) continue;
920
+ let inter = 0;
921
+ for (const t of terms) if (other.has(t)) inter += Math.log(1 + (model.zoneTerms.size || 1) / (model.termZones.get(t) ?? 1));
922
+ if (inter <= 0) continue;
923
+ scored.push({ entry: e, score: inter / Math.sqrt(terms.size * other.size) });
924
+ }
925
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit);
926
+ }
927
+ function predictFiles(model, opts) {
928
+ if (!hasZoneModel(model)) return [];
929
+ const limit = opts.limit ?? 40;
930
+ const best = /* @__PURE__ */ new Map();
931
+ const put = (path, weight, why) => {
932
+ const cur = best.get(path);
933
+ if (!cur || cur.weight < weight) best.set(path, { path, weight, why });
934
+ };
935
+ if (opts.text) {
936
+ const sim = similarTasks(model, opts.text, opts.similar ?? 5);
937
+ const topScore = sim[0]?.score ?? 0;
938
+ for (const { entry, score } of sim) {
939
+ const w = topScore > 0 ? Math.min(1, score / topScore) : 0;
940
+ 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}`);
941
+ }
942
+ }
943
+ const zone = normalizeZoneName(opts.zone) ?? (opts.text ? suggestZonesFromText(model, opts.text)[0]?.zone ?? null : null);
944
+ if (zone) {
945
+ const stat = model.zones.find((z) => z.zone === zone);
946
+ 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}`));
947
+ }
948
+ return [...best.values()].sort((a, b) => b.weight - a.weight || a.path.localeCompare(b.path)).slice(0, limit);
949
+ }
950
+ function manualIsBroader(manual, files) {
951
+ if (!files.length) return false;
952
+ const withSegment = files.filter((f) => f.split("/").some((seg) => seg.toLowerCase() === manual)).length;
953
+ return withSegment / files.length >= 0.6;
954
+ }
955
+ function checkZoneAgreement(model, entries = model.entries) {
956
+ const rows = [];
957
+ for (const e of entries) {
958
+ const manual = normalizeZoneName(e.area);
959
+ if (!manual) continue;
960
+ const guess = zoneForFiles(model, e.files);
961
+ const suggested = guess?.zone ?? null;
962
+ const verdict = suggested === manual ? "match" : suggested && manualIsBroader(manual, e.files) ? "compatible" : "mismatch";
963
+ rows.push({ slug: e.slug, manual, suggested, match: verdict === "match", verdict });
964
+ }
965
+ const matched = rows.filter((r) => r.verdict === "match").length;
966
+ const compatible = rows.filter((r) => r.verdict === "compatible").length;
967
+ const n = rows.length;
968
+ return { checked: n, matched, compatible, rate: n ? matched / n : 0, looseRate: n ? (matched + compatible) / n : 0, rows };
969
+ }
970
+ var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS;
971
+ var init_codeZones = __esm({
972
+ "../swarm-core/src/codeZones.ts"() {
973
+ "use strict";
974
+ SPINE_ZONE = "core";
975
+ DEFAULTS = {
976
+ minTasks: 5,
977
+ spineMinTasks: 3,
978
+ spineMinZones: 3,
979
+ spineDominantShare: 0.6
980
+ };
981
+ EMPTY_ZONE_MODEL = {
982
+ tasks: 0,
983
+ zones: [],
984
+ fileZone: /* @__PURE__ */ new Map(),
985
+ spine: [],
986
+ taskZone: /* @__PURE__ */ new Map(),
987
+ zoneTerms: /* @__PURE__ */ new Map(),
988
+ termZones: /* @__PURE__ */ new Map(),
989
+ entries: [],
990
+ entryTerms: []
991
+ };
992
+ GENERIC_DIRS = /* @__PURE__ */ new Set([
993
+ "src",
994
+ "source",
995
+ "lib",
996
+ "libs",
997
+ "util",
998
+ "utils",
999
+ "common",
1000
+ "internal",
1001
+ "app",
1002
+ "test",
1003
+ "tests",
1004
+ "__tests__",
1005
+ "spec",
1006
+ "specs",
1007
+ "index"
1008
+ ]);
1009
+ WORKSPACE_DIRS = /* @__PURE__ */ new Set(["packages", "apps", "services", "libs", "modules"]);
1010
+ GENERIC_TEST_DIRS = /* @__PURE__ */ new Set(["tests", "test", "__tests__", "spec"]);
1011
+ STOP_TERMS = /* @__PURE__ */ new Set([
1012
+ "the",
1013
+ "and",
1014
+ "for",
1015
+ "with",
1016
+ "from",
1017
+ "into",
1018
+ "src",
1019
+ "test",
1020
+ "tests",
1021
+ "index",
1022
+ "packages",
1023
+ "package",
1024
+ "json",
1025
+ "yml",
1026
+ "yaml",
1027
+ "tsx",
1028
+ "mjs",
1029
+ "cjs",
1030
+ "\u0447\u0442\u043E",
1031
+ "\u043A\u0430\u043A",
1032
+ "\u0434\u043B\u044F",
1033
+ "\u043F\u0440\u0438",
1034
+ "\u044D\u0442\u043E",
1035
+ "\u0438\u043B\u0438",
1036
+ "\u0435\u0441\u043B\u0438",
1037
+ "\u0447\u0442\u043E\u0431\u044B",
1038
+ "\u0442\u043E\u0436\u0435",
1039
+ "\u0435\u0433\u043E",
1040
+ "\u0432\u0441\u0435",
1041
+ "feat",
1042
+ "fix",
1043
+ "chore",
1044
+ "docs",
1045
+ "refactor",
1046
+ "\u043F\u0435\u0440\u0444",
1047
+ "wip"
1048
+ ]);
1049
+ }
1050
+ });
1051
+
627
1052
  // ../swarm-core/src/index.ts
628
1053
  var init_src = __esm({
629
1054
  "../swarm-core/src/index.ts"() {
@@ -640,15 +1065,489 @@ var init_src = __esm({
640
1065
  init_names();
641
1066
  init_baseBranch();
642
1067
  init_hosts();
1068
+ init_hostPool();
643
1069
  init_hostCommands();
644
1070
  init_relief();
1071
+ init_cost();
1072
+ init_runGuards();
1073
+ init_codeZones();
645
1074
  }
646
1075
  });
647
1076
 
648
- // src/config.ts
1077
+ // src/claudeBin.ts
1078
+ import { existsSync, readdirSync } from "fs";
1079
+ import { win32 as winPath } from "path";
1080
+ function compareVersionDirs(a, b) {
1081
+ const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
1082
+ const pa = parts(a);
1083
+ const pb = parts(b);
1084
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1085
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1086
+ if (d !== 0) return d;
1087
+ }
1088
+ return 0;
1089
+ }
1090
+ function realListDir(dir) {
1091
+ try {
1092
+ return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1093
+ } catch {
1094
+ return [];
1095
+ }
1096
+ }
1097
+ function winVersionRoots(env, listDir) {
1098
+ const roots = [];
1099
+ const appData = env.APPDATA;
1100
+ if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
1101
+ const localAppData = env.LOCALAPPDATA;
1102
+ if (localAppData) {
1103
+ const packages = winPath.join(localAppData, "Packages");
1104
+ for (const name of listDir(packages)) {
1105
+ if (!name.startsWith("Claude")) continue;
1106
+ roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
1107
+ }
1108
+ }
1109
+ return roots;
1110
+ }
1111
+ function findWindowsClaude(opts = {}) {
1112
+ const env = opts.env ?? process.env;
1113
+ const listDir = opts.listDir ?? realListDir;
1114
+ const fileExists = opts.fileExists ?? existsSync;
1115
+ let best = null;
1116
+ for (const root of winVersionRoots(env, listDir)) {
1117
+ for (const version of listDir(root)) {
1118
+ const candidate = winPath.join(root, version, "claude.exe");
1119
+ if (!fileExists(candidate)) continue;
1120
+ if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
1121
+ }
1122
+ }
1123
+ return best?.path ?? null;
1124
+ }
1125
+ function resolveClaudeBin(opts = {}) {
1126
+ const env = opts.env ?? process.env;
1127
+ const platform = opts.platform ?? process.platform;
1128
+ const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
1129
+ if (explicit) return explicit;
1130
+ if (platform !== "win32") return CLAUDE_FALLBACK;
1131
+ if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
1132
+ windowsScanDone = true;
1133
+ const found = findWindowsClaude({ ...opts, env });
1134
+ cachedWindowsBin = found;
1135
+ if (found) return found;
1136
+ if (!warnedNoWindowsClaude) {
1137
+ warnedNoWindowsClaude = true;
1138
+ console.warn(
1139
+ "[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."
1140
+ );
1141
+ }
1142
+ return CLAUDE_FALLBACK;
1143
+ }
1144
+ var CLAUDE_FALLBACK, warnedNoWindowsClaude, cachedWindowsBin, windowsScanDone;
1145
+ var init_claudeBin = __esm({
1146
+ "src/claudeBin.ts"() {
1147
+ "use strict";
1148
+ CLAUDE_FALLBACK = "claude";
1149
+ warnedNoWindowsClaude = false;
1150
+ cachedWindowsBin = null;
1151
+ windowsScanDone = false;
1152
+ }
1153
+ });
1154
+
1155
+ // src/pricing.ts
1156
+ function costFromUsage(usage2, model, table = MODEL_PRICING) {
1157
+ const pricing = model ? table[model] : void 0;
1158
+ if (!pricing) {
1159
+ 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` };
1160
+ }
1161
+ const input = usage2.input_tokens ?? 0;
1162
+ const output = usage2.output_tokens ?? 0;
1163
+ const cacheWrite = usage2.cache_creation_input_tokens ?? 0;
1164
+ const cacheRead = usage2.cache_read_input_tokens ?? 0;
1165
+ const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
1166
+ return { costUsd };
1167
+ }
1168
+ var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
1169
+ var init_pricing = __esm({
1170
+ "src/pricing.ts"() {
1171
+ "use strict";
1172
+ MODEL_PRICING = {
1173
+ // Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
1174
+ // Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
1175
+ // ветвление по времени внутри расчёта стоимости.
1176
+ "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
1177
+ "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
1178
+ // Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
1179
+ "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
1180
+ "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
1181
+ };
1182
+ CACHE_READ_FACTOR = 0.1;
1183
+ CACHE_WRITE_5M_FACTOR = 1.25;
1184
+ }
1185
+ });
1186
+
1187
+ // src/claudeResult.ts
1188
+ function parseClaudeResultJson(raw) {
1189
+ const trimmed = raw.trim();
1190
+ if (!trimmed) return null;
1191
+ try {
1192
+ const parsed = JSON.parse(trimmed);
1193
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
1194
+ } catch {
1195
+ return null;
1196
+ }
1197
+ }
1198
+ function launchCostUsd(result, model) {
1199
+ if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
1200
+ return { costUsd: result.total_cost_usd };
1201
+ }
1202
+ if (!result.usage) return { costUsd: 0 };
1203
+ return costFromUsage(result.usage, model);
1204
+ }
1205
+ function launchSpend(result) {
1206
+ const u = result.usage ?? {};
1207
+ const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1208
+ return {
1209
+ tokensIn: num(u.input_tokens) + num(u.cache_creation_input_tokens) + num(u.cache_read_input_tokens),
1210
+ tokensOut: num(u.output_tokens),
1211
+ durationMs: num(result.duration_ms)
1212
+ };
1213
+ }
1214
+ var init_claudeResult = __esm({
1215
+ "src/claudeResult.ts"() {
1216
+ "use strict";
1217
+ init_pricing();
1218
+ }
1219
+ });
1220
+
1221
+ // src/executors/claudeCode.ts
1222
+ import { readdirSync as readdirSync2, statSync, writeFileSync } from "fs";
649
1223
  import { homedir } from "os";
650
- import { basename, join, resolve } from "path";
651
- import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "fs";
1224
+ import { join } from "path";
1225
+ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
1226
+ const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
1227
+ const base3 = [
1228
+ "--dangerously-skip-permissions",
1229
+ ...ISOLATION_FLAGS,
1230
+ "--mcp-config",
1231
+ cfgPath,
1232
+ ...settings ? ["--settings", settings] : [],
1233
+ ...pluginFlags,
1234
+ "--output-format",
1235
+ "json",
1236
+ "-p",
1237
+ prompt
1238
+ ];
1239
+ const withModel = model ? ["--model", model, ...base3] : base3;
1240
+ return continueSession ? ["--continue", ...withModel] : withModel;
1241
+ }
1242
+ function droneDenyRules(paths) {
1243
+ const abs = (p) => `//${p.replace(/^\/+/, "")}`;
1244
+ return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
1245
+ }
1246
+ function droneSettingsJson(paths) {
1247
+ return JSON.stringify({ permissions: { deny: droneDenyRules(paths) } });
1248
+ }
1249
+ function transcriptDir(worktree, projectsDir) {
1250
+ return join(projectsDir, worktree.replace(/[/.]/g, "-"));
1251
+ }
1252
+ function sessionTranscriptExists(worktree, projectsDir = join(homedir(), ".claude", "projects")) {
1253
+ try {
1254
+ return readdirSync2(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
1255
+ } catch {
1256
+ return false;
1257
+ }
1258
+ }
1259
+ function transcriptMtimeMs(worktree, projectsDir = join(homedir(), ".claude", "projects")) {
1260
+ const dir = transcriptDir(worktree, projectsDir);
1261
+ let files;
1262
+ try {
1263
+ files = readdirSync2(dir);
1264
+ } catch {
1265
+ return null;
1266
+ }
1267
+ let latest = null;
1268
+ for (const f of files) {
1269
+ if (!f.endsWith(".jsonl")) continue;
1270
+ try {
1271
+ const mtime2 = statSync(join(dir, f)).mtimeMs;
1272
+ if (latest === null || mtime2 > latest) latest = mtime2;
1273
+ } catch {
1274
+ }
1275
+ }
1276
+ return latest;
1277
+ }
1278
+ function writeMcpConfig(spec) {
1279
+ const cfg = {
1280
+ mcpServers: {
1281
+ [spec.mcp.name]: { command: spec.mcp.command, args: spec.mcp.args, env: spec.mcp.env }
1282
+ }
1283
+ };
1284
+ const path = join(spec.worktree, MCP_CONFIG_FILE);
1285
+ writeFileSync(path, JSON.stringify(cfg, null, 2));
1286
+ return path;
1287
+ }
1288
+ var ISOLATION_FLAGS, MCP_CONFIG_FILE, claudeCodeExecutor;
1289
+ var init_claudeCode = __esm({
1290
+ "src/executors/claudeCode.ts"() {
1291
+ "use strict";
1292
+ init_claudeBin();
1293
+ init_claudeResult();
1294
+ ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
1295
+ MCP_CONFIG_FILE = ".swarm-mcp.json";
1296
+ claudeCodeExecutor = {
1297
+ id: "claude-code",
1298
+ title: "Claude Code (claude)",
1299
+ binEnv: "SWARM_CLAUDE_BIN",
1300
+ 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" },
1301
+ resolveBin: () => resolveClaudeBin(),
1302
+ // Стрелка, а не метод: `plan` могут вызвать в отрыве от объекта (деструктуризация в тестах,
1303
+ // проброс колбэком), и `this` тогда уедет в undefined.
1304
+ plan: (spec) => {
1305
+ const cfgPath = writeMcpConfig(spec);
1306
+ const settings = spec.denyPaths.length > 0 ? droneSettingsJson(spec.denyPaths) : void 0;
1307
+ return {
1308
+ bin: resolveClaudeBin(),
1309
+ args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings)
1310
+ };
1311
+ },
1312
+ parseResult(raw, model) {
1313
+ const result = parseClaudeResultJson(raw);
1314
+ if (!result) return null;
1315
+ const est = launchCostUsd(result, model);
1316
+ const spend = launchSpend(result);
1317
+ return {
1318
+ costUsd: est.costUsd,
1319
+ ...est.warning ? { warning: est.warning } : {},
1320
+ tokensIn: spend.tokensIn,
1321
+ tokensOut: spend.tokensOut,
1322
+ durationMs: spend.durationMs
1323
+ };
1324
+ },
1325
+ session: {
1326
+ exists: (worktree) => sessionTranscriptExists(worktree),
1327
+ mtimeMs: (worktree) => transcriptMtimeMs(worktree)
1328
+ },
1329
+ // `claude -p "ok" --max-turns 1` (TASWM-23) — прямее, чем HEAD на статус-хост: дёргает ТОТ
1330
+ // ЖЕ путь (auth + реальный ответ модели), что и дрон. ISOLATION_FLAGS те же, что у дрона
1331
+ // (TASWM-90): SessionStart-хуки юзера могли бы подвесить пробу и дать ложный шторм.
1332
+ probe: {
1333
+ args: [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"],
1334
+ // TASWM-214: исчерпанный лимит подписки — НЕ шторм провайдера, и путать их нельзя.
1335
+ limitPattern: /(weekly limit|usage limit|hit your .*limit)/i
1336
+ }
1337
+ };
1338
+ }
1339
+ });
1340
+
1341
+ // src/swarmHome.ts
1342
+ import { homedir as homedir2 } from "os";
1343
+ import { join as join2 } from "path";
1344
+ import { mkdirSync } from "fs";
1345
+ var SWARM_HOME;
1346
+ var init_swarmHome = __esm({
1347
+ "src/swarmHome.ts"() {
1348
+ "use strict";
1349
+ SWARM_HOME = process.env.SWARM_HOME || join2(homedir2(), ".taskless-swarm");
1350
+ mkdirSync(SWARM_HOME, { recursive: true });
1351
+ }
1352
+ });
1353
+
1354
+ // src/executors/codex.ts
1355
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync3, statSync as statSync2, symlinkSync } from "fs";
1356
+ import { join as join3 } from "path";
1357
+ function resolveCodexBin(env = process.env) {
1358
+ return (env.SWARM_CODEX_BIN ?? "").trim() || "codex";
1359
+ }
1360
+ function codexHomePath(worktree, swarmHome = SWARM_HOME) {
1361
+ return join3(swarmHome, "executor-state", "codex", worktree.replace(/[/.]/g, "-"));
1362
+ }
1363
+ function ensureCodexHome(worktree, realHome, env = process.env, swarmHome = SWARM_HOME) {
1364
+ const home = codexHomePath(worktree, swarmHome);
1365
+ mkdirSync2(home, { recursive: true });
1366
+ const real = (env.CODEX_HOME ?? "").trim() || join3(realHome, ".codex");
1367
+ for (const file of ["auth.json", "config.toml"]) {
1368
+ const target = join3(real, file);
1369
+ const link = join3(home, file);
1370
+ if (!existsSync2(target) || existsSync2(link)) continue;
1371
+ try {
1372
+ symlinkSync(target, link);
1373
+ } catch {
1374
+ }
1375
+ }
1376
+ return home;
1377
+ }
1378
+ function tomlValue(v) {
1379
+ if (typeof v === "string") {
1380
+ const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
1381
+ return `"${escaped}"`;
1382
+ }
1383
+ if (Array.isArray(v)) return `[${v.map((x) => tomlValue(x)).join(", ")}]`;
1384
+ return `{${Object.entries(v).map(([k, val]) => `${k} = ${tomlValue(val)}`).join(", ")}}`;
1385
+ }
1386
+ function mcpConfigArgs(spec) {
1387
+ const inline = `{command = ${tomlValue(spec.mcp.command)}, args = ${tomlValue(spec.mcp.args)}, env = ${tomlValue(spec.mcp.env)}}`;
1388
+ return ["-c", `mcp_servers.${spec.mcp.name}=${inline}`];
1389
+ }
1390
+ function skillsNotice(skillDirs) {
1391
+ if (skillDirs.length === 0) return "";
1392
+ 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.";
1393
+ }
1394
+ function readUsage(node) {
1395
+ if (typeof node !== "object" || node === null) return null;
1396
+ const rec = node;
1397
+ const direct = rec.total_token_usage ?? rec.usage ?? rec.token_usage;
1398
+ if (direct && typeof direct === "object") return direct;
1399
+ const info = rec.info;
1400
+ if (info && typeof info === "object") return readUsage(info);
1401
+ return null;
1402
+ }
1403
+ function parseCodexStream(raw) {
1404
+ let usage2 = null;
1405
+ let completed = false;
1406
+ for (const line of raw.split("\n")) {
1407
+ const trimmed = line.trim();
1408
+ if (!trimmed) continue;
1409
+ let parsed;
1410
+ try {
1411
+ parsed = JSON.parse(trimmed);
1412
+ } catch {
1413
+ continue;
1414
+ }
1415
+ if (typeof parsed !== "object" || parsed === null) continue;
1416
+ const rec = parsed;
1417
+ const msg = typeof rec.msg === "object" && rec.msg !== null ? rec.msg : rec;
1418
+ const type = typeof msg.type === "string" ? msg.type : null;
1419
+ const found = readUsage(msg);
1420
+ if (found) usage2 = found;
1421
+ if (type && TERMINAL_EVENTS.has(type)) completed = true;
1422
+ }
1423
+ return { usage: usage2, completed };
1424
+ }
1425
+ function newestRolloutMtime(dir, depth = 4) {
1426
+ let entries;
1427
+ try {
1428
+ entries = readdirSync3(dir);
1429
+ } catch {
1430
+ return null;
1431
+ }
1432
+ let latest = null;
1433
+ for (const name of entries) {
1434
+ const full = join3(dir, name);
1435
+ let st;
1436
+ try {
1437
+ st = statSync2(full);
1438
+ } catch {
1439
+ continue;
1440
+ }
1441
+ if (st.isDirectory()) {
1442
+ if (depth <= 0) continue;
1443
+ const nested = newestRolloutMtime(full, depth - 1);
1444
+ if (nested !== null && (latest === null || nested > latest)) latest = nested;
1445
+ } else if (name.endsWith(".jsonl")) {
1446
+ if (latest === null || st.mtimeMs > latest) latest = st.mtimeMs;
1447
+ }
1448
+ }
1449
+ return latest;
1450
+ }
1451
+ var BYPASS_FLAG, SKIP_GIT_CHECK_FLAG, CODEX_MODEL_PRICING, TERMINAL_EVENTS, codexExecutor;
1452
+ var init_codex = __esm({
1453
+ "src/executors/codex.ts"() {
1454
+ "use strict";
1455
+ init_swarmHome();
1456
+ init_pricing();
1457
+ BYPASS_FLAG = "--dangerously-bypass-approvals-and-sandbox";
1458
+ SKIP_GIT_CHECK_FLAG = "--skip-git-repo-check";
1459
+ CODEX_MODEL_PRICING = {};
1460
+ TERMINAL_EVENTS = /* @__PURE__ */ new Set(["task_complete", "turn.completed", "thread.completed", "session.completed"]);
1461
+ codexExecutor = {
1462
+ id: "codex",
1463
+ title: "Codex CLI (codex)",
1464
+ binEnv: "SWARM_CODEX_BIN",
1465
+ 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" },
1466
+ resolveBin: () => resolveCodexBin(),
1467
+ // Стрелка, а не метод — см. тот же комментарий в claudeCode.ts.
1468
+ plan: (spec) => {
1469
+ const notice = skillsNotice(spec.skillDirs);
1470
+ const prompt = notice ? `${spec.prompt}
1471
+
1472
+ ${notice}` : spec.prompt;
1473
+ const args = [
1474
+ "exec",
1475
+ // Продолжение оборванной сессии — ПОДКОМАНДА, а не флаг: `exec resume --last` берёт
1476
+ // последнюю сессию этого CODEX_HOME (то есть этого worktree, см. codexHome).
1477
+ ...spec.continueSession ? ["resume", "--last"] : [],
1478
+ "--json",
1479
+ SKIP_GIT_CHECK_FLAG,
1480
+ BYPASS_FLAG,
1481
+ ...spec.model ? ["-m", spec.model] : [],
1482
+ ...mcpConfigArgs(spec),
1483
+ prompt
1484
+ ];
1485
+ return { bin: resolveCodexBin(), args, env: { CODEX_HOME: ensureCodexHome(spec.worktree, spec.realHome) } };
1486
+ },
1487
+ parseResult(raw, model) {
1488
+ const { usage: usage2, completed } = parseCodexStream(raw);
1489
+ if (!completed) return null;
1490
+ if (!usage2) 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" };
1491
+ const cached = usage2.cached_input_tokens ?? 0;
1492
+ const common = {
1493
+ input_tokens: Math.max(0, (usage2.input_tokens ?? 0) - cached),
1494
+ output_tokens: usage2.output_tokens ?? 0,
1495
+ cache_read_input_tokens: cached
1496
+ };
1497
+ const est = costFromUsage(common, model, CODEX_MODEL_PRICING);
1498
+ return est.warning ? { costUsd: est.costUsd, warning: est.warning } : { costUsd: est.costUsd };
1499
+ },
1500
+ session: {
1501
+ exists: (worktree) => newestRolloutMtime(join3(codexHomePath(worktree), "sessions")) !== null,
1502
+ mtimeMs: (worktree) => newestRolloutMtime(join3(codexHomePath(worktree), "sessions"))
1503
+ },
1504
+ probe: {
1505
+ // Тот же принцип, что у claude: дешёвый реальный прогон — он проверяет и auth, и живость
1506
+ // провайдера разом. Один короткий ответ, без тулов и без сети сверх модели.
1507
+ args: ["exec", "--json", SKIP_GIT_CHECK_FLAG, BYPASS_FLAG, "ok"],
1508
+ limitPattern: /(usage limit|rate limit|quota exceeded|insufficient_quota)/i
1509
+ }
1510
+ };
1511
+ }
1512
+ });
1513
+
1514
+ // src/executors/types.ts
1515
+ var init_types2 = __esm({
1516
+ "src/executors/types.ts"() {
1517
+ "use strict";
1518
+ }
1519
+ });
1520
+
1521
+ // src/executors/index.ts
1522
+ function resolveExecutor(id) {
1523
+ const key = (id ?? "").trim();
1524
+ if (!key) return EXECUTORS[DEFAULT_EXECUTOR_ID];
1525
+ const found = EXECUTORS[key];
1526
+ if (!found) {
1527
+ 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(", ")}`);
1528
+ }
1529
+ return found;
1530
+ }
1531
+ var DEFAULT_EXECUTOR_ID, EXECUTORS;
1532
+ var init_executors = __esm({
1533
+ "src/executors/index.ts"() {
1534
+ "use strict";
1535
+ init_claudeCode();
1536
+ init_codex();
1537
+ init_types2();
1538
+ init_claudeCode();
1539
+ init_codex();
1540
+ DEFAULT_EXECUTOR_ID = "claude-code";
1541
+ EXECUTORS = {
1542
+ [claudeCodeExecutor.id]: claudeCodeExecutor,
1543
+ [codexExecutor.id]: codexExecutor
1544
+ };
1545
+ }
1546
+ });
1547
+
1548
+ // src/config.ts
1549
+ import { basename, join as join4, resolve } from "path";
1550
+ import { readFileSync, realpathSync, renameSync, writeFileSync as writeFileSync2 } from "fs";
652
1551
  function normalizeRepo(repo) {
653
1552
  const r = (repo ?? "").trim();
654
1553
  if (!r) return "";
@@ -659,7 +1558,7 @@ function normalizeRepo(repo) {
659
1558
  }
660
1559
  }
661
1560
  function droneWorktreePath(worktreesDir, repo, slug) {
662
- return join(worktreesDir, basename(normalizeRepo(repo)), slug);
1561
+ return join4(worktreesDir, basename(normalizeRepo(repo)), slug);
663
1562
  }
664
1563
  function resolveEffectiveWorktreesDir(deps) {
665
1564
  if (deps.envOverride) return deps.envOverride;
@@ -680,33 +1579,35 @@ function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
680
1579
  }
681
1580
  function tokenFromFile() {
682
1581
  try {
683
- return readFileSync(join(SWARM_HOME, "token"), "utf8").trim();
1582
+ return readFileSync(join4(SWARM_HOME, "token"), "utf8").trim();
684
1583
  } catch {
685
1584
  return "";
686
1585
  }
687
1586
  }
688
- 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;
1587
+ 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;
689
1588
  var init_config = __esm({
690
1589
  "src/config.ts"() {
691
1590
  "use strict";
692
1591
  init_src();
693
- SWARM_HOME = process.env.SWARM_HOME || join(homedir(), ".taskless-swarm");
694
- mkdirSync(SWARM_HOME, { recursive: true });
695
- DB_PATH = process.env.SWARM_DB || join(SWARM_HOME, "swarm.db");
696
- DRONE_HOME = process.env.SWARM_DRONE_HOME || join(SWARM_HOME, "drone-home");
1592
+ init_executors();
1593
+ init_swarmHome();
1594
+ init_swarmHome();
1595
+ DB_PATH = process.env.SWARM_DB || join4(SWARM_HOME, "swarm.db");
1596
+ EXECUTOR = resolveExecutor(process.env.SWARM_EXECUTOR);
1597
+ DRONE_HOME = process.env.SWARM_DRONE_HOME || join4(SWARM_HOME, "drone-home");
697
1598
  DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
698
1599
  DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
699
- WORKTREES_NOINDEX_DIR = join(SWARM_HOME, "worktrees.noindex");
700
- WORKTREES_LEGACY_DIR = join(SWARM_HOME, "worktrees");
1600
+ WORKTREES_NOINDEX_DIR = join4(SWARM_HOME, "worktrees.noindex");
1601
+ WORKTREES_LEGACY_DIR = join4(SWARM_HOME, "worktrees");
701
1602
  WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
702
- RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join(SWARM_HOME, "run-logs");
703
- DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join(SWARM_HOME, "overmind.log");
1603
+ RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join4(SWARM_HOME, "run-logs");
1604
+ DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join4(SWARM_HOME, "overmind.log");
704
1605
  DAEMON_LOG_MAX_BYTES = envNum("SWARM_DAEMON_LOG_MAX_BYTES", 16 * 1024 * 1024);
705
1606
  DAEMON_LOG_KEEP = envNum("SWARM_DAEMON_LOG_KEEP", 2);
706
1607
  DAEMON_LOG_CHECK_MS = envNum("SWARM_DAEMON_LOG_CHECK_MS", 6e4);
707
1608
  DIST_BUILD = true;
708
1609
  SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
709
- SWARM_OUTBOX = process.env.SWARM_OUTBOX || join(SWARM_HOME, "outbox.jsonl");
1610
+ SWARM_OUTBOX = process.env.SWARM_OUTBOX || join4(SWARM_HOME, "outbox.jsonl");
710
1611
  SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
711
1612
  SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
712
1613
  SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
@@ -731,15 +1632,15 @@ var init_config = __esm({
731
1632
  DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
732
1633
  DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
733
1634
  DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
734
- DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join(SWARM_HOME, "drone-channel.key");
1635
+ DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join4(SWARM_HOME, "drone-channel.key");
735
1636
  DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
736
- HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join(SWARM_HOME, "overmind.heartbeat");
1637
+ HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join4(SWARM_HOME, "overmind.heartbeat");
737
1638
  HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
738
1639
  SWARM_REPO = process.env.SWARM_REPO || "";
739
1640
  SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
740
1641
  SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
741
1642
  SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
742
- REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join(SWARM_HOME, "repo-map.json");
1643
+ REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join4(SWARM_HOME, "repo-map.json");
743
1644
  SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
744
1645
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
745
1646
  SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
@@ -750,7 +1651,7 @@ var init_config = __esm({
750
1651
  TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
751
1652
  SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
752
1653
  SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
753
- REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join(SWARM_HOME, "review-cutoff.json");
1654
+ REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join4(SWARM_HOME, "review-cutoff.json");
754
1655
  SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
755
1656
  SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
756
1657
  WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
@@ -845,14 +1746,14 @@ var init_storm = __esm({
845
1746
  });
846
1747
 
847
1748
  // src/repoRegistry.ts
848
- import { $ } from "execa";
1749
+ import { $ as $2 } from "execa";
849
1750
  function parseRepoFullName(remoteUrl) {
850
1751
  const m = remoteUrl.trim().match(/^(?:https?:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(\.git)?\/?$/i);
851
1752
  if (!m) return null;
852
1753
  return `${m[1]}/${m[2]}`.toLowerCase();
853
1754
  }
854
1755
  async function resolveRepoFullName(path) {
855
- const res = await $({ reject: false })`git -C ${path} remote get-url origin`;
1756
+ const res = await $2({ reject: false })`git -C ${path} remote get-url origin`;
856
1757
  if (res.exitCode !== 0) return null;
857
1758
  return parseRepoFullName(res.stdout);
858
1759
  }
@@ -875,8 +1776,8 @@ var init_repoRegistry = __esm({
875
1776
  });
876
1777
 
877
1778
  // src/repoScan.ts
878
- import { readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
879
- import { join as join3 } from "path";
1779
+ import { readdirSync as readdirSync5, readFileSync as readFileSync5, statSync as statSync5 } from "fs";
1780
+ import { join as join6 } from "path";
880
1781
  var init_repoScan = __esm({
881
1782
  "src/repoScan.ts"() {
882
1783
  "use strict";
@@ -885,9 +1786,9 @@ var init_repoScan = __esm({
885
1786
  });
886
1787
 
887
1788
  // src/repoDiscovery.ts
888
- import { homedir as homedir2 } from "os";
889
- import { readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "fs";
890
- import { isAbsolute, join as join4, resolve as resolve2 } from "path";
1789
+ import { homedir as homedir3 } from "os";
1790
+ import { readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
1791
+ import { isAbsolute, join as join7, resolve as resolve2 } from "path";
891
1792
  function repoMapLines(rows) {
892
1793
  if (!rows?.length) return [];
893
1794
  const missing = rows.filter((r) => !r.path).length;
@@ -932,7 +1833,7 @@ var init_package = __esm({
932
1833
  "package.json"() {
933
1834
  package_default = {
934
1835
  name: "@taskless-app/swarm",
935
- version: "0.5.0",
1836
+ version: "0.6.0",
936
1837
  type: "module",
937
1838
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
938
1839
  keywords: [
@@ -999,7 +1900,7 @@ var init_version = __esm({
999
1900
 
1000
1901
  // src/droneChannel.ts
1001
1902
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
1002
- import { chmodSync, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
1903
+ import { chmodSync, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
1003
1904
  import { dirname as dirname2 } from "path";
1004
1905
  import { serve } from "@hono/node-server";
1005
1906
  function loadChannelSecret(path) {
@@ -1009,8 +1910,8 @@ function loadChannelSecret(path) {
1009
1910
  } catch {
1010
1911
  }
1011
1912
  const secret = randomBytes(32).toString("hex");
1012
- mkdirSync3(dirname2(path), { recursive: true });
1013
- writeFileSync5(path, secret, { mode: 384 });
1913
+ mkdirSync5(dirname2(path), { recursive: true });
1914
+ writeFileSync6(path, secret, { mode: 384 });
1014
1915
  try {
1015
1916
  chmodSync(path, 384);
1016
1917
  } catch {
@@ -1049,87 +1950,9 @@ var init_droneChannel = __esm({
1049
1950
  }
1050
1951
  });
1051
1952
 
1052
- // src/claudeBin.ts
1053
- import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
1054
- import { win32 as winPath } from "path";
1055
- function compareVersionDirs(a, b) {
1056
- const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
1057
- const pa = parts(a);
1058
- const pb = parts(b);
1059
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1060
- const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1061
- if (d !== 0) return d;
1062
- }
1063
- return 0;
1064
- }
1065
- function realListDir(dir) {
1066
- try {
1067
- return readdirSync3(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1068
- } catch {
1069
- return [];
1070
- }
1071
- }
1072
- function winVersionRoots(env, listDir) {
1073
- const roots = [];
1074
- const appData = env.APPDATA;
1075
- if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
1076
- const localAppData = env.LOCALAPPDATA;
1077
- if (localAppData) {
1078
- const packages = winPath.join(localAppData, "Packages");
1079
- for (const name of listDir(packages)) {
1080
- if (!name.startsWith("Claude")) continue;
1081
- roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
1082
- }
1083
- }
1084
- return roots;
1085
- }
1086
- function findWindowsClaude(opts = {}) {
1087
- const env = opts.env ?? process.env;
1088
- const listDir = opts.listDir ?? realListDir;
1089
- const fileExists = opts.fileExists ?? existsSync5;
1090
- let best = null;
1091
- for (const root of winVersionRoots(env, listDir)) {
1092
- for (const version of listDir(root)) {
1093
- const candidate = winPath.join(root, version, "claude.exe");
1094
- if (!fileExists(candidate)) continue;
1095
- if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
1096
- }
1097
- }
1098
- return best?.path ?? null;
1099
- }
1100
- function resolveClaudeBin(opts = {}) {
1101
- const env = opts.env ?? process.env;
1102
- const platform = opts.platform ?? process.platform;
1103
- const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
1104
- if (explicit) return explicit;
1105
- if (platform !== "win32") return CLAUDE_FALLBACK;
1106
- if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
1107
- windowsScanDone = true;
1108
- const found = findWindowsClaude({ ...opts, env });
1109
- cachedWindowsBin = found;
1110
- if (found) return found;
1111
- if (!warnedNoWindowsClaude) {
1112
- warnedNoWindowsClaude = true;
1113
- console.warn(
1114
- "[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."
1115
- );
1116
- }
1117
- return CLAUDE_FALLBACK;
1118
- }
1119
- var CLAUDE_FALLBACK, warnedNoWindowsClaude, cachedWindowsBin, windowsScanDone;
1120
- var init_claudeBin = __esm({
1121
- "src/claudeBin.ts"() {
1122
- "use strict";
1123
- CLAUDE_FALLBACK = "claude";
1124
- warnedNoWindowsClaude = false;
1125
- cachedWindowsBin = null;
1126
- windowsScanDone = false;
1127
- }
1128
- });
1129
-
1130
1953
  // src/agentToken.ts
1131
1954
  import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3 } from "fs";
1132
- import { join as join11 } from "path";
1955
+ import { join as join14 } from "path";
1133
1956
  function loadAgentToken() {
1134
1957
  try {
1135
1958
  const s = JSON.parse(readFileSync10(STORE_PATH2, "utf8"));
@@ -1175,7 +1998,7 @@ var init_agentToken = __esm({
1175
1998
  "src/agentToken.ts"() {
1176
1999
  "use strict";
1177
2000
  init_config();
1178
- STORE_PATH2 = join11(SWARM_HOME, "agent.json");
2001
+ STORE_PATH2 = join14(SWARM_HOME, "agent.json");
1179
2002
  base2 = (api) => api.replace(/\/$/, "");
1180
2003
  }
1181
2004
  });
@@ -1385,7 +2208,7 @@ __export(db_exports, {
1385
2208
  SwarmDb: () => SwarmDb
1386
2209
  });
1387
2210
  import { createRequire as createRequire2 } from "module";
1388
- import { existsSync as existsSync9 } from "fs";
2211
+ import { existsSync as existsSync10 } from "fs";
1389
2212
  import { resolve as resolve3 } from "path";
1390
2213
  function rowToQuestion(row) {
1391
2214
  return {
@@ -1419,6 +2242,10 @@ function rowToRun(row) {
1419
2242
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
1420
2243
  model: row.model ?? null,
1421
2244
  cost_usd: row.cost_usd ?? 0,
2245
+ // Токены/время работы (TASWM-353) — старые прогоны без колонок → 0.
2246
+ tokens_in: row.tokens_in ?? 0,
2247
+ tokens_out: row.tokens_out ?? 0,
2248
+ duration_ms: row.duration_ms ?? 0,
1422
2249
  pr_url: row.pr_url ?? null,
1423
2250
  superseded_by: row.superseded_by ?? null,
1424
2251
  decision: row.decision ? JSON.parse(row.decision) : null,
@@ -1484,6 +2311,9 @@ CREATE TABLE IF NOT EXISTS runs (
1484
2311
  depends_on TEXT,
1485
2312
  model TEXT,
1486
2313
  cost_usd REAL NOT NULL DEFAULT 0,
2314
+ tokens_in INTEGER NOT NULL DEFAULT 0,
2315
+ tokens_out INTEGER NOT NULL DEFAULT 0,
2316
+ duration_ms INTEGER NOT NULL DEFAULT 0,
1487
2317
  pr_url TEXT,
1488
2318
  superseded_by INTEGER,
1489
2319
  decision TEXT,
@@ -1577,7 +2407,11 @@ CREATE TABLE IF NOT EXISTS imports (
1577
2407
  // Базовая ветка PR (TASWM-285): куда дрон открывает PR и от чего ответвляется.
1578
2408
  "ALTER TABLE runs ADD COLUMN base_branch TEXT",
1579
2409
  // Воркспейс задачи прогона (TASWM-288): им пинятся задачные MCP-вызовы прогона.
1580
- "ALTER TABLE runs ADD COLUMN workspace_id TEXT"
2410
+ "ALTER TABLE runs ADD COLUMN workspace_id TEXT",
2411
+ // Чем обошёлся прогон, кроме денег (TASWM-353): токены вход/выход и время работы.
2412
+ "ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
2413
+ "ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
2414
+ "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0"
1581
2415
  ]) {
1582
2416
  try {
1583
2417
  this.db.exec(alter);
@@ -1870,7 +2704,7 @@ CREATE TABLE IF NOT EXISTS imports (
1870
2704
  const repoN = normalizeRepo(repo);
1871
2705
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
1872
2706
  if (already) return { imported: false, runs: already.run_count };
1873
- if (!existsSync9(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
2707
+ if (!existsSync10(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
1874
2708
  const src = new this.databaseCtor(abs);
1875
2709
  try {
1876
2710
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -2197,8 +3031,8 @@ CREATE TABLE IF NOT EXISTS imports (
2197
3031
  });
2198
3032
 
2199
3033
  // src/platform.ts
2200
- import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
2201
- import { delimiter, join as join16 } from "path";
3034
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
3035
+ import { delimiter, join as join19 } from "path";
2202
3036
  function safeReadText(path) {
2203
3037
  try {
2204
3038
  return readFileSync14(path, "utf8");
@@ -2207,13 +3041,13 @@ function safeReadText(path) {
2207
3041
  }
2208
3042
  }
2209
3043
  function resolveCommandPath(name, env = process.env, platform = process.platform) {
2210
- if (name.includes("/") || name.includes("\\")) return existsSync10(name) ? name : null;
3044
+ if (name.includes("/") || name.includes("\\")) return existsSync11(name) ? name : null;
2211
3045
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
2212
3046
  const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
2213
3047
  for (const dir of dirs) {
2214
3048
  for (const ext of exts) {
2215
- const candidate = join16(dir, name + ext);
2216
- if (existsSync10(candidate)) return candidate;
3049
+ const candidate = join19(dir, name + ext);
3050
+ if (existsSync11(candidate)) return candidate;
2217
3051
  }
2218
3052
  }
2219
3053
  return null;
@@ -2316,13 +3150,13 @@ var init_platform = __esm({
2316
3150
  });
2317
3151
 
2318
3152
  // src/envGate.ts
2319
- import { existsSync as existsSync11 } from "fs";
2320
- import { join as join17 } from "path";
3153
+ import { existsSync as existsSync12 } from "fs";
3154
+ import { join as join20 } from "path";
2321
3155
  import { createConnection } from "net";
2322
3156
  import { execa as execa3 } from "execa";
2323
3157
  function detectRequirements(repo, io = {}) {
2324
- const exists = io.exists ?? existsSync11;
2325
- const marker = COMPOSE_MARKERS.find((f) => exists(join17(repo, f)));
3158
+ const exists = io.exists ?? existsSync12;
3159
+ const marker = COMPOSE_MARKERS.find((f) => exists(join20(repo, f)));
2326
3160
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
2327
3161
  }
2328
3162
  async function dockerAlive() {
@@ -2389,7 +3223,7 @@ var init_release = __esm({
2389
3223
  });
2390
3224
 
2391
3225
  // src/doctor.ts
2392
- import { isAbsolute as isAbsolute4, join as join18 } from "path";
3226
+ import { isAbsolute as isAbsolute4, join as join21 } from "path";
2393
3227
  function ok(id, title, detail) {
2394
3228
  return { id, title, status: "ok", detail, remedies: [], critical: false };
2395
3229
  }
@@ -2410,21 +3244,20 @@ function checkNode(io) {
2410
3244
  critical: true
2411
3245
  };
2412
3246
  }
2413
- function checkClaude(io) {
2414
- const bin = resolveClaudeBin();
2415
- if (io.hasCommand(bin)) {
2416
- return ok("claude", "Claude Code (claude)", bin === "claude" ? "\u043D\u0430\u0439\u0434\u0435\u043D \u0432 PATH" : `\u043D\u0430\u0439\u0434\u0435\u043D: ${bin}`);
2417
- }
3247
+ function checkExecutor(io, executor = resolveExecutor()) {
3248
+ const bin = executor.resolveBin();
3249
+ const where = /[/\\]/.test(bin) ? `\u043D\u0430\u0439\u0434\u0435\u043D: ${bin}` : "\u043D\u0430\u0439\u0434\u0435\u043D \u0432 PATH";
3250
+ if (io.hasCommand(bin)) return ok("claude", executor.title, where);
2418
3251
  const remedies = [
2419
3252
  {
2420
- what: "Claude Code CLI \u2014 \u0438\u043C \u0434\u0440\u043E\u043D \u043F\u0438\u0448\u0435\u0442 \u043A\u043E\u0434",
2421
- command: globalInstallCommand(io.globalPm === "sources" ? "npm" : io.globalPm, "@anthropic-ai/claude-code"),
3253
+ what: executor.install.what,
3254
+ command: globalInstallCommand(io.globalPm === "sources" ? "npm" : io.globalPm, executor.install.pkg),
2422
3255
  sudo: false,
2423
3256
  auto: true
2424
3257
  }
2425
3258
  ];
2426
- const detail = io.platform.kind === "windows" ? "\u0432 PATH \u043D\u0435\u0442 `claude`. \u041D\u0430 Windows spawn \u0438\u0449\u0435\u0442 \u0444\u0430\u0439\u043B \u043F\u043E PATHEXT \u2014 bash-\u043E\u0431\u0451\u0440\u0442\u043A\u0430 \u0431\u0435\u0437 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0435\u043C\u0443 \u043D\u0435 \u0432\u0438\u0434\u043D\u0430; \u043D\u0435\u0441\u0442\u0430\u043D\u0434\u0430\u0440\u0442\u043D\u0430\u044F \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u0437\u0430\u0434\u0430\u0451\u0442\u0441\u044F SWARM_CLAUDE_BIN" : "\u0432 PATH \u043D\u0435\u0442 `claude` \u2014 \u0434\u0440\u043E\u043D\u0443 \u043D\u0435\u0447\u0435\u043C \u043F\u0438\u0441\u0430\u0442\u044C \u043A\u043E\u0434";
2427
- return { id: "claude", title: "Claude Code (claude)", status: "fail", detail, remedies, critical: true };
3259
+ const detail = io.platform.kind === "windows" ? `\u0432 PATH \u043D\u0435\u0442 \`${bin}\`. \u041D\u0430 Windows spawn \u0438\u0449\u0435\u0442 \u0444\u0430\u0439\u043B \u043F\u043E PATHEXT \u2014 bash-\u043E\u0431\u0451\u0440\u0442\u043A\u0430 \u0431\u0435\u0437 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0435\u043C\u0443 \u043D\u0435 \u0432\u0438\u0434\u043D\u0430; \u043D\u0435\u0441\u0442\u0430\u043D\u0434\u0430\u0440\u0442\u043D\u0430\u044F \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u0437\u0430\u0434\u0430\u0451\u0442\u0441\u044F ${executor.binEnv}` : `\u0432 PATH \u043D\u0435\u0442 \`${bin}\` \u2014 \u0434\u0440\u043E\u043D\u0443 \u043D\u0435\u0447\u0435\u043C \u043F\u0438\u0441\u0430\u0442\u044C \u043A\u043E\u0434`;
3260
+ return { id: "claude", title: executor.title, status: "fail", detail, remedies, critical: true };
2428
3261
  }
2429
3262
  async function checkGh(io) {
2430
3263
  if (!io.hasCommand("gh")) {
@@ -2597,7 +3430,7 @@ function checkRepos(io, snap) {
2597
3430
  critical: true
2598
3431
  };
2599
3432
  }
2600
- if (env && !io.exists(join18(env, ".git"))) {
3433
+ if (env && !io.exists(join21(env, ".git"))) {
2601
3434
  return {
2602
3435
  id: "repos",
2603
3436
  title,
@@ -2607,7 +3440,7 @@ function checkRepos(io, snap) {
2607
3440
  critical: true
2608
3441
  };
2609
3442
  }
2610
- const broken = snap.paths.filter((p) => !io.exists(join18(p, ".git")));
3443
+ const broken = snap.paths.filter((p) => !io.exists(join21(p, ".git")));
2611
3444
  if (broken.length) {
2612
3445
  return {
2613
3446
  id: "repos",
@@ -2697,7 +3530,7 @@ async function runCheck(id, io) {
2697
3530
  case "version":
2698
3531
  return checkSwarmVersion(io);
2699
3532
  case "claude":
2700
- return checkClaude(io);
3533
+ return checkExecutor(io);
2701
3534
  case "gh":
2702
3535
  return checkGh(io);
2703
3536
  case "screenshots":
@@ -2754,6 +3587,7 @@ var init_doctor = __esm({
2754
3587
  "src/doctor.ts"() {
2755
3588
  "use strict";
2756
3589
  init_claudeBin();
3590
+ init_executors();
2757
3591
  init_envGate();
2758
3592
  init_platform();
2759
3593
  init_release();
@@ -2771,11 +3605,11 @@ var init_doctor = __esm({
2771
3605
 
2772
3606
  // src/repoMemory.ts
2773
3607
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync12 } from "fs";
2774
- import { join as join19 } from "path";
3608
+ import { join as join22 } from "path";
2775
3609
  function loadRememberedRepos(home) {
2776
3610
  let raw;
2777
3611
  try {
2778
- raw = readFileSync15(join19(home, FILE), "utf8");
3612
+ raw = readFileSync15(join22(home, FILE), "utf8");
2779
3613
  } catch {
2780
3614
  return [];
2781
3615
  }
@@ -2810,7 +3644,7 @@ __export(installCli_exports, {
2810
3644
  runInstallCli: () => runInstallCli,
2811
3645
  shortenError: () => shortenError
2812
3646
  });
2813
- import { existsSync as existsSync12 } from "fs";
3647
+ import { existsSync as existsSync13 } from "fs";
2814
3648
  function classifyChromiumError(message) {
2815
3649
  if (/playwright-core не установлен|Cannot find (module|package)/i.test(message)) return "no-module";
2816
3650
  if (/error while loading shared libraries|cannot open shared object file|missing dependencies|\blib[\w.+-]*\.so[.\d]*\b/i.test(message)) return "no-libs";
@@ -2865,7 +3699,7 @@ function defaultDoctorIO(packageVersion) {
2865
3699
  packageVersion,
2866
3700
  entry,
2867
3701
  hasCommand: (name) => hasCommandInPath(name),
2868
- exists: existsSync12,
3702
+ exists: existsSync13,
2869
3703
  run: (command) => runQuiet(command),
2870
3704
  latestVersion: () => latestMemo ??= fetchLatestVersion(),
2871
3705
  probeChromium: () => probeChromium(),
@@ -3075,12 +3909,12 @@ var init_updateCli = __esm({
3075
3909
  // src/cli.ts
3076
3910
  init_cliIo();
3077
3911
  import { basename as basename4 } from "path";
3078
- import { existsSync as existsSync13, readdirSync as readdirSync7, statSync as statSync8, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
3912
+ import { existsSync as existsSync14, readdirSync as readdirSync9, statSync as statSync9, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
3079
3913
  import { setTimeout as sleep2 } from "timers/promises";
3080
3914
 
3081
3915
  // src/swarmClient.ts
3082
3916
  init_config();
3083
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3917
+ import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
3084
3918
  import { dirname } from "path";
3085
3919
  import { randomUUID } from "crypto";
3086
3920
 
@@ -3246,8 +4080,8 @@ init_names2();
3246
4080
  init_storm();
3247
4081
 
3248
4082
  // src/logTail.ts
3249
- import { closeSync, existsSync, openSync, readSync, readdirSync, statSync } from "fs";
3250
- import { join as join2 } from "path";
4083
+ import { closeSync, existsSync as existsSync3, openSync, readSync, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
4084
+ import { join as join5 } from "path";
3251
4085
  var TAIL_LINES_FALLBACK = 200;
3252
4086
  var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
3253
4087
 
@@ -3315,6 +4149,15 @@ var SwarmClient = class _SwarmClient {
3315
4149
  nextRunId = 1;
3316
4150
  nextEventId = 1;
3317
4151
  nextQuestionId = 1;
4152
+ // TASWM-350: ключ НЕЗАКРЫТОЙ попытки взять работу. Живёт от первого вызова claim до
4153
+ // первого настоящего ответа прода; всё это время ретраи идут с ним, и повтор после
4154
+ // обрыва связи не превращается во второе взятие. См. claim().
4155
+ pendingClaimKey = null;
4156
+ // TASWM-357: последнее НАПЕЧАТАННОЕ про пропущенные claim'ом задачи (родители и
4157
+ // заблокированные). Дедуп по тексту: claim идёт раз в ~60 c, а набор пропущенных меняется
4158
+ // редко — без этого лог был бы простынёй из одной и той же строки, и в ней потонуло бы
4159
+ // всё остальное. Меняется набор (эпик закрыли / блокер прошли) → печатаем снова.
4160
+ lastClaimSkipLog = null;
3318
4161
  // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
3319
4162
  // Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
3320
4163
  // переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
@@ -3325,6 +4168,9 @@ var SwarmClient = class _SwarmClient {
3325
4168
  // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
3326
4169
  // каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
3327
4170
  notifyLevelUnsupported = false;
4171
+ // TAS-485: то же для гашения разрешившихся тревог (swarm_notify_resolve) — прод старее
4172
+ // демона не знает тула; ретраить это каждый тик бессмысленно, см. resolveAnomalies.
4173
+ notifyResolveUnsupported = false;
3328
4174
  // Корреляция с продом.
3329
4175
  keyByLocal = /* @__PURE__ */ new Map();
3330
4176
  // local run id → runKey
@@ -3367,24 +4213,24 @@ var SwarmClient = class _SwarmClient {
3367
4213
  this.repoFullNames = registry.map((e) => e.repoFullName);
3368
4214
  this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
3369
4215
  this.host = opts.host ?? null;
3370
- this.repoExists = opts.repoExists ?? existsSync2;
4216
+ this.repoExists = opts.repoExists ?? existsSync4;
3371
4217
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
3372
4218
  this.outboxPath = opts.outboxPath;
3373
4219
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
3374
4220
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
3375
- mkdirSync2(dirname(this.outboxPath), { recursive: true });
4221
+ mkdirSync4(dirname(this.outboxPath), { recursive: true });
3376
4222
  this.recover();
3377
4223
  }
3378
4224
  // ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
3379
4225
  recover() {
3380
- if (existsSync2(this.mapPath)) {
4226
+ if (existsSync4(this.mapPath)) {
3381
4227
  try {
3382
4228
  const obj = JSON.parse(readFileSync3(this.mapPath, "utf8"));
3383
4229
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
3384
4230
  } catch {
3385
4231
  }
3386
4232
  }
3387
- if (existsSync2(this.outboxPath)) {
4233
+ if (existsSync4(this.outboxPath)) {
3388
4234
  const txt = readFileSync3(this.outboxPath, "utf8");
3389
4235
  for (const line of txt.split("\n")) {
3390
4236
  const s = line.trim();
@@ -3405,7 +4251,7 @@ var SwarmClient = class _SwarmClient {
3405
4251
  // eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
3406
4252
  // переприсваиваем локально (id событий — только порядок, не корреляция с продом).
3407
4253
  replayGlobalEvents() {
3408
- if (!existsSync2(this.stormPath)) return;
4254
+ if (!existsSync4(this.stormPath)) return;
3409
4255
  let txt;
3410
4256
  try {
3411
4257
  txt = readFileSync3(this.stormPath, "utf8");
@@ -3474,8 +4320,8 @@ var SwarmClient = class _SwarmClient {
3474
4320
  this.scheduleFlush();
3475
4321
  }
3476
4322
  // Прод-патч прогона: локальные поля Run → поля swarm_run_upsert (см. TASWM-74).
3477
- // pid/worktree_path (рантайм), respawns (нет в схеме тула), снимок задачи (task_*,
3478
- // прод берёт join'ом из Task) и priority (локальная очередь ЭТОГО демона, TASWM-269)
4323
+ // pid/worktree_path (рантайм), снимок задачи (task_*, прод берёт join'ом из Task)
4324
+ // и priority (локальная очередь ЭТОГО демона, TASWM-269)
3479
4325
  // в прод НЕ уходят. kind → role (плоский enum). Self-ref
3480
4326
  // прогонов hero_run_id/resolves_run_id/superseded_by → heroId/resolvesRunId/
3481
4327
  // supersededById как ОТЛОЖЕННЫЕ ссылки (RunRef) — cuid резолвится на флаше.
@@ -3486,6 +4332,10 @@ var SwarmClient = class _SwarmClient {
3486
4332
  if ("model" in patch) out.model = patch.model ?? null;
3487
4333
  if ("name" in patch) out.droneName = patch.name ?? null;
3488
4334
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
4335
+ if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
4336
+ if ("tokens_out" in patch && patch.tokens_out !== void 0) out.tokensOut = patch.tokens_out;
4337
+ if ("duration_ms" in patch && patch.duration_ms !== void 0) out.durationMs = patch.duration_ms;
4338
+ if ("respawns" in patch && patch.respawns !== void 0) out.respawns = patch.respawns;
3489
4339
  if ("pr_url" in patch) out.prUrl = patch.pr_url ?? null;
3490
4340
  if ("branch" in patch) out.branch = patch.branch ?? null;
3491
4341
  if ("repo" in patch) out.repo = patch.repo ?? null;
@@ -3551,6 +4401,10 @@ var SwarmClient = class _SwarmClient {
3551
4401
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
3552
4402
  model: input.model ?? null,
3553
4403
  cost_usd: 0,
4404
+ tokens_in: 0,
4405
+ // копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
4406
+ tokens_out: 0,
4407
+ duration_ms: 0,
3554
4408
  pr_url: null,
3555
4409
  superseded_by: null,
3556
4410
  decision: input.decision ?? null,
@@ -3673,6 +4527,7 @@ var SwarmClient = class _SwarmClient {
3673
4527
  if (!d) return 0;
3674
4528
  d.respawns += 1;
3675
4529
  d.updated_at = stamp();
4530
+ this.enqueueRun(d, this.runFields({ respawns: d.respawns }));
3676
4531
  return d.respawns;
3677
4532
  }
3678
4533
  cancel(id, reason) {
@@ -4088,7 +4943,7 @@ var SwarmClient = class _SwarmClient {
4088
4943
  * транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
4089
4944
  */
4090
4945
  reconcileRuntime(opts) {
4091
- const wtExists = opts.worktreeExists ?? existsSync2;
4946
+ const wtExists = opts.worktreeExists ?? existsSync4;
4092
4947
  const pidByWorktree = /* @__PURE__ */ new Map();
4093
4948
  try {
4094
4949
  for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
@@ -4176,23 +5031,69 @@ var SwarmClient = class _SwarmClient {
4176
5031
  * шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
4177
5032
  */
4178
5033
  async claim() {
5034
+ this.pendingClaimKey ??= randomUUID();
4179
5035
  let res;
4180
5036
  try {
4181
5037
  res = await this.call("swarm_claim_work", {
4182
5038
  ...this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {},
4183
5039
  // TASWM-281: взятая из общей очереди задача сразу принадлежит ЭТОЙ машине —
4184
5040
  // прод помечает созданный прогон hostId, чужой демон его не увидит.
4185
- ...this.host ? { hostId: this.host.hostId } : {}
5041
+ ...this.host ? { hostId: this.host.hostId } : {},
5042
+ idempotencyKey: this.pendingClaimKey
4186
5043
  });
4187
5044
  } catch {
4188
5045
  return null;
4189
5046
  }
4190
5047
  if (res?.isError) return null;
5048
+ this.pendingClaimKey = null;
4191
5049
  const sc = res.structuredContent ?? {};
5050
+ this.logClaimSkips(sc);
4192
5051
  if (!sc.claimed || typeof sc.run !== "object" || sc.run === null) return null;
4193
5052
  const local = this.hydrateRun(sc.run);
4194
5053
  return local ? this.mat(local) : null;
4195
5054
  }
5055
+ /**
5056
+ * TASWM-357: назвать в логе задачи, которые прод НЕ отдал в работу осознанно — родителей с
5057
+ * незавершёнными подзадачами (работа всегда в листьях) и листья с незакрытым блокером
5058
+ * (явный порядок, связь задач «блокирует»). Печатаем ЗДЕСЬ, а не на проде: оба фильтра
5059
+ * живут SQL-условиями и молчат по природе, а `display` claim'а демон не логирует — без
5060
+ * этой строки «почему эпик стоит нетронутым» не видно ниоткуда, и оператор ровно так же
5061
+ * гадает, как гадал, пока дрон вставал на эпике с вопросом «меня спавнили на родителя».
5062
+ *
5063
+ * Формат прода — `data.skippedParents: [{slug,title,openChildren}]` и
5064
+ * `data.skippedBlocked: [{slug,title,blockers[]}]`. Читаем защищённо: у старого прода
5065
+ * полей нет вовсе (тогда молчим), а разбирать чужую строку `display` вместо структуры —
5066
+ * тот же класс хрупкости, что уже кусал нас в notifyAnomaly.
5067
+ */
5068
+ logClaimSkips(sc) {
5069
+ const rows = (raw) => (Array.isArray(raw) ? raw : []).map((p) => typeof p === "object" && p !== null ? p : null).filter((p) => p !== null && typeof p.slug === "string");
5070
+ const others = (flag) => flag === true ? " \u0438 \u0434\u0440\u0443\u0433\u0438\u0435" : "";
5071
+ const lines = [];
5072
+ const parents = rows(sc.skippedParents).map(
5073
+ (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)`
5074
+ );
5075
+ if (parents.length > 0) {
5076
+ lines.push(
5077
+ `[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)}`
5078
+ );
5079
+ }
5080
+ const blocked = rows(sc.skippedBlocked).map(
5081
+ (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"}`
5082
+ );
5083
+ if (blocked.length > 0) {
5084
+ lines.push(
5085
+ `[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)}`
5086
+ );
5087
+ }
5088
+ const text = lines.join("\n");
5089
+ if (text.length === 0) {
5090
+ this.lastClaimSkipLog = null;
5091
+ return;
5092
+ }
5093
+ if (text === this.lastClaimSkipLog) return;
5094
+ this.lastClaimSkipLog = text;
5095
+ for (const line of lines) console.log(line);
5096
+ }
4196
5097
  /**
4197
5098
  * TASWM-281: пометить своими прогоны, под которыми на ЭТОЙ машине нашёлся ЖИВОЙ
4198
5099
  * процесс (после reconcileRuntime на старте). Это ровно те прогоны, где сомнений в
@@ -4466,12 +5367,20 @@ var SwarmClient = class _SwarmClient {
4466
5367
  * Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
4467
5368
  * означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
4468
5369
  *
5370
+ * TASWM-352: тем же пульсом уходит ЁМКОСТЬ этой машины (`capacity` — живой потолок
5371
+ * дронов ЭТОГО демона: машинный оверрайд, иначе общая настройка владельца). По ней
5372
+ * планировщик роя раскладывает очередь между машинами: до этого у прода было ОДНО
5373
+ * число на владельца, и ноутбук с рабочей станцией делили его поровну, хотя тянут
5374
+ * разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
5375
+ * трогает прошлое значение — та же логика, что у repoFullNames.
5376
+ *
4469
5377
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
4470
5378
  */
4471
- async heartbeat(daemonVersion, pause) {
5379
+ async heartbeat(daemonVersion, pause, capacity) {
4472
5380
  try {
4473
5381
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
4474
5382
  if (daemonVersion) args.daemonVersion = daemonVersion;
5383
+ if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
4475
5384
  if (pause !== void 0) {
4476
5385
  args.paused = !!pause?.paused;
4477
5386
  if (pause?.reason) args.pauseReason = pause.reason;
@@ -4577,6 +5486,35 @@ var SwarmClient = class _SwarmClient {
4577
5486
  const res = await this.call("swarm_notify", base3);
4578
5487
  if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
4579
5488
  }
5489
+ /**
5490
+ * TAS-485: погасить в инбоксе владельца тревоги, чьё условие ушло. Ключи — те же, с
5491
+ * которыми уходил notifyAnomaly (прод сам ставит им префикс `swarm:`).
5492
+ *
5493
+ * Бросает при сбое — СОЗНАТЕЛЬНО, как notifyAnomaly: вызывающий (tickOnce) вычёркивает
5494
+ * ключи из durable-очереди только после успеха, иначе «погашено» разошлось бы с тем,
5495
+ * что человек видит в инбоксе, и разошлось бы навсегда — повторить будет некому.
5496
+ *
5497
+ * ДЕПЛОЙ-ОКНО. Демон из свежего чекаута против ещё не задеплоенного прода получит
5498
+ * «неизвестный тул». Это не ошибка доставки, а отсутствие фичи на той стороне: ретраить
5499
+ * её каждые ~4 секунды бессмысленно, поэтому первую такую осечку запоминаем на жизнь
5500
+ * процесса и дальше молча пропускаем гашение (надзор в остальном работает как раньше).
5501
+ * Тот же приём, что у notifyAnomaly с аргументом `level`.
5502
+ */
5503
+ async resolveAnomalies(keys) {
5504
+ const tags = [...new Set(keys.filter((k) => k.length > 0 && k.length <= 120))];
5505
+ if (tags.length === 0 || this.notifyResolveUnsupported) return;
5506
+ const res = await this.call("swarm_notify_resolve", { tags });
5507
+ if (!res?.isError) return;
5508
+ const text = JSON.stringify(res);
5509
+ if (text.includes("swarm_notify_resolve") && (text.includes("Unknown tool") || text.includes("\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D"))) {
5510
+ this.notifyResolveUnsupported = true;
5511
+ console.warn(
5512
+ "[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."
5513
+ );
5514
+ return;
5515
+ }
5516
+ throw new Error(`swarm_notify_resolve \u043E\u0442\u0432\u0435\u0440\u0433 ${tags.length} \u043A\u043B\u044E\u0447(\u0435\u0439)`);
5517
+ }
4580
5518
  /**
4581
5519
  * TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
4582
5520
  * через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
@@ -4641,6 +5579,10 @@ var SwarmClient = class _SwarmClient {
4641
5579
  depends_on: [],
4642
5580
  model: null,
4643
5581
  cost_usd: 0,
5582
+ tokens_in: 0,
5583
+ // токены и время работы (TASWM-353) — прод-поля, заполняются ниже
5584
+ tokens_out: 0,
5585
+ duration_ms: 0,
4644
5586
  pr_url: null,
4645
5587
  superseded_by: null,
4646
5588
  decision: null,
@@ -4651,9 +5593,9 @@ var SwarmClient = class _SwarmClient {
4651
5593
  review_verdict: null,
4652
5594
  review_phase: null,
4653
5595
  // вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
4654
- // Локальная бухгалтерия (в прод-схему не уходит, как respawns) — на гидрации из
4655
- // прода дефолт 0; существующая локальная запись НЕ перетирается (поле не в field-copy
4656
- // ниже), в отличие от respawns, — авто-ревью v2, TASWM-162.
5596
+ // Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
5597
+ // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
5598
+ // в отличие от respawns, — авто-ревью v2, TASWM-162.
4657
5599
  review_cycle: 0,
4658
5600
  task_title: null,
4659
5601
  task_desc: null,
@@ -4705,6 +5647,9 @@ var SwarmClient = class _SwarmClient {
4705
5647
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
4706
5648
  local.model = pr.model ?? null;
4707
5649
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
5650
+ local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
5651
+ local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
5652
+ local.duration_ms = typeof pr.durationMs === "number" ? pr.durationMs : 0;
4708
5653
  local.pr_url = pr.prUrl ?? null;
4709
5654
  local.stop_reason = pr.stopReason ?? null;
4710
5655
  local.respawns = typeof pr.respawns === "number" ? pr.respawns : 0;
@@ -4962,13 +5907,13 @@ var SwarmClient = class _SwarmClient {
4962
5907
  }
4963
5908
  persistPending() {
4964
5909
  try {
4965
- writeFileSync2(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
5910
+ writeFileSync3(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
4966
5911
  } catch {
4967
5912
  }
4968
5913
  }
4969
5914
  persistMap() {
4970
5915
  try {
4971
- writeFileSync2(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
5916
+ writeFileSync3(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
4972
5917
  } catch {
4973
5918
  }
4974
5919
  }
@@ -4993,6 +5938,97 @@ function parseDecision(v) {
4993
5938
  }
4994
5939
  }
4995
5940
 
5941
+ // src/cli.ts
5942
+ init_src();
5943
+
5944
+ // src/gitHistory.ts
5945
+ import { $ } from "execa";
5946
+ var REC = "";
5947
+ var FIELD = "";
5948
+ var LOG_FORMAT = `${REC}%H${FIELD}%s`;
5949
+ var SLUG_IN_SUBJECT = /\b([A-Z][A-Z0-9]{0,7}-\d+)\b/;
5950
+ function slugFromSubject(subject) {
5951
+ return SLUG_IN_SUBJECT.exec(subject)?.[1] ?? null;
5952
+ }
5953
+ var SCOPE_IN_SUBJECT = /(?:^|·\s*)[a-z]+\(([^)]+)\):/g;
5954
+ function scopeFromText(text) {
5955
+ if (!text) return null;
5956
+ const votes = /* @__PURE__ */ new Map();
5957
+ for (const m of text.matchAll(SCOPE_IN_SUBJECT)) {
5958
+ const scope = m[1].trim().toLowerCase();
5959
+ if (scope) votes.set(scope, (votes.get(scope) ?? 0) + 1);
5960
+ }
5961
+ let best = null;
5962
+ let bestN = 0;
5963
+ for (const [scope, n] of votes) {
5964
+ if (n > bestN) {
5965
+ best = scope;
5966
+ bestN = n;
5967
+ }
5968
+ }
5969
+ return best;
5970
+ }
5971
+ function unquotePath(raw) {
5972
+ const p = raw.trim();
5973
+ return p.length >= 2 && p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
5974
+ }
5975
+ function parseGitLog(out) {
5976
+ const commits = [];
5977
+ for (const chunk of out.split(REC)) {
5978
+ if (!chunk.trim()) continue;
5979
+ const [head, ...rest] = chunk.split("\n");
5980
+ const [sha, subject] = (head ?? "").split(FIELD);
5981
+ if (!sha) continue;
5982
+ const files = rest.map(unquotePath).filter(Boolean);
5983
+ commits.push({ sha, subject: subject ?? "", files });
5984
+ }
5985
+ return commits;
5986
+ }
5987
+ var NOISE_RE = /^(journal\/|changelog\/|docs\/|design\/|scratch\/|\.claude\/|.*\.lock$|pnpm-lock\.yaml$)/;
5988
+ function isNoisePath(path) {
5989
+ return NOISE_RE.test(path);
5990
+ }
5991
+ var GROUP_DEFAULTS = { maxFilesPerCommit: 120, ignore: isNoisePath };
5992
+ function groupTaskHistory(commits, opts = {}) {
5993
+ const o = { ...GROUP_DEFAULTS, ...opts };
5994
+ const bySlug = /* @__PURE__ */ new Map();
5995
+ for (const c of commits) {
5996
+ const slug = slugFromSubject(c.subject);
5997
+ if (!slug) continue;
5998
+ if (c.files.length > o.maxFilesPerCommit) continue;
5999
+ const files = c.files.filter((f) => !o.ignore(f));
6000
+ if (!files.length) continue;
6001
+ let acc = bySlug.get(slug);
6002
+ if (!acc) bySlug.set(slug, acc = { files: /* @__PURE__ */ new Set(), texts: [] });
6003
+ for (const f of files) acc.files.add(f);
6004
+ acc.texts.push(c.subject);
6005
+ }
6006
+ return [...bySlug.entries()].map(([slug, acc]) => ({
6007
+ slug,
6008
+ files: [...acc.files],
6009
+ text: acc.texts.join(" \xB7 ")
6010
+ }));
6011
+ }
6012
+ async function readCommitLog(repo, opts = {}) {
6013
+ const max = opts.maxCommits ?? 1500;
6014
+ try {
6015
+ const { stdout } = await $({ cwd: repo, reject: false })`git log --no-merges --name-only --pretty=format:${LOG_FORMAT} -n ${String(max)} ${opts.ref ?? "HEAD"}`;
6016
+ return parseGitLog(stdout ?? "");
6017
+ } catch {
6018
+ return [];
6019
+ }
6020
+ }
6021
+ async function readTaskHistory(repo, opts = {}) {
6022
+ return groupTaskHistory(await readCommitLog(repo, opts), opts);
6023
+ }
6024
+
6025
+ // src/zones.ts
6026
+ init_src();
6027
+ var DEFAULT_TTL_MS = 10 * 6e4;
6028
+ function withManualAreas(history, areas) {
6029
+ return history.map((e) => ({ ...e, area: normalizeZoneName(areas.get(e.slug) ?? null) }));
6030
+ }
6031
+
4996
6032
  // src/cli.ts
4997
6033
  init_config();
4998
6034
 
@@ -5064,11 +6100,11 @@ function tailFile(path, lines) {
5064
6100
  }
5065
6101
 
5066
6102
  // src/logRotate.ts
5067
- import { copyFileSync, renameSync as renameSync2, statSync as statSync2, fstatSync, truncateSync, unlinkSync, existsSync as existsSync3 } from "fs";
6103
+ import { copyFileSync, renameSync as renameSync2, statSync as statSync4, fstatSync, truncateSync, unlinkSync, existsSync as existsSync5 } from "fs";
5068
6104
  var realIo = {
5069
6105
  size: (path) => {
5070
6106
  try {
5071
- return statSync2(path).size;
6107
+ return statSync4(path).size;
5072
6108
  } catch {
5073
6109
  return null;
5074
6110
  }
@@ -5077,7 +6113,7 @@ var realIo = {
5077
6113
  rename: renameSync2,
5078
6114
  truncate: (path) => truncateSync(path, 0),
5079
6115
  unlink: unlinkSync,
5080
- exists: existsSync3
6116
+ exists: existsSync5
5081
6117
  };
5082
6118
  var STRAY_GENERATIONS = 8;
5083
6119
  function rotateDaemonLog(path, opts, io = realIo) {
@@ -5336,17 +6372,17 @@ init_repoDiscovery();
5336
6372
 
5337
6373
  // src/hostIdentity.ts
5338
6374
  import { hostname } from "os";
5339
- import { existsSync as existsSync4, readFileSync as readFileSync7, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "fs";
5340
- import { join as join5 } from "path";
6375
+ import { existsSync as existsSync6, readFileSync as readFileSync7, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
6376
+ import { join as join8 } from "path";
5341
6377
  import { randomUUID as randomUUID2 } from "crypto";
5342
6378
  function hostIdentityPath(home) {
5343
- return join5(home, "host.json");
6379
+ return join8(home, "host.json");
5344
6380
  }
5345
6381
  function loadHostIdentity(deps) {
5346
- const exists = deps.exists ?? existsSync4;
6382
+ const exists = deps.exists ?? existsSync6;
5347
6383
  const readFile2 = deps.readFile ?? ((p) => readFileSync7(p, "utf8"));
5348
6384
  const writeFile2 = deps.writeFile ?? ((p, data) => {
5349
- writeFileSync4(`${p}.tmp`, data);
6385
+ writeFileSync5(`${p}.tmp`, data);
5350
6386
  renameSync4(`${p}.tmp`, p);
5351
6387
  });
5352
6388
  const host = deps.hostname ?? hostname;
@@ -5458,9 +6494,9 @@ import { basename as basename2 } from "path";
5458
6494
  import { serve as serve2 } from "@hono/node-server";
5459
6495
 
5460
6496
  // src/diff.ts
5461
- import { statSync as statSync4 } from "fs";
5462
- import { join as join6 } from "path";
5463
- import { $ as $2 } from "execa";
6497
+ import { statSync as statSync6 } from "fs";
6498
+ import { join as join9 } from "path";
6499
+ import { $ as $3 } from "execa";
5464
6500
  var MAX_FILES = 200;
5465
6501
  var MAX_LINES_PER_FILE = 1500;
5466
6502
  var MAX_UNTRACKED = 60;
@@ -5580,12 +6616,12 @@ function truncateFile(f) {
5580
6616
  }
5581
6617
  async function resolveBase(wt, baseBranch) {
5582
6618
  const tryRef = async (ref) => {
5583
- const sha = (await $2({ reject: false })`git -C ${wt} rev-parse --verify --quiet ${ref + "^{commit}"}`).stdout.trim();
6619
+ const sha = (await $3({ reject: false })`git -C ${wt} rev-parse --verify --quiet ${ref + "^{commit}"}`).stdout.trim();
5584
6620
  if (!sha) return null;
5585
- const mb = (await $2({ reject: false })`git -C ${wt} merge-base HEAD ${ref}`).stdout.trim();
6621
+ const mb = (await $3({ reject: false })`git -C ${wt} merge-base HEAD ${ref}`).stdout.trim();
5586
6622
  return mb || null;
5587
6623
  };
5588
- const originHead = (await $2({ reject: false })`git -C ${wt} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
6624
+ const originHead = (await $3({ reject: false })`git -C ${wt} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
5589
6625
  const candidates = [
5590
6626
  ...baseBranch ? [`origin/${baseBranch}`, baseBranch] : [],
5591
6627
  originHead,
@@ -5602,7 +6638,7 @@ async function resolveBase(wt, baseBranch) {
5602
6638
  }
5603
6639
  function mtime(path) {
5604
6640
  try {
5605
- return statSync4(path).mtimeMs;
6641
+ return statSync6(path).mtimeMs;
5606
6642
  } catch {
5607
6643
  return 0;
5608
6644
  }
@@ -5611,23 +6647,23 @@ async function collectRunDiff(worktree, opts = {}) {
5611
6647
  const stub = (reason) => ({ ok: false, reason, pr: opts.pr ?? null, files: [], added: 0, removed: 0 });
5612
6648
  const gone = opts.status === "queued" ? "\u0434\u0440\u043E\u043D \u0435\u0449\u0451 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u043A\u043E\u0434\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442" : "worktree \u0443\u0431\u0440\u0430\u043D \u2014 \u043A\u043E\u0434 \u0441\u043C\u043E\u0442\u0440\u0438 \u0432 PR";
5613
6649
  if (!worktree) return stub(gone);
5614
- const isWt = (await $2({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
6650
+ const isWt = (await $3({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
5615
6651
  if (isWt !== "true") return stub(gone);
5616
6652
  const base3 = await resolveBase(worktree, opts.baseBranch);
5617
6653
  const from = base3 ?? "HEAD";
5618
- const trackedTxt = (await $2({ reject: false })`git -C ${worktree} diff --no-color --find-renames ${from} --`).stdout;
6654
+ const trackedTxt = (await $3({ reject: false })`git -C ${worktree} diff --no-color --find-renames ${from} --`).stdout;
5619
6655
  let files = parseUnifiedDiff(trackedTxt);
5620
- const untrackedRaw = (await $2({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
6656
+ const untrackedRaw = (await $3({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
5621
6657
  const untracked = untrackedRaw.split("\0").filter(Boolean).slice(0, MAX_UNTRACKED);
5622
6658
  for (const rel of untracked) {
5623
- const t = (await $2({ reject: false })`git -C ${worktree} diff --no-color --no-index -- /dev/null ${rel}`).stdout;
6659
+ const t = (await $3({ reject: false })`git -C ${worktree} diff --no-color --no-index -- /dev/null ${rel}`).stdout;
5624
6660
  for (const pf of parseUnifiedDiff(t)) {
5625
6661
  pf.status = "added";
5626
6662
  if (!pf.path || pf.path === "/dev/null") pf.path = rel;
5627
6663
  files.push(pf);
5628
6664
  }
5629
6665
  }
5630
- files.sort((a, b) => mtime(join6(worktree, b.path)) - mtime(join6(worktree, a.path)));
6666
+ files.sort((a, b) => mtime(join9(worktree, b.path)) - mtime(join9(worktree, a.path)));
5631
6667
  const truncatedFiles = files.length > MAX_FILES;
5632
6668
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
5633
6669
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -5649,10 +6685,10 @@ init_storm();
5649
6685
 
5650
6686
  // src/gh.ts
5651
6687
  init_src();
5652
- import { $ as $3 } from "execa";
6688
+ import { $ as $4 } from "execa";
5653
6689
  var NO_CHECKS_STDERR = /no checks reported/i;
5654
6690
  var realChecksRunner = async (pr) => {
5655
- const r = await $3({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
6691
+ const r = await $4({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
5656
6692
  const stdout = r.stdout ?? "";
5657
6693
  if (!stdout.trim() && NO_CHECKS_STDERR.test(r.stderr ?? "")) return "[]";
5658
6694
  return stdout;
@@ -5701,14 +6737,14 @@ init_droneChannel();
5701
6737
  // src/oauth.ts
5702
6738
  init_config();
5703
6739
  import { createHash, randomBytes as randomBytes2 } from "crypto";
5704
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2 } from "fs";
5705
- import { join as join7 } from "path";
6740
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, unlinkSync as unlinkSync2 } from "fs";
6741
+ import { join as join10 } from "path";
5706
6742
  var REGISTER = "/api/oauth/register";
5707
6743
  var AUTHORIZE = "/api/oauth/authorize";
5708
6744
  var TOKEN = "/api/oauth/token";
5709
6745
  var SCOPE = "openid profile email taskless:full";
5710
- var STORE_PATH = join7(SWARM_HOME, "oauth.json");
5711
- var CLIENT_PATH = join7(SWARM_HOME, "oauth-client.json");
6746
+ var STORE_PATH = join10(SWARM_HOME, "oauth.json");
6747
+ var CLIENT_PATH = join10(SWARM_HOME, "oauth-client.json");
5712
6748
  function makePkce() {
5713
6749
  const verifier = randomBytes2(48).toString("base64url");
5714
6750
  const challenge = createHash("sha256").update(verifier).digest("base64url");
@@ -5799,7 +6835,7 @@ function loadStore() {
5799
6835
  }
5800
6836
  }
5801
6837
  function saveStore(s) {
5802
- writeFileSync6(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
6838
+ writeFileSync7(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
5803
6839
  }
5804
6840
  function clearStore() {
5805
6841
  try {
@@ -5818,7 +6854,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
5818
6854
  const existing = loadClientId();
5819
6855
  if (existing) return existing;
5820
6856
  const clientId = await registerClient({ api, redirectUri }, fetchFn);
5821
- writeFileSync6(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
6857
+ writeFileSync7(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
5822
6858
  return clientId;
5823
6859
  }
5824
6860
  async function ensureAccess(store, opts = {}) {
@@ -5892,6 +6928,10 @@ function roySnapshot(db2, now = Date.now(), overmindLog = [], paused = false, da
5892
6928
  stopReason: r.stop_reason,
5893
6929
  reason,
5894
6930
  updated: r.updated_at,
6931
+ startedAt: r.created_at,
6932
+ // TASWM-351: возраст прогона на карточке
6933
+ costUsd: r.cost_usd || 0,
6934
+ // TASWM-351: то же поле, что уходит в прод (swarm_run_upsert{costUsd})
5895
6935
  step,
5896
6936
  hasDecision: withQ.has(r.id),
5897
6937
  rescued: evs.some((e) => e.kind === "rescued"),
@@ -7397,17 +8437,16 @@ function startWeb(deps, port = WEB_PORT) {
7397
8437
  import { cancelRun, respawnRun, runDaemon, wakeRun, OVERMIND_ENTRY, listDroneProcesses } from "./overmind.js";
7398
8438
 
7399
8439
  // src/launcher.ts
7400
- init_claudeBin();
7401
8440
  import { spawn as spawn2 } from "child_process";
7402
- import { closeSync as closeSync3, existsSync as existsSync6, mkdirSync as mkdirSync5, openSync as openSync3, readdirSync as readdirSync5, statSync as statSync6, writeFileSync as writeFileSync7 } from "fs";
8441
+ import { closeSync as closeSync3, existsSync as existsSync7, mkdirSync as mkdirSync7, openSync as openSync3, readdirSync as readdirSync7 } from "fs";
7403
8442
  import { createRequire } from "module";
7404
- import { homedir as homedir3 } from "os";
7405
- import { dirname as dirname3, join as join9 } from "path";
8443
+ import { homedir as homedir4 } from "os";
8444
+ import { dirname as dirname3, join as join12 } from "path";
7406
8445
  import { fileURLToPath } from "url";
7407
8446
 
7408
8447
  // src/droneHome.ts
7409
- import { lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync4, rmSync, statSync as statSync5, symlinkSync } from "fs";
7410
- import { join as join8, relative, sep } from "path";
8448
+ import { lstatSync, mkdirSync as mkdirSync6, readdirSync as readdirSync6, rmSync, statSync as statSync7, symlinkSync as symlinkSync2 } from "fs";
8449
+ import { join as join11, relative, sep } from "path";
7411
8450
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
7412
8451
  function droneHomeDeny(env = process.env) {
7413
8452
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -7425,34 +8464,34 @@ function buildDroneHome(opts) {
7425
8464
  const { realHome, droneHome } = opts;
7426
8465
  const deny = opts.deny ?? droneHomeDeny();
7427
8466
  const platform = opts.platform ?? process.platform;
7428
- mkdirSync4(droneHome, { recursive: true, mode: 448 });
7429
- const wanted = new Set(mirroredEntries(readdirSync4(realHome), deny, platform));
8467
+ mkdirSync6(droneHome, { recursive: true, mode: 448 });
8468
+ const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
7430
8469
  const rel = relative(realHome, droneHome);
7431
8470
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
7432
- for (const existing of readdirSync4(droneHome)) {
8471
+ for (const existing of readdirSync6(droneHome)) {
7433
8472
  if (wanted.has(existing)) continue;
7434
- const p = join8(droneHome, existing);
8473
+ const p = join11(droneHome, existing);
7435
8474
  try {
7436
8475
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
7437
8476
  } catch {
7438
8477
  }
7439
8478
  }
7440
8479
  for (const name of wanted) {
7441
- const link = join8(droneHome, name);
8480
+ const link = join11(droneHome, name);
7442
8481
  try {
7443
8482
  lstatSync(link);
7444
8483
  continue;
7445
8484
  } catch {
7446
8485
  }
7447
- const target = join8(realHome, name);
8486
+ const target = join11(realHome, name);
7448
8487
  let isDir;
7449
8488
  try {
7450
- isDir = statSync5(target).isDirectory();
8489
+ isDir = statSync7(target).isDirectory();
7451
8490
  } catch {
7452
8491
  continue;
7453
8492
  }
7454
8493
  try {
7455
- symlinkSync(target, link, isDir ? "junction" : "file");
8494
+ symlinkSync2(target, link, isDir ? "junction" : "file");
7456
8495
  } catch (e) {
7457
8496
  if (e.code !== "EEXIST") throw e;
7458
8497
  }
@@ -7465,10 +8504,10 @@ var REAL_PATH_ENV_PINS = [
7465
8504
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
7466
8505
  ];
7467
8506
  function realPathEnvPins(realHome) {
7468
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join8(realHome, ...segments)]));
8507
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join11(realHome, ...segments)]));
7469
8508
  }
7470
- function droneDenyRules(realHome, swarmHome) {
7471
- const paths = [
8509
+ function dronePrivatePaths(realHome, swarmHome) {
8510
+ return [
7472
8511
  // НЕ `${swarmHome}/**`. Worktree дрона живёт ВНУТРИ swarmHome
7473
8512
  // (`<swarmHome>/worktrees.noindex/<repo>/<slug>`), и правило на весь каталог закрывало
7474
8513
  // дрону его собственный рабочий каталог: Read/Write/Edit отвечали «File is in a directory
@@ -7489,11 +8528,6 @@ function droneDenyRules(realHome, swarmHome) {
7489
8528
  `${realHome}/.claude/.credentials.json`,
7490
8529
  `${realHome}/.npmrc`
7491
8530
  ];
7492
- const abs = (p) => `//${p.replace(/^\/+/, "")}`;
7493
- return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
7494
- }
7495
- function droneSettingsJson(realHome, swarmHome) {
7496
- return JSON.stringify({ permissions: { deny: droneDenyRules(realHome, swarmHome) } });
7497
8531
  }
7498
8532
 
7499
8533
  // src/testForks.ts
@@ -7525,6 +8559,7 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
7525
8559
  }
7526
8560
 
7527
8561
  // src/launcher.ts
8562
+ init_executors();
7528
8563
  var requireFromHere = createRequire(import.meta.url);
7529
8564
  function resolveTsxCli() {
7530
8565
  try {
@@ -7535,29 +8570,12 @@ function resolveTsxCli() {
7535
8570
  );
7536
8571
  }
7537
8572
  }
7538
- var ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
7539
- function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
7540
- const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
7541
- const base3 = [
7542
- "--dangerously-skip-permissions",
7543
- ...ISOLATION_FLAGS,
7544
- "--mcp-config",
7545
- cfgPath,
7546
- ...settings ? ["--settings", settings] : [],
7547
- ...pluginFlags,
7548
- "--output-format",
7549
- "json",
7550
- "-p",
7551
- prompt
7552
- ];
7553
- const withModel = model ? ["--model", model, ...base3] : base3;
7554
- return continueSession ? ["--continue", ...withModel] : withModel;
7555
- }
8573
+ var MCP_SERVER_NAME = "overmind";
7556
8574
  function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
7557
8575
  const env = { ...base3 };
7558
8576
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
7559
8577
  if (isolate && !base3.TURBO_CACHE_DIR) {
7560
- env.TURBO_CACHE_DIR = join9(worktree, ".turbo", "cache");
8578
+ env.TURBO_CACHE_DIR = join12(worktree, ".turbo", "cache");
7561
8579
  }
7562
8580
  Object.assign(env, droneTestEnv(droneCap, base3));
7563
8581
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -7585,20 +8603,20 @@ var DAEMON_STATE_ENV = [
7585
8603
  "TASKLESS_TOKEN"
7586
8604
  ];
7587
8605
  function bundledRoleDir(kind) {
7588
- return join9(dirname3(fileURLToPath(import.meta.url)), "..", "roles", kind);
8606
+ return join12(dirname3(fileURLToPath(import.meta.url)), "..", "roles", kind);
7589
8607
  }
7590
8608
  function roleDir(worktree, kind) {
7591
- return join9(worktree, ".claude", "roles", kind);
8609
+ return join12(worktree, ".claude", "roles", kind);
7592
8610
  }
7593
8611
  function roleDirs(worktree, kind) {
7594
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync6(dir));
8612
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync7(dir));
7595
8613
  }
7596
8614
  function nextRunLogPath(runLogsDir, runId) {
7597
- const dir = join9(runLogsDir, String(runId));
7598
- mkdirSync5(dir, { recursive: true });
8615
+ const dir = join12(runLogsDir, String(runId));
8616
+ mkdirSync7(dir, { recursive: true });
7599
8617
  let files = [];
7600
8618
  try {
7601
- files = readdirSync5(dir);
8619
+ files = readdirSync7(dir);
7602
8620
  } catch {
7603
8621
  }
7604
8622
  let next = 0;
@@ -7606,7 +8624,7 @@ function nextRunLogPath(runLogsDir, runId) {
7606
8624
  const m = /^(\d+)\.json/.exec(f);
7607
8625
  if (m) next = Math.max(next, Number(m[1]) + 1);
7608
8626
  }
7609
- return join9(dir, `${next}.json`);
8627
+ return join12(dir, `${next}.json`);
7610
8628
  }
7611
8629
  function openNextRunLog(runLogsDir, runId) {
7612
8630
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -7641,57 +8659,63 @@ function makeLauncher(cfg) {
7641
8659
  );
7642
8660
  }
7643
8661
  const channelEnv = cfg.channel && channelToken ? { SWARM_DRONE_CHANNEL_URL: cfg.channel.url, SWARM_DRONE_CHANNEL_TOKEN: channelToken } : { SWARM_DB: cfg.dbPath };
7644
- const mcpConfig = {
7645
- mcpServers: {
7646
- overmind: {
7647
- command: process.execPath,
7648
- args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["--conditions=development", resolveTsxCli(), cfg.overmindEntry, "--drone", "--run", String(run.id)],
7649
- // SWARM_DRONE_CHANNEL_URL/TOKEN (TASWM-295) ИЛИ SWARM_DB (откат) — см. channelEnv выше.
7650
- // Канал: дрон говорит только с демоном, файл базы ему не отдаётся вовсе. Токен подписан
7651
- // на СТАБИЛЬНЫЙ ключ прогона, поэтому переживает рестарт демона.
7652
- // SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — доверенный
7653
- // источник «чья это сессия» (read_task и отчёт о сдаче доверяют ему, а не строке,
7654
- // найденной по номеру прогона).
7655
- // SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID (TASWM-263) и SWARM_RUN_NAME
7656
- // (TASWM-265): роль, связи и имя прогона той же спавн-фиксацией. Нужны РЕЖИМУ ОТКАТА —
7657
- // ensureRunHydrated заводит ими стаб-строку в sqlite, если демон в client-режиме её там
7658
- // не заводил. На канале прогон отдаёт демон целиком, и стаб не нужен.
7659
- // SWARM_WORKSPACE_ID (TASWM-288): ВОРКСПЕЙС прогона — той же спавн-фиксацией: задачные
7660
- // вызовы (read_task, отчёт, тулы Героя) дрон делает НЕ через канал, а прямо в Taskless,
7661
- // и воркспейс им нужен верно сразу. Без него read_task/отчёт дрона
7662
- // ушли бы с глобальным пином демона — ровно то, что чинит эта задача. Пусто
7663
- // (прогон до фичи / sqlite-бэкенд) → ключ не кладём, дрон берёт глобальный пин.
7664
- // SWARM_HOME (TASWM-289): дрон едет с ТЕНЕВЫМ HOME, в котором `.taskless-swarm`
7665
- // нет вовсе, — а дрон-MCP из этого же дерева процессов читает oauth.json
7666
- // (connectTaskless в runDroneServer), пульс овермайнда и workspace.json. Наследовать
7667
- // ему нечего, поэтому реальный SWARM_HOME кладём явно. Дрон этот конфиг прочитать
7668
- // может, но нового ему это не даёт: его собственный worktree лежит ВНУТРИ
7669
- // SWARM_HOME, путь он и так знает (см. «чего этот модуль не делает» в droneHome.ts).
7670
- env: {
7671
- ...channelEnv,
7672
- ...cfg.droneIsolation ? { SWARM_HOME: cfg.droneIsolation.swarmHome } : {},
7673
- SWARM_TASK_SLUG: run.task_slug,
7674
- SWARM_RUN_KIND: run.kind,
7675
- SWARM_RUN_NAME: run.name,
7676
- ...run.workspace_id ? { SWARM_WORKSPACE_ID: run.workspace_id } : {},
7677
- ...run.reviews_run_id != null ? { SWARM_REVIEWS_RUN_ID: String(run.reviews_run_id) } : {},
7678
- ...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
7679
- }
7680
- }
8662
+ const mcp = {
8663
+ name: MCP_SERVER_NAME,
8664
+ command: process.execPath,
8665
+ args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["--conditions=development", resolveTsxCli(), cfg.overmindEntry, "--drone", "--run", String(run.id)],
8666
+ // SWARM_DRONE_CHANNEL_URL/TOKEN (TASWM-295) ИЛИ SWARM_DB (откат) — см. channelEnv выше.
8667
+ // Канал: дрон говорит только с демоном, файл базы ему не отдаётся вовсе. Токен подписан
8668
+ // на СТАБИЛЬНЫЙ ключ прогона, поэтому переживает рестарт демона.
8669
+ // SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — доверенный
8670
+ // источник «чья это сессия» (read_task и отчёт о сдаче доверяют ему, а не строке,
8671
+ // найденной по номеру прогона).
8672
+ // SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID (TASWM-263) и SWARM_RUN_NAME
8673
+ // (TASWM-265): роль, связи и имя прогона той же спавн-фиксацией. Нужны РЕЖИМУ ОТКАТА —
8674
+ // ensureRunHydrated заводит ими стаб-строку в sqlite, если демон в client-режиме её там
8675
+ // не заводил. На канале прогон отдаёт демон целиком, и стаб не нужен.
8676
+ // SWARM_WORKSPACE_ID (TASWM-288): ВОРКСПЕЙС прогона — той же спавн-фиксацией: задачные
8677
+ // вызовы (read_task, отчёт, тулы Героя) дрон делает НЕ через канал, а прямо в Taskless,
8678
+ // и воркспейс им нужен верно сразу. Без него read_task/отчёт дрона
8679
+ // ушли бы с глобальным пином демона — ровно то, что чинит эта задача. Пусто
8680
+ // (прогон до фичи / sqlite-бэкенд) → ключ не кладём, дрон берёт глобальный пин.
8681
+ // SWARM_HOME (TASWM-289): дрон едет с ТЕНЕВЫМ HOME, в котором `.taskless-swarm`
8682
+ // нет вовсе, — а дрон-MCP из этого же дерева процессов читает oauth.json
8683
+ // (connectTaskless в runDroneServer), пульс овермайнда и workspace.json. Наследовать
8684
+ // ему нечего, поэтому реальный SWARM_HOME кладём явно. Дрон этот конфиг прочитать
8685
+ // может, но нового ему это не даёт: его собственный worktree лежит ВНУТРИ
8686
+ // SWARM_HOME, путь он и так знает (см. «чего этот модуль не делает» в droneHome.ts).
8687
+ env: {
8688
+ ...channelEnv,
8689
+ ...cfg.droneIsolation ? { SWARM_HOME: cfg.droneIsolation.swarmHome } : {},
8690
+ SWARM_TASK_SLUG: run.task_slug,
8691
+ SWARM_RUN_KIND: run.kind,
8692
+ SWARM_RUN_NAME: run.name,
8693
+ ...run.workspace_id ? { SWARM_WORKSPACE_ID: run.workspace_id } : {},
8694
+ ...run.reviews_run_id != null ? { SWARM_REVIEWS_RUN_ID: String(run.reviews_run_id) } : {},
8695
+ ...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
7681
8696
  }
7682
8697
  };
7683
- const cfgPath = join9(worktree, ".swarm-mcp.json");
7684
- writeFileSync7(cfgPath, JSON.stringify(mcpConfig, null, 2));
7685
- const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
7686
- const pluginDirs = roleDirs(worktree, run.kind);
7687
- const realHome = cfg.droneIsolation?.realHome ?? homedir3();
8698
+ const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8699
+ const realHome = cfg.droneIsolation?.realHome ?? homedir4();
7688
8700
  const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
7689
- const settings = cfg.droneIsolation ? droneSettingsJson(realHome, cfg.droneIsolation.swarmHome) : void 0;
7690
- if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync6(join9(cfg.droneIsolation.swarmHome, "token"))) {
8701
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync7(join12(cfg.droneIsolation.swarmHome, "token"))) {
7691
8702
  console.error(
7692
- `[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.`
8703
+ `[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.`
7693
8704
  );
7694
8705
  }
8706
+ const executor = cfg.executor ?? resolveExecutor();
8707
+ const plan = executor.plan({
8708
+ worktree,
8709
+ prompt,
8710
+ continueSession: !!continueSession,
8711
+ model,
8712
+ mcp,
8713
+ skillDirs: roleDirs(worktree, run.kind),
8714
+ // Закрытые дрону пути имеют смысл только при теневом HOME: без него дрон и так
8715
+ // сидит в HOME демона, и запирать половину дверей — самообман.
8716
+ denyPaths: cfg.droneIsolation ? dronePrivatePaths(realHome, cfg.droneIsolation.swarmHome) : [],
8717
+ realHome
8718
+ });
7695
8719
  let stdio = "ignore";
7696
8720
  let outFd;
7697
8721
  let errFd;
@@ -7701,15 +8725,16 @@ function makeLauncher(cfg) {
7701
8725
  errFd = fds.err ?? void 0;
7702
8726
  stdio = ["ignore", outFd, errFd ?? "ignore"];
7703
8727
  }
7704
- const child = spawn2(cfg.claudeBin ?? resolveClaudeBin(), claudeArgs(cfgPath, prompt, continueSession, model, pluginDirs, settings), {
8728
+ const child = spawn2(cfg.bin ?? plan.bin, plan.args, {
7705
8729
  cwd: worktree,
7706
8730
  detached: true,
7707
8731
  stdio,
7708
8732
  // Пер-worktree TURBO_CACHE_DIR (TASWM-232): дрон не отыгрывает кеш соседнего
7709
8733
  // worktree, проверки исполняются реально. Теневой HOME + чистка env от состояния
7710
8734
  // демона (TASWM-289). Доля тестовых форков от числа дронов на хосте (TASWM-246).
7711
- // Остальной env демона наследуется, как раньше.
7712
- env: droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null)
8735
+ // Остальной env демона наследуется, как раньше. Добавки адаптера — последними:
8736
+ // они про его собственное состояние, и перебивать их ройным нечем.
8737
+ env: { ...droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null), ...plan.env }
7713
8738
  });
7714
8739
  if (outFd !== void 0) closeSync3(outFd);
7715
8740
  if (errFd !== void 0) closeSync3(errFd);
@@ -7723,25 +8748,25 @@ function makeLauncher(cfg) {
7723
8748
  init_droneChannel();
7724
8749
 
7725
8750
  // src/git.ts
7726
- import { existsSync as existsSync7, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
8751
+ import { existsSync as existsSync8, readdirSync as readdirSync8, statSync as statSync8 } from "fs";
7727
8752
  import { appendFile, mkdir, readFile, rm } from "fs/promises";
7728
- import { isAbsolute as isAbsolute2, join as join10 } from "path";
7729
- import { $ as $4 } from "execa";
8753
+ import { isAbsolute as isAbsolute2, join as join13 } from "path";
8754
+ import { $ as $5 } from "execa";
7730
8755
  function isLinkedWorktree(dir) {
7731
8756
  try {
7732
- return statSync7(join10(dir, ".git")).isFile();
8757
+ return statSync8(join13(dir, ".git")).isFile();
7733
8758
  } catch {
7734
8759
  return false;
7735
8760
  }
7736
8761
  }
7737
8762
  async function detachWorktree(repo, worktree) {
7738
- await $4({ reject: false })`git -C ${repo} worktree remove --force ${worktree}`;
8763
+ await $5({ reject: false })`git -C ${repo} worktree remove --force ${worktree}`;
7739
8764
  if (await ensureWorktreeDirGone(worktree)) {
7740
- await $4({ reject: false })`git -C ${repo} worktree prune`;
8765
+ await $5({ reject: false })`git -C ${repo} worktree prune`;
7741
8766
  }
7742
8767
  }
7743
8768
  async function ensureWorktreeDirGone(worktree) {
7744
- if (!existsSync7(worktree)) return false;
8769
+ if (!existsSync8(worktree)) return false;
7745
8770
  if (!isLinkedWorktree(worktree)) {
7746
8771
  console.error(
7747
8772
  `[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.`
@@ -7756,16 +8781,16 @@ async function ensureWorktreeDirGone(worktree) {
7756
8781
  );
7757
8782
  return true;
7758
8783
  }
7759
- if (existsSync7(worktree)) {
8784
+ if (existsSync8(worktree)) {
7760
8785
  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.`);
7761
8786
  }
7762
8787
  return true;
7763
8788
  }
7764
8789
  async function removeWorktree(repo, path) {
7765
- const branch = (await $4({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
8790
+ const branch = (await $5({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
7766
8791
  await cleanupWorktreeProcesses(path);
7767
8792
  await detachWorktree(repo, path);
7768
- if (branch) await $4({ reject: false })`git -C ${repo} branch -D ${branch}`;
8793
+ if (branch) await $5({ reject: false })`git -C ${repo} branch -D ${branch}`;
7769
8794
  }
7770
8795
 
7771
8796
  // src/cli.ts
@@ -7778,7 +8803,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
7778
8803
 
7779
8804
  // src/attachments.ts
7780
8805
  import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
7781
- import { join as join12 } from "path";
8806
+ import { join as join15 } from "path";
7782
8807
  var ATTACH_MAX_TOTAL_BYTES = 24 * 1024 * 1024;
7783
8808
  var ATTACH_MAX_FILE_BYTES = 8 * 1024 * 1024;
7784
8809
  var IMAGE_EXT = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "avif", "heic"]);
@@ -7788,7 +8813,7 @@ var KNOWN_EXT = /* @__PURE__ */ new Set([...IMAGE_EXT, ...DOC_EXT, ...TEXT_EXT])
7788
8813
 
7789
8814
  // src/shotUpload.ts
7790
8815
  import { readFile as fsReadFile } from "fs/promises";
7791
- import { basename as basename3, isAbsolute as isAbsolute3, join as join13 } from "path";
8816
+ import { basename as basename3, isAbsolute as isAbsolute3, join as join16 } from "path";
7792
8817
  var SHOT_MAX_FILE_BYTES = 10 * 1024 * 1024;
7793
8818
 
7794
8819
  // src/taskless.ts
@@ -7798,8 +8823,8 @@ init_agentToken();
7798
8823
  // src/workspace.ts
7799
8824
  init_config();
7800
8825
  import { readFileSync as readFileSync11, writeFileSync as writeFileSync9, unlinkSync as unlinkSync4 } from "fs";
7801
- import { join as join14 } from "path";
7802
- var WORKSPACE_PATH = join14(SWARM_HOME, "workspace.json");
8826
+ import { join as join17 } from "path";
8827
+ var WORKSPACE_PATH = join17(SWARM_HOME, "workspace.json");
7803
8828
  function loadWorkspace() {
7804
8829
  try {
7805
8830
  const w = JSON.parse(readFileSync11(WORKSPACE_PATH, "utf8"));
@@ -8110,14 +9135,14 @@ async function runShotCli(argv, deps = {}) {
8110
9135
  init_daemonLink();
8111
9136
 
8112
9137
  // src/pauseState.ts
8113
- import { existsSync as existsSync8, readFileSync as readFileSync12, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync10 } from "fs";
8114
- import { join as join15 } from "path";
9138
+ import { existsSync as existsSync9, readFileSync as readFileSync12, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync10 } from "fs";
9139
+ import { join as join18 } from "path";
8115
9140
  function pauseStatePath(home) {
8116
- return join15(home, "pause.json");
9141
+ return join18(home, "pause.json");
8117
9142
  }
8118
9143
  var ACTIVE = { paused: false, at: "", source: "cli" };
8119
9144
  function loadPauseState(home, io = {}) {
8120
- const exists = io.exists ?? existsSync8;
9145
+ const exists = io.exists ?? existsSync9;
8121
9146
  const readFile2 = io.readFile ?? ((p) => readFileSync12(p, "utf8"));
8122
9147
  const path = pauseStatePath(home);
8123
9148
  if (!exists(path)) return ACTIVE;
@@ -8137,7 +9162,7 @@ function loadPauseState(home, io = {}) {
8137
9162
  function savePauseState(home, state, io = {}) {
8138
9163
  const path = pauseStatePath(home);
8139
9164
  if (!state.paused) {
8140
- const remove = io.remove ?? ((p) => existsSync8(p) && unlinkSync5(p));
9165
+ const remove = io.remove ?? ((p) => existsSync9(p) && unlinkSync5(p));
8141
9166
  remove(path);
8142
9167
  return;
8143
9168
  }
@@ -8417,7 +9442,7 @@ async function runDaemonLogs(argv) {
8417
9442
  console.error(`\u043B\u043E\u0433 \u041F\u0420\u041E\u0413\u041E\u041D\u0410 \u2014 \u044D\u0442\u043E \xABswarm log ${stray}\xBB (\u0431\u0435\u0437 s). \xABswarm logs\xBB \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442 \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430; \u0447\u0438\u0441\u043B\u043E \u0441\u0442\u0440\u043E\u043A \u2014 \xABswarm logs -n 200\xBB.`);
8418
9443
  return 1;
8419
9444
  }
8420
- if (!existsSync13(DAEMON_LOG_PATH)) {
9445
+ if (!existsSync14(DAEMON_LOG_PATH)) {
8421
9446
  console.error(`\u043B\u043E\u0433\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442: ${DAEMON_LOG_PATH}`);
8422
9447
  console.error("\u043E\u043D \u043F\u0438\u0448\u0435\u0442\u0441\u044F \u043F\u0440\u0438 \u0444\u043E\u043D\u043E\u0432\u043E\u043C \u0437\u0430\u043F\u0443\u0441\u043A\u0435 (swarm start). \u0414\u0435\u043C\u043E\u043D \u043F\u043E\u0434\u043D\u044F\u0442 \u0432 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435 (--foreground) \u2014 \u0432\u044B\u0432\u043E\u0434 \u0438\u0434\u0451\u0442 \u0442\u0443\u0434\u0430.");
8423
9448
  return 1;
@@ -8425,12 +9450,12 @@ async function runDaemonLogs(argv) {
8425
9450
  const head = tailFile(DAEMON_LOG_PATH, lines);
8426
9451
  if (head) console.log(head);
8427
9452
  if (!follow) return 0;
8428
- let pos = statSync8(DAEMON_LOG_PATH).size;
9453
+ let pos = statSync9(DAEMON_LOG_PATH).size;
8429
9454
  for (; ; ) {
8430
9455
  await sleep2(500);
8431
9456
  let size;
8432
9457
  try {
8433
- size = statSync8(DAEMON_LOG_PATH).size;
9458
+ size = statSync9(DAEMON_LOG_PATH).size;
8434
9459
  } catch {
8435
9460
  continue;
8436
9461
  }
@@ -8614,6 +9639,10 @@ function usage() {
8614
9639
  swarm spawn <slug> [area] [\u0444\u043B\u0430\u0433\u0438 \u043D\u0438\u0436\u0435]
8615
9640
  \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u044C
8616
9641
  swarm ls [\u0441\u0442\u0430\u0442\u0443\u0441] \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0436\u0438\u0432\u043E\u0433\u043E \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0440\u043E\u0432\u043D\u043E \u0442\u0435, \u0447\u0442\u043E \u0432 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u0435
9642
+ swarm zones [check|suggest <\u0442\u0435\u043A\u0441\u0442|slug>]
9643
+ \u0437\u043E\u043D\u044B \u043A\u043E\u0434\u0430, \u0432\u044B\u0432\u0435\u0434\u0435\u043D\u043D\u044B\u0435 \u0438\u0437 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0440\u0435\u043F\u043E (\u043A\u0430\u043A\u0438\u0435 \u0444\u0430\u0439\u043B\u044B \u043C\u0435\u043D\u044F\u044E\u0442\u0441\u044F
9644
+ \u0432\u043C\u0435\u0441\u0442\u0435, \u0447\u0442\u043E \u0442\u0443\u0442 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442). check \u2014 \u0441\u0432\u0435\u0440\u043A\u0430 \u0441 \u0440\u0443\u0447\u043D\u043E\u0439
9645
+ \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u043E\u0439 area:*; suggest \u2014 \u043A\u0430\u043A\u0443\u044E \u0437\u043E\u043D\u0443 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0435
8617
9646
  swarm wake <runId> \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D (id \u0438\u0437 swarm ls); \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E \u2192 \u043D\u0430\u0447\u043D\u0451\u0442 \u0441 \u043D\u0443\u043B\u044F
8618
9647
  swarm cancel <slug> <\u043F\u0440\u0438\u0447\u0438\u043D\u0430\u2026>
8619
9648
  \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430: \u0441\u0442\u0430\u0442\u0443\u0441 \xAB\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\xBB \u0441 \u043F\u0440\u0438\u0447\u0438\u043D\u043E\u0439, \u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u043E\u0441\u0442\u0430\u043D\u0435\u0442\u0441\u044F
@@ -8666,10 +9695,10 @@ function effectiveWorktreesDir() {
8666
9695
  legacyDir: WORKTREES_LEGACY_DIR,
8667
9696
  targetDir: WORKTREES_DIR,
8668
9697
  envOverride: process.env.SWARM_WORKTREES || "",
8669
- exists: (d) => existsSync13(d),
9698
+ exists: (d) => existsSync14(d),
8670
9699
  isNonEmptyDir: (d) => {
8671
9700
  try {
8672
- return readdirSync7(d).length > 0;
9701
+ return readdirSync9(d).length > 0;
8673
9702
  } catch {
8674
9703
  return false;
8675
9704
  }
@@ -8694,6 +9723,80 @@ async function makeCliStore(repo) {
8694
9723
  client.reconcileRuntime({ worktreesDir: effectiveWorktreesDir(), listProcesses: listDroneProcesses });
8695
9724
  return client;
8696
9725
  }
9726
+ async function runZones(db2, repo, args) {
9727
+ const manual = new Map(db2.listRuns().map((r) => [r.task_slug, r.area]));
9728
+ const raw = await readTaskHistory(repo);
9729
+ let fromScope = 0;
9730
+ for (const e of raw) {
9731
+ if (manual.get(e.slug)) continue;
9732
+ const scope = scopeFromText(e.text);
9733
+ if (scope) {
9734
+ manual.set(e.slug, scope);
9735
+ fromScope++;
9736
+ }
9737
+ }
9738
+ const history = withManualAreas(raw, manual);
9739
+ const model = buildZoneModel(history);
9740
+ if (!hasZoneModel(model)) {
9741
+ console.log(`\u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u043C\u0430\u043B\u043E (${history.length} \u0437\u0430\u0434\u0430\u0447 \u0441\u043E \u0441\u043B\u0430\u0433\u043E\u043C \u0432 \u043A\u043E\u043C\u043C\u0438\u0442\u0430\u0445) \u2014 \u0437\u043E\u043D\u044B \u043D\u0435 \u0432\u044B\u0432\u043E\u0434\u044F\u0442\u0441\u044F, \u0440\u0430\u0437\u0434\u0430\u0447\u0430 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u043E \u043B\u0435\u0439\u0431\u043B\u0430\u043C area:*`);
9742
+ return;
9743
+ }
9744
+ const sub = args[0];
9745
+ if (sub === "check") {
9746
+ const a = checkZoneAgreement(model);
9747
+ if (!a.checked) {
9748
+ console.log("\u043D\u0438 \u0443 \u043E\u0434\u043D\u043E\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u043D\u0435\u0442 \u0440\u0443\u0447\u043D\u043E\u0439 \u0437\u043E\u043D\u044B \u2014 \u0441\u0432\u0435\u0440\u044F\u0442\u044C \u043D\u0435 \u0441 \u0447\u0435\u043C (\u043F\u0440\u043E\u0441\u0442\u0430\u0432\u044C area:* \u0445\u043E\u0442\u044F \u0431\u044B \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u043C)");
9749
+ return;
9750
+ }
9751
+ console.log(
9752
+ `\u0441\u0432\u0435\u0440\u043A\u0430 \u0441 \u0440\u0443\u0447\u043D\u043E\u0439 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u043E\u0439: \u0442\u043E\u0447\u043D\u043E ${a.matched}/${a.checked} (${Math.round(a.rate * 100)}%), \u043D\u0435 \u043F\u0440\u043E\u0442\u0438\u0432\u043E\u0440\u0435\u0447\u0438\u0442 ${Math.round(a.looseRate * 100)}% (\u0440\u0443\u0447\u043D\u043E\u0439 \u043B\u0435\u0439\u0431\u043B \u0448\u0438\u0440\u0435 \u0432\u044B\u0432\u0435\u0434\u0435\u043D\u043D\u043E\u0439 \u0437\u043E\u043D\u044B)`
9753
+ );
9754
+ if (fromScope) {
9755
+ console.log(
9756
+ ` \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u0438: area:* \u0441 \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u0432 \u0440\u043E\u044F, \u0430 \u0433\u0434\u0435 \u0435\u0451 \u043D\u0435\u0442 (${fromScope} \u0437\u0430\u0434\u0430\u0447) \u2014 scope \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u043E\u0432 \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432 (\`feat(calls):\`). \u0421\u043B\u043E\u0432\u0430\u0440\u044C scope \u0448\u0438\u0440\u0435 \u0437\u043E\u043D: \xABci\xBB, \xABapi,web\xBB \u0437\u043E\u043D\u0430\u043C\u0438 \u043A\u043E\u0434\u0430 \u043D\u0435 \u044F\u0432\u043B\u044F\u044E\u0442\u0441\u044F \u0432\u043E\u0432\u0441\u0435.`
9757
+ );
9758
+ }
9759
+ const bad = a.rows.filter((r) => r.verdict === "mismatch");
9760
+ if (bad.length) {
9761
+ console.log(`
9762
+ \u0440\u0430\u0441\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u043F\u043E \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443 (${bad.length}) \u2014 \u0437\u0434\u0435\u0441\u044C \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u0430 \u0438 \u0444\u0430\u0439\u043B\u044B \u0433\u043E\u0432\u043E\u0440\u044F\u0442 \u0440\u0430\u0437\u043D\u043E\u0435:`);
9763
+ for (const r of bad.slice(0, 30)) console.log(` ${r.slug.padEnd(12)} \u0440\u0430\u0437\u043C\u0435\u0447\u0435\u043D\u043E ${r.manual} \u2192 \u043F\u043E \u0444\u0430\u0439\u043B\u0430\u043C ${r.suggested}`);
9764
+ }
9765
+ return;
9766
+ }
9767
+ if (sub === "suggest") {
9768
+ const query = args.slice(1).join(" ").trim();
9769
+ if (!query) {
9770
+ console.error('\u043D\u0443\u0436\u0435\u043D \u0442\u0435\u043A\u0441\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u043B\u0438 \u0435\u0451 \u0441\u043B\u0430\u0433: swarm zones suggest "\u0437\u0432\u043E\u043D\u043A\u0438: \u0442\u0430\u0439\u043C\u043B\u0430\u0439\u043D \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440\u0430"');
9771
+ process.exit(1);
9772
+ }
9773
+ const bySlug = db2.listRuns().find((r) => r.task_slug === query);
9774
+ const text = bySlug ? [bySlug.task_title ?? "", bySlug.task_desc ?? ""].join(" ").trim() || query : query;
9775
+ const guesses = suggestZonesFromText(model, text);
9776
+ if (!guesses.length) {
9777
+ console.log("\u043F\u043E \u044D\u0442\u043E\u043C\u0443 \u0442\u0435\u043A\u0441\u0442\u0443 \u0437\u043E\u043D\u0430 \u043D\u0435 \u0432\u044B\u0432\u043E\u0434\u0438\u0442\u0441\u044F \u2014 \u0438\u0441\u0442\u043E\u0440\u0438\u044F \u043D\u0438\u0447\u0435\u0433\u043E \u043F\u043E\u0445\u043E\u0436\u0435\u0433\u043E \u043D\u0435 \u043F\u043E\u043C\u043D\u0438\u0442");
9778
+ return;
9779
+ }
9780
+ for (const g of guesses) console.log(`${g.zone.padEnd(20)} ${String(Math.round(g.confidence * 100)).padStart(3)}% ${g.why}`);
9781
+ const files = predictFiles(model, { text });
9782
+ if (files.length) {
9783
+ console.log("\n\u0432\u0435\u0440\u043E\u044F\u0442\u043D\u043E \u0442\u0440\u043E\u043D\u0435\u0442:");
9784
+ for (const f of files.slice(0, 10)) console.log(` ${f.path} (${f.why})`);
9785
+ }
9786
+ return;
9787
+ }
9788
+ console.log(`\u0437\u043E\u043D\u044B \u043F\u043E \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0440\u0435\u043F\u043E (${model.tasks} \u0437\u0430\u0434\u0430\u0447):
9789
+ `);
9790
+ for (const z of model.zones.filter((x) => x.zone !== SPINE_ZONE)) {
9791
+ console.log(`${z.zone.padEnd(20)} \u0437\u0430\u0434\u0430\u0447 ${String(z.tasks).padStart(4)} \u0444\u0430\u0439\u043B\u043E\u0432 ${String(z.files.length).padStart(4)} \u043D\u0430\u043F\u0440.: ${z.files.slice(0, 2).join(", ")}`);
9792
+ }
9793
+ if (model.spine.length) {
9794
+ console.log(`
9795
+ \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${model.spine.length} \u0444\u0430\u0439\u043B\u043E\u0432) \u2014 \u0437\u0430\u0434\u0430\u0447\u0438 \u043F\u0440\u043E \u043D\u0435\u0433\u043E \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:`);
9796
+ for (const f of model.spine.slice(0, 15)) console.log(` ${f}`);
9797
+ }
9798
+ console.log("\n\u0441\u0432\u0435\u0440\u0438\u0442\u044C \u0441 \u0440\u0443\u0447\u043D\u043E\u0439 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u043E\u0439: swarm zones check");
9799
+ }
8697
9800
  async function finishClient(db2) {
8698
9801
  if (!(db2 instanceof SwarmClient)) return;
8699
9802
  await db2.flush();
@@ -8993,6 +10096,9 @@ switch (cmd) {
8993
10096
  overmindEntry: OVERMIND_ENTRY,
8994
10097
  dbPath: DB_PATH,
8995
10098
  runLogsDir: RUN_LOGS_DIR,
10099
+ // Тот же исполнитель, что у демона (TASWM-349): `swarm wake` при мёртвом демоне не
10100
+ // должен внезапно поднимать дрона другим харнессом.
10101
+ executor: EXECUTOR,
8996
10102
  droneIsolation: DRONE_ISOLATION,
8997
10103
  ...DRONE_CHANNEL_ENABLED ? {
8998
10104
  channel: {
@@ -9088,6 +10194,20 @@ switch (cmd) {
9088
10194
  db.close();
9089
10195
  process.exit(code);
9090
10196
  }
10197
+ // TASWM-348: зоны кода, выведенные из истории репозитория.
10198
+ // swarm zones — какие зоны видит рой и что считает общим хребтом
10199
+ // swarm zones check — сверка с ручной разметкой area:* на прошлых задачах
10200
+ // swarm zones suggest <текст|slug> — какую зону предложить задаче ПРИ ПОСТАНОВКЕ
10201
+ case "zones": {
10202
+ const { repo: repoFlag, rest } = takeRepo(rawArgs);
10203
+ const repo = repoFlag ?? SWARM_REPO;
10204
+ if (!repo) {
10205
+ console.error("\u043D\u0435\u0442 \u0440\u0435\u043F\u043E: \u0437\u0430\u0434\u0430\u0439 --repo <path> \u0438\u043B\u0438 SWARM_REPO");
10206
+ process.exit(1);
10207
+ }
10208
+ await runZones(db, repo, rest);
10209
+ break;
10210
+ }
9091
10211
  case "web": {
9092
10212
  const web = startWeb({ db });
9093
10213
  console.log(`\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430: http://127.0.0.1:${web.port} \u2190 \u043E\u0442\u043A\u0440\u043E\u0439 \u0438 \u0432\u043E\u0439\u0434\u0438 \u0447\u0435\u0440\u0435\u0437 Taskless`);