@taskless-app/swarm 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +59 -0
  2. package/dist/cli.js +1551 -312
  3. package/dist/overmind.js +2249 -636
  4. package/package.json +2 -1
package/dist/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,16 +604,30 @@ 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 SWARM_PACKAGE_NAME, MANUAL_UPDATE_COMMAND, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_ACK_GRACE_MS, HOST_COMMAND_MUTE_REASON, RUN_SCOPED_COMMAND_KINDS, RUN_SCOPED;
608
617
  var init_hostCommands = __esm({
609
618
  "../swarm-core/src/hostCommands.ts"() {
610
619
  "use strict";
620
+ SWARM_PACKAGE_NAME = "@taskless-app/swarm";
621
+ MANUAL_UPDATE_COMMAND = `npm i -g ${SWARM_PACKAGE_NAME}@latest`;
611
622
  HOST_COMMAND_STATUSES = ["pending", "delivered", "running", "done", "failed", "rejected", "expired"];
612
623
  TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "rejected", "expired"]);
613
624
  HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
614
625
  HOST_COMMAND_TTL_MS = 30 * 6e4;
615
626
  HOST_COMMAND_KEEPALIVE_MS = 10 * 6e4;
627
+ HOST_COMMAND_ACK_GRACE_MS = 5 * 6e4;
628
+ HOST_COMMAND_MUTE_REASON = `\u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B\u0430 \u0438\u0441\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u2014 \u0432\u0435\u0440\u043E\u044F\u0442\u043D\u043E, \u0434\u0435\u043C\u043E\u043D \u0441\u0442\u0430\u0440\u0435\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B. \u041E\u0431\u043D\u043E\u0432\u0438 \u0432\u0440\u0443\u0447\u043D\u0443\u044E: ${MANUAL_UPDATE_COMMAND}`;
629
+ RUN_SCOPED_COMMAND_KINDS = ["stop_run", "wake_run"];
630
+ RUN_SCOPED = new Set(RUN_SCOPED_COMMAND_KINDS);
616
631
  }
617
632
  });
618
633
 
@@ -624,6 +639,420 @@ var init_relief = __esm({
624
639
  }
625
640
  });
626
641
 
642
+ // ../swarm-core/src/cost.ts
643
+ var EMPTY_TASK_COST;
644
+ var init_cost = __esm({
645
+ "../swarm-core/src/cost.ts"() {
646
+ "use strict";
647
+ EMPTY_TASK_COST = Object.freeze({
648
+ costUsd: 0,
649
+ tokensIn: 0,
650
+ tokensOut: 0,
651
+ durationMs: 0,
652
+ runs: 0,
653
+ respawns: 0
654
+ });
655
+ }
656
+ });
657
+
658
+ // ../swarm-core/src/runGuards.ts
659
+ var DEFAULT_RUN_LIMITS;
660
+ var init_runGuards = __esm({
661
+ "../swarm-core/src/runGuards.ts"() {
662
+ "use strict";
663
+ DEFAULT_RUN_LIMITS = {
664
+ maxCostUsd: 20,
665
+ runTimeoutMs: 8 * 60 * 6e4,
666
+ stepTimeoutMs: 90 * 6e4,
667
+ lostMs: 30 * 6e4
668
+ };
669
+ }
670
+ });
671
+
672
+ // ../swarm-core/src/codeZones.ts
673
+ function hasZoneModel(m) {
674
+ return m.tasks > 0 && m.zones.length > 0;
675
+ }
676
+ function testFileFeature(basename5) {
677
+ const head = basename5.replace(/\.(test|spec)\.[a-z]+$/i, "").split(".")[0].toLowerCase();
678
+ return head && !GENERIC_DIRS.has(head) ? head : null;
679
+ }
680
+ function pathZoneCandidate(path) {
681
+ const parts = path.split("/").filter(Boolean);
682
+ if (parts.length <= 1) return "root";
683
+ if (WORKSPACE_DIRS.has(parts[0]) && parts.length >= 3) {
684
+ const pkg = parts[1];
685
+ if (parts[2] === "src" && parts.length >= 5) {
686
+ const feature = parts[3];
687
+ if (!GENERIC_DIRS.has(feature)) return feature;
688
+ }
689
+ if (GENERIC_TEST_DIRS.has(parts[2])) {
690
+ const feature = testFileFeature(parts[parts.length - 1]);
691
+ if (feature) return feature;
692
+ }
693
+ return pkg;
694
+ }
695
+ if (parts[0] === "src" && parts.length >= 3 && !GENERIC_DIRS.has(parts[1])) return parts[1];
696
+ return parts[0];
697
+ }
698
+ function tokenize(text) {
699
+ const out = [];
700
+ for (const raw of text.split(/[^\p{L}\p{N}]+/u)) {
701
+ if (!raw) continue;
702
+ for (const piece of raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")) {
703
+ const t = piece.toLowerCase();
704
+ if (t.length < 3 || STOP_TERMS.has(t) || /^\d+$/.test(t)) continue;
705
+ out.push(t);
706
+ }
707
+ }
708
+ return out;
709
+ }
710
+ function normalizeZoneName(raw) {
711
+ if (!raw) return null;
712
+ const s = raw.trim().toLowerCase().replace(/^area:/, "");
713
+ return s || null;
714
+ }
715
+ function bump(m, k, by = 1) {
716
+ m.set(k, (m.get(k) ?? 0) + by);
717
+ }
718
+ function topKey(m) {
719
+ let best = null;
720
+ let bestN = 0;
721
+ let total = 0;
722
+ for (const [k, n] of m) {
723
+ total += n;
724
+ if (n > bestN) {
725
+ best = k;
726
+ bestN = n;
727
+ }
728
+ }
729
+ return best === null || total === 0 ? null : { key: best, count: bestN, share: bestN / total };
730
+ }
731
+ function buildZoneModel(entries, opts = {}) {
732
+ const o = { ...DEFAULTS, ...opts };
733
+ const tasks = entries.filter((e) => e.files.length > 0);
734
+ if (tasks.length < o.minTasks) return EMPTY_ZONE_MODEL;
735
+ const fileTasks = /* @__PURE__ */ new Map();
736
+ const candidate = /* @__PURE__ */ new Map();
737
+ for (const t of tasks) {
738
+ for (const f of new Set(t.files)) {
739
+ bump(fileTasks, f);
740
+ if (!candidate.has(f)) candidate.set(f, pathZoneCandidate(f));
741
+ }
742
+ }
743
+ const zoneOf = new Map(candidate);
744
+ for (const [file, seen] of fileTasks) {
745
+ if (seen < 2) continue;
746
+ const votes = /* @__PURE__ */ new Map();
747
+ for (const t of tasks) {
748
+ const files = [...new Set(t.files)];
749
+ if (!files.includes(file)) continue;
750
+ const others = files.filter((f) => f !== file);
751
+ if (!others.length) continue;
752
+ for (const f of others) bump(votes, candidate.get(f), 1 / others.length);
753
+ }
754
+ const top = topKey(votes);
755
+ if (top && top.share >= 0.6 && top.key !== candidate.get(file)) zoneOf.set(file, top.key);
756
+ }
757
+ const voteWeight = (file) => 1 / Math.log2(2 + (fileTasks.get(file) ?? 1));
758
+ const taskZonePre = /* @__PURE__ */ new Map();
759
+ for (const t of tasks) {
760
+ const votes = /* @__PURE__ */ new Map();
761
+ for (const f of new Set(t.files)) bump(votes, zoneOf.get(f), voteWeight(f));
762
+ const top = topKey(votes);
763
+ if (top) taskZonePre.set(t.slug, top.key);
764
+ }
765
+ const fileZones = /* @__PURE__ */ new Map();
766
+ for (const t of tasks) {
767
+ const tz = taskZonePre.get(t.slug);
768
+ if (!tz) continue;
769
+ for (const f of new Set(t.files)) {
770
+ let m = fileZones.get(f);
771
+ if (!m) fileZones.set(f, m = /* @__PURE__ */ new Map());
772
+ bump(m, tz);
773
+ }
774
+ }
775
+ const spineSet = /* @__PURE__ */ new Set();
776
+ for (const [file, seen] of fileTasks) {
777
+ if (seen < o.spineMinTasks) continue;
778
+ const zonesOfFile = fileZones.get(file);
779
+ const dominant = zonesOfFile ? topKey(zonesOfFile) : null;
780
+ if ((zonesOfFile?.size ?? 0) < o.spineMinZones) continue;
781
+ if (dominant && dominant.share >= o.spineDominantShare) continue;
782
+ spineSet.add(file);
783
+ }
784
+ const fileZone = /* @__PURE__ */ new Map();
785
+ for (const [f, z] of zoneOf) if (!spineSet.has(f)) fileZone.set(f, z);
786
+ const taskZone = /* @__PURE__ */ new Map();
787
+ for (const t of tasks) {
788
+ const votes = /* @__PURE__ */ new Map();
789
+ for (const f of new Set(t.files)) {
790
+ const z = fileZone.get(f);
791
+ if (z) bump(votes, z, voteWeight(f));
792
+ }
793
+ const top = topKey(votes);
794
+ taskZone.set(t.slug, top ? top.key : SPINE_ZONE);
795
+ }
796
+ const labelVotes = /* @__PURE__ */ new Map();
797
+ for (const t of tasks) {
798
+ const manual = normalizeZoneName(t.area);
799
+ const z = taskZone.get(t.slug);
800
+ if (!manual || !z || z === SPINE_ZONE || manual === SPINE_ZONE) continue;
801
+ let m = labelVotes.get(z);
802
+ if (!m) labelVotes.set(z, m = /* @__PURE__ */ new Map());
803
+ bump(m, manual);
804
+ }
805
+ const rename = /* @__PURE__ */ new Map();
806
+ const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
807
+ const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
808
+ const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
809
+ return sum(b[1]) - sum(a[1]) || a[0].localeCompare(b[0]);
810
+ });
811
+ for (const [zone, votes] of clustersByEvidence) {
812
+ const top = topKey(votes);
813
+ if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
814
+ rename.set(zone, top.key);
815
+ takenNames.add(top.key);
816
+ }
817
+ const renamed = (z) => rename.get(z) ?? z;
818
+ if (rename.size) {
819
+ for (const [f, z] of fileZone) fileZone.set(f, renamed(z));
820
+ for (const [s, z] of taskZone) taskZone.set(s, renamed(z));
821
+ }
822
+ const zoneTerms = /* @__PURE__ */ new Map();
823
+ const addTerm = (zone, term, w) => {
824
+ let m = zoneTerms.get(zone);
825
+ if (!m) zoneTerms.set(zone, m = /* @__PURE__ */ new Map());
826
+ bump(m, term, w);
827
+ };
828
+ for (const [f, z] of fileZone) for (const term of tokenize(f)) addTerm(z, term, 1);
829
+ for (const f of spineSet) for (const term of tokenize(f)) addTerm(SPINE_ZONE, term, 1);
830
+ for (const t of tasks) {
831
+ const z = taskZone.get(t.slug);
832
+ if (!z || !t.text) continue;
833
+ for (const term of tokenize(t.text)) addTerm(z, term, 2);
834
+ }
835
+ const termZones = /* @__PURE__ */ new Map();
836
+ for (const terms of zoneTerms.values()) for (const term of terms.keys()) bump(termZones, term);
837
+ const zoneFiles = /* @__PURE__ */ new Map();
838
+ for (const [f, z] of fileZone) {
839
+ const arr = zoneFiles.get(z) ?? [];
840
+ arr.push(f);
841
+ zoneFiles.set(z, arr);
842
+ }
843
+ const zoneTaskCount = /* @__PURE__ */ new Map();
844
+ for (const z of taskZone.values()) bump(zoneTaskCount, z);
845
+ const byFreq = (a, b) => (fileTasks.get(b) ?? 0) - (fileTasks.get(a) ?? 0) || a.localeCompare(b);
846
+ 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));
847
+ const spine = [...spineSet].sort(byFreq);
848
+ if (spineSet.size) zones.push({ zone: SPINE_ZONE, tasks: zoneTaskCount.get(SPINE_ZONE) ?? 0, files: spine });
849
+ const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
850
+ return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
851
+ }
852
+ function zoneForFiles(model, files) {
853
+ if (!hasZoneModel(model) || !files.length) return null;
854
+ const spineSet = new Set(model.spine);
855
+ const touchedSpine = files.filter((f) => spineSet.has(f));
856
+ const votes = /* @__PURE__ */ new Map();
857
+ for (const f of new Set(files)) {
858
+ const z = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
859
+ if (z) bump(votes, z);
860
+ }
861
+ const top = topKey(votes);
862
+ if (!top) {
863
+ return {
864
+ zone: SPINE_ZONE,
865
+ confidence: 1,
866
+ 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(", ")})`,
867
+ spine: true
868
+ };
869
+ }
870
+ return {
871
+ zone: top.key,
872
+ confidence: top.share,
873
+ 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}`,
874
+ spine: touchedSpine.length > 0
875
+ };
876
+ }
877
+ function suggestZonesFromText(model, text, limit = 3) {
878
+ if (!hasZoneModel(model)) return [];
879
+ const terms = tokenize(text);
880
+ if (!terms.length) return [];
881
+ const zoneCount = model.zoneTerms.size || 1;
882
+ const idf = (term) => Math.log(1 + zoneCount / (model.termZones.get(term) ?? 1));
883
+ const scores = /* @__PURE__ */ new Map();
884
+ const hits = /* @__PURE__ */ new Map();
885
+ const tasksOfZone = new Map(model.zones.map((z) => [z.zone, z.tasks]));
886
+ for (const [zone, dict] of model.zoneTerms) {
887
+ if ((tasksOfZone.get(zone) ?? 0) < 2) continue;
888
+ let dot = 0;
889
+ let norm = 0;
890
+ const matched = [];
891
+ for (const [term, w] of dict) {
892
+ const v = Math.log(1 + w) * idf(term);
893
+ norm += v * v;
894
+ }
895
+ if (!norm) continue;
896
+ for (const term of new Set(terms)) {
897
+ const w = dict.get(term);
898
+ if (!w) continue;
899
+ dot += Math.log(1 + w) * idf(term) * idf(term);
900
+ matched.push(term);
901
+ }
902
+ const score = dot / Math.sqrt(norm) * Math.log(1 + (tasksOfZone.get(zone) ?? 1));
903
+ if (score > 0) {
904
+ scores.set(zone, score);
905
+ hits.set(zone, matched);
906
+ }
907
+ }
908
+ const total = [...scores.values()].reduce((a, b) => a + b, 0);
909
+ if (!total) return [];
910
+ return [...scores.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([zone, s]) => ({
911
+ zone,
912
+ confidence: s / total,
913
+ why: `\u0441\u043E\u0432\u043F\u0430\u043B\u043E \u043F\u043E \u0441\u043B\u043E\u0432\u0430\u043C: ${(hits.get(zone) ?? []).slice(0, 5).join(", ")}`,
914
+ spine: zone === SPINE_ZONE
915
+ }));
916
+ }
917
+ function similarTasks(model, text, limit = 5) {
918
+ const terms = new Set(tokenize(text));
919
+ if (!terms.size) return [];
920
+ const scored = [];
921
+ for (const [i, e] of model.entries.entries()) {
922
+ const other = model.entryTerms[i];
923
+ if (!other?.size) continue;
924
+ let inter = 0;
925
+ for (const t of terms) if (other.has(t)) inter += Math.log(1 + (model.zoneTerms.size || 1) / (model.termZones.get(t) ?? 1));
926
+ if (inter <= 0) continue;
927
+ scored.push({ entry: e, score: inter / Math.sqrt(terms.size * other.size) });
928
+ }
929
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit);
930
+ }
931
+ function predictFiles(model, opts) {
932
+ if (!hasZoneModel(model)) return [];
933
+ const limit = opts.limit ?? 40;
934
+ const best = /* @__PURE__ */ new Map();
935
+ const put = (path, weight, why) => {
936
+ const cur = best.get(path);
937
+ if (!cur || cur.weight < weight) best.set(path, { path, weight, why });
938
+ };
939
+ if (opts.text) {
940
+ const sim = similarTasks(model, opts.text, opts.similar ?? 5);
941
+ const topScore = sim[0]?.score ?? 0;
942
+ for (const { entry, score } of sim) {
943
+ const w = topScore > 0 ? Math.min(1, score / topScore) : 0;
944
+ 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}`);
945
+ }
946
+ }
947
+ const zone = normalizeZoneName(opts.zone) ?? (opts.text ? suggestZonesFromText(model, opts.text)[0]?.zone ?? null : null);
948
+ if (zone) {
949
+ const stat = model.zones.find((z) => z.zone === zone);
950
+ 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}`));
951
+ }
952
+ return [...best.values()].sort((a, b) => b.weight - a.weight || a.path.localeCompare(b.path)).slice(0, limit);
953
+ }
954
+ function manualIsBroader(manual, files) {
955
+ if (!files.length) return false;
956
+ const withSegment = files.filter((f) => f.split("/").some((seg) => seg.toLowerCase() === manual)).length;
957
+ return withSegment / files.length >= 0.6;
958
+ }
959
+ function checkZoneAgreement(model, entries = model.entries) {
960
+ const rows = [];
961
+ for (const e of entries) {
962
+ const manual = normalizeZoneName(e.area);
963
+ if (!manual) continue;
964
+ const guess = zoneForFiles(model, e.files);
965
+ const suggested = guess?.zone ?? null;
966
+ const verdict = suggested === manual ? "match" : suggested && manualIsBroader(manual, e.files) ? "compatible" : "mismatch";
967
+ rows.push({ slug: e.slug, manual, suggested, match: verdict === "match", verdict });
968
+ }
969
+ const matched = rows.filter((r) => r.verdict === "match").length;
970
+ const compatible = rows.filter((r) => r.verdict === "compatible").length;
971
+ const n = rows.length;
972
+ return { checked: n, matched, compatible, rate: n ? matched / n : 0, looseRate: n ? (matched + compatible) / n : 0, rows };
973
+ }
974
+ var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS;
975
+ var init_codeZones = __esm({
976
+ "../swarm-core/src/codeZones.ts"() {
977
+ "use strict";
978
+ SPINE_ZONE = "core";
979
+ DEFAULTS = {
980
+ minTasks: 5,
981
+ spineMinTasks: 3,
982
+ spineMinZones: 3,
983
+ spineDominantShare: 0.6
984
+ };
985
+ EMPTY_ZONE_MODEL = {
986
+ tasks: 0,
987
+ zones: [],
988
+ fileZone: /* @__PURE__ */ new Map(),
989
+ spine: [],
990
+ taskZone: /* @__PURE__ */ new Map(),
991
+ zoneTerms: /* @__PURE__ */ new Map(),
992
+ termZones: /* @__PURE__ */ new Map(),
993
+ entries: [],
994
+ entryTerms: []
995
+ };
996
+ GENERIC_DIRS = /* @__PURE__ */ new Set([
997
+ "src",
998
+ "source",
999
+ "lib",
1000
+ "libs",
1001
+ "util",
1002
+ "utils",
1003
+ "common",
1004
+ "internal",
1005
+ "app",
1006
+ "test",
1007
+ "tests",
1008
+ "__tests__",
1009
+ "spec",
1010
+ "specs",
1011
+ "index"
1012
+ ]);
1013
+ WORKSPACE_DIRS = /* @__PURE__ */ new Set(["packages", "apps", "services", "libs", "modules"]);
1014
+ GENERIC_TEST_DIRS = /* @__PURE__ */ new Set(["tests", "test", "__tests__", "spec"]);
1015
+ STOP_TERMS = /* @__PURE__ */ new Set([
1016
+ "the",
1017
+ "and",
1018
+ "for",
1019
+ "with",
1020
+ "from",
1021
+ "into",
1022
+ "src",
1023
+ "test",
1024
+ "tests",
1025
+ "index",
1026
+ "packages",
1027
+ "package",
1028
+ "json",
1029
+ "yml",
1030
+ "yaml",
1031
+ "tsx",
1032
+ "mjs",
1033
+ "cjs",
1034
+ "\u0447\u0442\u043E",
1035
+ "\u043A\u0430\u043A",
1036
+ "\u0434\u043B\u044F",
1037
+ "\u043F\u0440\u0438",
1038
+ "\u044D\u0442\u043E",
1039
+ "\u0438\u043B\u0438",
1040
+ "\u0435\u0441\u043B\u0438",
1041
+ "\u0447\u0442\u043E\u0431\u044B",
1042
+ "\u0442\u043E\u0436\u0435",
1043
+ "\u0435\u0433\u043E",
1044
+ "\u0432\u0441\u0435",
1045
+ "feat",
1046
+ "fix",
1047
+ "chore",
1048
+ "docs",
1049
+ "refactor",
1050
+ "\u043F\u0435\u0440\u0444",
1051
+ "wip"
1052
+ ]);
1053
+ }
1054
+ });
1055
+
627
1056
  // ../swarm-core/src/index.ts
628
1057
  var init_src = __esm({
629
1058
  "../swarm-core/src/index.ts"() {
@@ -640,15 +1069,489 @@ var init_src = __esm({
640
1069
  init_names();
641
1070
  init_baseBranch();
642
1071
  init_hosts();
1072
+ init_hostPool();
643
1073
  init_hostCommands();
644
1074
  init_relief();
1075
+ init_cost();
1076
+ init_runGuards();
1077
+ init_codeZones();
645
1078
  }
646
1079
  });
647
1080
 
648
- // src/config.ts
1081
+ // src/claudeBin.ts
1082
+ import { existsSync, readdirSync } from "fs";
1083
+ import { win32 as winPath } from "path";
1084
+ function compareVersionDirs(a, b) {
1085
+ const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
1086
+ const pa = parts(a);
1087
+ const pb = parts(b);
1088
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1089
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
1090
+ if (d !== 0) return d;
1091
+ }
1092
+ return 0;
1093
+ }
1094
+ function realListDir(dir) {
1095
+ try {
1096
+ return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1097
+ } catch {
1098
+ return [];
1099
+ }
1100
+ }
1101
+ function winVersionRoots(env, listDir) {
1102
+ const roots = [];
1103
+ const appData = env.APPDATA;
1104
+ if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
1105
+ const localAppData = env.LOCALAPPDATA;
1106
+ if (localAppData) {
1107
+ const packages = winPath.join(localAppData, "Packages");
1108
+ for (const name of listDir(packages)) {
1109
+ if (!name.startsWith("Claude")) continue;
1110
+ roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
1111
+ }
1112
+ }
1113
+ return roots;
1114
+ }
1115
+ function findWindowsClaude(opts = {}) {
1116
+ const env = opts.env ?? process.env;
1117
+ const listDir = opts.listDir ?? realListDir;
1118
+ const fileExists = opts.fileExists ?? existsSync;
1119
+ let best = null;
1120
+ for (const root of winVersionRoots(env, listDir)) {
1121
+ for (const version of listDir(root)) {
1122
+ const candidate = winPath.join(root, version, "claude.exe");
1123
+ if (!fileExists(candidate)) continue;
1124
+ if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
1125
+ }
1126
+ }
1127
+ return best?.path ?? null;
1128
+ }
1129
+ function resolveClaudeBin(opts = {}) {
1130
+ const env = opts.env ?? process.env;
1131
+ const platform = opts.platform ?? process.platform;
1132
+ const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
1133
+ if (explicit) return explicit;
1134
+ if (platform !== "win32") return CLAUDE_FALLBACK;
1135
+ if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
1136
+ windowsScanDone = true;
1137
+ const found = findWindowsClaude({ ...opts, env });
1138
+ cachedWindowsBin = found;
1139
+ if (found) return found;
1140
+ if (!warnedNoWindowsClaude) {
1141
+ warnedNoWindowsClaude = true;
1142
+ console.warn(
1143
+ "[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."
1144
+ );
1145
+ }
1146
+ return CLAUDE_FALLBACK;
1147
+ }
1148
+ var CLAUDE_FALLBACK, warnedNoWindowsClaude, cachedWindowsBin, windowsScanDone;
1149
+ var init_claudeBin = __esm({
1150
+ "src/claudeBin.ts"() {
1151
+ "use strict";
1152
+ CLAUDE_FALLBACK = "claude";
1153
+ warnedNoWindowsClaude = false;
1154
+ cachedWindowsBin = null;
1155
+ windowsScanDone = false;
1156
+ }
1157
+ });
1158
+
1159
+ // src/pricing.ts
1160
+ function costFromUsage(usage2, model, table = MODEL_PRICING) {
1161
+ const pricing = model ? table[model] : void 0;
1162
+ if (!pricing) {
1163
+ 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` };
1164
+ }
1165
+ const input = usage2.input_tokens ?? 0;
1166
+ const output = usage2.output_tokens ?? 0;
1167
+ const cacheWrite = usage2.cache_creation_input_tokens ?? 0;
1168
+ const cacheRead = usage2.cache_read_input_tokens ?? 0;
1169
+ const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
1170
+ return { costUsd };
1171
+ }
1172
+ var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
1173
+ var init_pricing = __esm({
1174
+ "src/pricing.ts"() {
1175
+ "use strict";
1176
+ MODEL_PRICING = {
1177
+ // Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
1178
+ // Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
1179
+ // ветвление по времени внутри расчёта стоимости.
1180
+ "claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
1181
+ "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
1182
+ // Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
1183
+ "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
1184
+ "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
1185
+ };
1186
+ CACHE_READ_FACTOR = 0.1;
1187
+ CACHE_WRITE_5M_FACTOR = 1.25;
1188
+ }
1189
+ });
1190
+
1191
+ // src/claudeResult.ts
1192
+ function parseClaudeResultJson(raw) {
1193
+ const trimmed = raw.trim();
1194
+ if (!trimmed) return null;
1195
+ try {
1196
+ const parsed = JSON.parse(trimmed);
1197
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
1198
+ } catch {
1199
+ return null;
1200
+ }
1201
+ }
1202
+ function launchCostUsd(result, model) {
1203
+ if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
1204
+ return { costUsd: result.total_cost_usd };
1205
+ }
1206
+ if (!result.usage) return { costUsd: 0 };
1207
+ return costFromUsage(result.usage, model);
1208
+ }
1209
+ function launchSpend(result) {
1210
+ const u = result.usage ?? {};
1211
+ const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1212
+ return {
1213
+ tokensIn: num(u.input_tokens) + num(u.cache_creation_input_tokens) + num(u.cache_read_input_tokens),
1214
+ tokensOut: num(u.output_tokens),
1215
+ durationMs: num(result.duration_ms)
1216
+ };
1217
+ }
1218
+ var init_claudeResult = __esm({
1219
+ "src/claudeResult.ts"() {
1220
+ "use strict";
1221
+ init_pricing();
1222
+ }
1223
+ });
1224
+
1225
+ // src/executors/claudeCode.ts
1226
+ import { readdirSync as readdirSync2, statSync, writeFileSync } from "fs";
649
1227
  import { homedir } from "os";
650
- import { basename, join, resolve } from "path";
651
- import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "fs";
1228
+ import { join } from "path";
1229
+ function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
1230
+ const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
1231
+ const base3 = [
1232
+ "--dangerously-skip-permissions",
1233
+ ...ISOLATION_FLAGS,
1234
+ "--mcp-config",
1235
+ cfgPath,
1236
+ ...settings ? ["--settings", settings] : [],
1237
+ ...pluginFlags,
1238
+ "--output-format",
1239
+ "json",
1240
+ "-p",
1241
+ prompt
1242
+ ];
1243
+ const withModel = model ? ["--model", model, ...base3] : base3;
1244
+ return continueSession ? ["--continue", ...withModel] : withModel;
1245
+ }
1246
+ function droneDenyRules(paths) {
1247
+ const abs = (p) => `//${p.replace(/^\/+/, "")}`;
1248
+ return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
1249
+ }
1250
+ function droneSettingsJson(paths) {
1251
+ return JSON.stringify({ permissions: { deny: droneDenyRules(paths) } });
1252
+ }
1253
+ function transcriptDir(worktree, projectsDir) {
1254
+ return join(projectsDir, worktree.replace(/[/.]/g, "-"));
1255
+ }
1256
+ function sessionTranscriptExists(worktree, projectsDir = join(homedir(), ".claude", "projects")) {
1257
+ try {
1258
+ return readdirSync2(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
1259
+ } catch {
1260
+ return false;
1261
+ }
1262
+ }
1263
+ function transcriptMtimeMs(worktree, projectsDir = join(homedir(), ".claude", "projects")) {
1264
+ const dir = transcriptDir(worktree, projectsDir);
1265
+ let files;
1266
+ try {
1267
+ files = readdirSync2(dir);
1268
+ } catch {
1269
+ return null;
1270
+ }
1271
+ let latest = null;
1272
+ for (const f of files) {
1273
+ if (!f.endsWith(".jsonl")) continue;
1274
+ try {
1275
+ const mtime2 = statSync(join(dir, f)).mtimeMs;
1276
+ if (latest === null || mtime2 > latest) latest = mtime2;
1277
+ } catch {
1278
+ }
1279
+ }
1280
+ return latest;
1281
+ }
1282
+ function writeMcpConfig(spec) {
1283
+ const cfg = {
1284
+ mcpServers: {
1285
+ [spec.mcp.name]: { command: spec.mcp.command, args: spec.mcp.args, env: spec.mcp.env }
1286
+ }
1287
+ };
1288
+ const path = join(spec.worktree, MCP_CONFIG_FILE);
1289
+ writeFileSync(path, JSON.stringify(cfg, null, 2));
1290
+ return path;
1291
+ }
1292
+ var ISOLATION_FLAGS, MCP_CONFIG_FILE, claudeCodeExecutor;
1293
+ var init_claudeCode = __esm({
1294
+ "src/executors/claudeCode.ts"() {
1295
+ "use strict";
1296
+ init_claudeBin();
1297
+ init_claudeResult();
1298
+ ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
1299
+ MCP_CONFIG_FILE = ".swarm-mcp.json";
1300
+ claudeCodeExecutor = {
1301
+ id: "claude-code",
1302
+ title: "Claude Code (claude)",
1303
+ binEnv: "SWARM_CLAUDE_BIN",
1304
+ 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" },
1305
+ resolveBin: () => resolveClaudeBin(),
1306
+ // Стрелка, а не метод: `plan` могут вызвать в отрыве от объекта (деструктуризация в тестах,
1307
+ // проброс колбэком), и `this` тогда уедет в undefined.
1308
+ plan: (spec) => {
1309
+ const cfgPath = writeMcpConfig(spec);
1310
+ const settings = spec.denyPaths.length > 0 ? droneSettingsJson(spec.denyPaths) : void 0;
1311
+ return {
1312
+ bin: resolveClaudeBin(),
1313
+ args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings)
1314
+ };
1315
+ },
1316
+ parseResult(raw, model) {
1317
+ const result = parseClaudeResultJson(raw);
1318
+ if (!result) return null;
1319
+ const est = launchCostUsd(result, model);
1320
+ const spend = launchSpend(result);
1321
+ return {
1322
+ costUsd: est.costUsd,
1323
+ ...est.warning ? { warning: est.warning } : {},
1324
+ tokensIn: spend.tokensIn,
1325
+ tokensOut: spend.tokensOut,
1326
+ durationMs: spend.durationMs
1327
+ };
1328
+ },
1329
+ session: {
1330
+ exists: (worktree) => sessionTranscriptExists(worktree),
1331
+ mtimeMs: (worktree) => transcriptMtimeMs(worktree)
1332
+ },
1333
+ // `claude -p "ok" --max-turns 1` (TASWM-23) — прямее, чем HEAD на статус-хост: дёргает ТОТ
1334
+ // ЖЕ путь (auth + реальный ответ модели), что и дрон. ISOLATION_FLAGS те же, что у дрона
1335
+ // (TASWM-90): SessionStart-хуки юзера могли бы подвесить пробу и дать ложный шторм.
1336
+ probe: {
1337
+ args: [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"],
1338
+ // TASWM-214: исчерпанный лимит подписки — НЕ шторм провайдера, и путать их нельзя.
1339
+ limitPattern: /(weekly limit|usage limit|hit your .*limit)/i
1340
+ }
1341
+ };
1342
+ }
1343
+ });
1344
+
1345
+ // src/swarmHome.ts
1346
+ import { homedir as homedir2 } from "os";
1347
+ import { join as join2 } from "path";
1348
+ import { mkdirSync } from "fs";
1349
+ var SWARM_HOME;
1350
+ var init_swarmHome = __esm({
1351
+ "src/swarmHome.ts"() {
1352
+ "use strict";
1353
+ SWARM_HOME = process.env.SWARM_HOME || join2(homedir2(), ".taskless-swarm");
1354
+ mkdirSync(SWARM_HOME, { recursive: true });
1355
+ }
1356
+ });
1357
+
1358
+ // src/executors/codex.ts
1359
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync3, statSync as statSync2, symlinkSync } from "fs";
1360
+ import { join as join3 } from "path";
1361
+ function resolveCodexBin(env = process.env) {
1362
+ return (env.SWARM_CODEX_BIN ?? "").trim() || "codex";
1363
+ }
1364
+ function codexHomePath(worktree, swarmHome = SWARM_HOME) {
1365
+ return join3(swarmHome, "executor-state", "codex", worktree.replace(/[/.]/g, "-"));
1366
+ }
1367
+ function ensureCodexHome(worktree, realHome, env = process.env, swarmHome = SWARM_HOME) {
1368
+ const home = codexHomePath(worktree, swarmHome);
1369
+ mkdirSync2(home, { recursive: true });
1370
+ const real = (env.CODEX_HOME ?? "").trim() || join3(realHome, ".codex");
1371
+ for (const file of ["auth.json", "config.toml"]) {
1372
+ const target = join3(real, file);
1373
+ const link = join3(home, file);
1374
+ if (!existsSync2(target) || existsSync2(link)) continue;
1375
+ try {
1376
+ symlinkSync(target, link);
1377
+ } catch {
1378
+ }
1379
+ }
1380
+ return home;
1381
+ }
1382
+ function tomlValue(v) {
1383
+ if (typeof v === "string") {
1384
+ const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
1385
+ return `"${escaped}"`;
1386
+ }
1387
+ if (Array.isArray(v)) return `[${v.map((x) => tomlValue(x)).join(", ")}]`;
1388
+ return `{${Object.entries(v).map(([k, val]) => `${k} = ${tomlValue(val)}`).join(", ")}}`;
1389
+ }
1390
+ function mcpConfigArgs(spec) {
1391
+ const inline = `{command = ${tomlValue(spec.mcp.command)}, args = ${tomlValue(spec.mcp.args)}, env = ${tomlValue(spec.mcp.env)}}`;
1392
+ return ["-c", `mcp_servers.${spec.mcp.name}=${inline}`];
1393
+ }
1394
+ function skillsNotice(skillDirs) {
1395
+ if (skillDirs.length === 0) return "";
1396
+ 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.";
1397
+ }
1398
+ function readUsage(node) {
1399
+ if (typeof node !== "object" || node === null) return null;
1400
+ const rec = node;
1401
+ const direct = rec.total_token_usage ?? rec.usage ?? rec.token_usage;
1402
+ if (direct && typeof direct === "object") return direct;
1403
+ const info = rec.info;
1404
+ if (info && typeof info === "object") return readUsage(info);
1405
+ return null;
1406
+ }
1407
+ function parseCodexStream(raw) {
1408
+ let usage2 = null;
1409
+ let completed = false;
1410
+ for (const line of raw.split("\n")) {
1411
+ const trimmed = line.trim();
1412
+ if (!trimmed) continue;
1413
+ let parsed;
1414
+ try {
1415
+ parsed = JSON.parse(trimmed);
1416
+ } catch {
1417
+ continue;
1418
+ }
1419
+ if (typeof parsed !== "object" || parsed === null) continue;
1420
+ const rec = parsed;
1421
+ const msg = typeof rec.msg === "object" && rec.msg !== null ? rec.msg : rec;
1422
+ const type = typeof msg.type === "string" ? msg.type : null;
1423
+ const found = readUsage(msg);
1424
+ if (found) usage2 = found;
1425
+ if (type && TERMINAL_EVENTS.has(type)) completed = true;
1426
+ }
1427
+ return { usage: usage2, completed };
1428
+ }
1429
+ function newestRolloutMtime(dir, depth = 4) {
1430
+ let entries;
1431
+ try {
1432
+ entries = readdirSync3(dir);
1433
+ } catch {
1434
+ return null;
1435
+ }
1436
+ let latest = null;
1437
+ for (const name of entries) {
1438
+ const full = join3(dir, name);
1439
+ let st;
1440
+ try {
1441
+ st = statSync2(full);
1442
+ } catch {
1443
+ continue;
1444
+ }
1445
+ if (st.isDirectory()) {
1446
+ if (depth <= 0) continue;
1447
+ const nested = newestRolloutMtime(full, depth - 1);
1448
+ if (nested !== null && (latest === null || nested > latest)) latest = nested;
1449
+ } else if (name.endsWith(".jsonl")) {
1450
+ if (latest === null || st.mtimeMs > latest) latest = st.mtimeMs;
1451
+ }
1452
+ }
1453
+ return latest;
1454
+ }
1455
+ var BYPASS_FLAG, SKIP_GIT_CHECK_FLAG, CODEX_MODEL_PRICING, TERMINAL_EVENTS, codexExecutor;
1456
+ var init_codex = __esm({
1457
+ "src/executors/codex.ts"() {
1458
+ "use strict";
1459
+ init_swarmHome();
1460
+ init_pricing();
1461
+ BYPASS_FLAG = "--dangerously-bypass-approvals-and-sandbox";
1462
+ SKIP_GIT_CHECK_FLAG = "--skip-git-repo-check";
1463
+ CODEX_MODEL_PRICING = {};
1464
+ TERMINAL_EVENTS = /* @__PURE__ */ new Set(["task_complete", "turn.completed", "thread.completed", "session.completed"]);
1465
+ codexExecutor = {
1466
+ id: "codex",
1467
+ title: "Codex CLI (codex)",
1468
+ binEnv: "SWARM_CODEX_BIN",
1469
+ 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" },
1470
+ resolveBin: () => resolveCodexBin(),
1471
+ // Стрелка, а не метод — см. тот же комментарий в claudeCode.ts.
1472
+ plan: (spec) => {
1473
+ const notice = skillsNotice(spec.skillDirs);
1474
+ const prompt = notice ? `${spec.prompt}
1475
+
1476
+ ${notice}` : spec.prompt;
1477
+ const args = [
1478
+ "exec",
1479
+ // Продолжение оборванной сессии — ПОДКОМАНДА, а не флаг: `exec resume --last` берёт
1480
+ // последнюю сессию этого CODEX_HOME (то есть этого worktree, см. codexHome).
1481
+ ...spec.continueSession ? ["resume", "--last"] : [],
1482
+ "--json",
1483
+ SKIP_GIT_CHECK_FLAG,
1484
+ BYPASS_FLAG,
1485
+ ...spec.model ? ["-m", spec.model] : [],
1486
+ ...mcpConfigArgs(spec),
1487
+ prompt
1488
+ ];
1489
+ return { bin: resolveCodexBin(), args, env: { CODEX_HOME: ensureCodexHome(spec.worktree, spec.realHome) } };
1490
+ },
1491
+ parseResult(raw, model) {
1492
+ const { usage: usage2, completed } = parseCodexStream(raw);
1493
+ if (!completed) return null;
1494
+ 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" };
1495
+ const cached = usage2.cached_input_tokens ?? 0;
1496
+ const common = {
1497
+ input_tokens: Math.max(0, (usage2.input_tokens ?? 0) - cached),
1498
+ output_tokens: usage2.output_tokens ?? 0,
1499
+ cache_read_input_tokens: cached
1500
+ };
1501
+ const est = costFromUsage(common, model, CODEX_MODEL_PRICING);
1502
+ return est.warning ? { costUsd: est.costUsd, warning: est.warning } : { costUsd: est.costUsd };
1503
+ },
1504
+ session: {
1505
+ exists: (worktree) => newestRolloutMtime(join3(codexHomePath(worktree), "sessions")) !== null,
1506
+ mtimeMs: (worktree) => newestRolloutMtime(join3(codexHomePath(worktree), "sessions"))
1507
+ },
1508
+ probe: {
1509
+ // Тот же принцип, что у claude: дешёвый реальный прогон — он проверяет и auth, и живость
1510
+ // провайдера разом. Один короткий ответ, без тулов и без сети сверх модели.
1511
+ args: ["exec", "--json", SKIP_GIT_CHECK_FLAG, BYPASS_FLAG, "ok"],
1512
+ limitPattern: /(usage limit|rate limit|quota exceeded|insufficient_quota)/i
1513
+ }
1514
+ };
1515
+ }
1516
+ });
1517
+
1518
+ // src/executors/types.ts
1519
+ var init_types2 = __esm({
1520
+ "src/executors/types.ts"() {
1521
+ "use strict";
1522
+ }
1523
+ });
1524
+
1525
+ // src/executors/index.ts
1526
+ function resolveExecutor(id) {
1527
+ const key = (id ?? "").trim();
1528
+ if (!key) return EXECUTORS[DEFAULT_EXECUTOR_ID];
1529
+ const found = EXECUTORS[key];
1530
+ if (!found) {
1531
+ 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(", ")}`);
1532
+ }
1533
+ return found;
1534
+ }
1535
+ var DEFAULT_EXECUTOR_ID, EXECUTORS;
1536
+ var init_executors = __esm({
1537
+ "src/executors/index.ts"() {
1538
+ "use strict";
1539
+ init_claudeCode();
1540
+ init_codex();
1541
+ init_types2();
1542
+ init_claudeCode();
1543
+ init_codex();
1544
+ DEFAULT_EXECUTOR_ID = "claude-code";
1545
+ EXECUTORS = {
1546
+ [claudeCodeExecutor.id]: claudeCodeExecutor,
1547
+ [codexExecutor.id]: codexExecutor
1548
+ };
1549
+ }
1550
+ });
1551
+
1552
+ // src/config.ts
1553
+ import { basename, join as join4, resolve } from "path";
1554
+ import { readFileSync, realpathSync, renameSync, writeFileSync as writeFileSync2 } from "fs";
652
1555
  function normalizeRepo(repo) {
653
1556
  const r = (repo ?? "").trim();
654
1557
  if (!r) return "";
@@ -659,7 +1562,7 @@ function normalizeRepo(repo) {
659
1562
  }
660
1563
  }
661
1564
  function droneWorktreePath(worktreesDir, repo, slug) {
662
- return join(worktreesDir, basename(normalizeRepo(repo)), slug);
1565
+ return join4(worktreesDir, basename(normalizeRepo(repo)), slug);
663
1566
  }
664
1567
  function resolveEffectiveWorktreesDir(deps) {
665
1568
  if (deps.envOverride) return deps.envOverride;
@@ -680,33 +1583,35 @@ function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
680
1583
  }
681
1584
  function tokenFromFile() {
682
1585
  try {
683
- return readFileSync(join(SWARM_HOME, "token"), "utf8").trim();
1586
+ return readFileSync(join4(SWARM_HOME, "token"), "utf8").trim();
684
1587
  } catch {
685
1588
  return "";
686
1589
  }
687
1590
  }
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;
1591
+ 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
1592
  var init_config = __esm({
690
1593
  "src/config.ts"() {
691
1594
  "use strict";
692
1595
  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");
1596
+ init_executors();
1597
+ init_swarmHome();
1598
+ init_swarmHome();
1599
+ DB_PATH = process.env.SWARM_DB || join4(SWARM_HOME, "swarm.db");
1600
+ EXECUTOR = resolveExecutor(process.env.SWARM_EXECUTOR);
1601
+ DRONE_HOME = process.env.SWARM_DRONE_HOME || join4(SWARM_HOME, "drone-home");
697
1602
  DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
698
1603
  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");
1604
+ WORKTREES_NOINDEX_DIR = join4(SWARM_HOME, "worktrees.noindex");
1605
+ WORKTREES_LEGACY_DIR = join4(SWARM_HOME, "worktrees");
701
1606
  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");
1607
+ RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join4(SWARM_HOME, "run-logs");
1608
+ DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join4(SWARM_HOME, "overmind.log");
704
1609
  DAEMON_LOG_MAX_BYTES = envNum("SWARM_DAEMON_LOG_MAX_BYTES", 16 * 1024 * 1024);
705
1610
  DAEMON_LOG_KEEP = envNum("SWARM_DAEMON_LOG_KEEP", 2);
706
1611
  DAEMON_LOG_CHECK_MS = envNum("SWARM_DAEMON_LOG_CHECK_MS", 6e4);
707
1612
  DIST_BUILD = true;
708
1613
  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");
1614
+ SWARM_OUTBOX = process.env.SWARM_OUTBOX || join4(SWARM_HOME, "outbox.jsonl");
710
1615
  SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
711
1616
  SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
712
1617
  SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
@@ -731,15 +1636,15 @@ var init_config = __esm({
731
1636
  DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
732
1637
  DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
733
1638
  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");
1639
+ DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join4(SWARM_HOME, "drone-channel.key");
735
1640
  DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
736
- HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join(SWARM_HOME, "overmind.heartbeat");
1641
+ HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join4(SWARM_HOME, "overmind.heartbeat");
737
1642
  HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
738
1643
  SWARM_REPO = process.env.SWARM_REPO || "";
739
1644
  SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
740
1645
  SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
741
1646
  SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
742
- REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join(SWARM_HOME, "repo-map.json");
1647
+ REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join4(SWARM_HOME, "repo-map.json");
743
1648
  SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
744
1649
  TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
745
1650
  SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
@@ -750,7 +1655,7 @@ var init_config = __esm({
750
1655
  TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
751
1656
  SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
752
1657
  SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
753
- REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join(SWARM_HOME, "review-cutoff.json");
1658
+ REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join4(SWARM_HOME, "review-cutoff.json");
754
1659
  SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
755
1660
  SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
756
1661
  WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
@@ -845,14 +1750,14 @@ var init_storm = __esm({
845
1750
  });
846
1751
 
847
1752
  // src/repoRegistry.ts
848
- import { $ } from "execa";
1753
+ import { $ as $2 } from "execa";
849
1754
  function parseRepoFullName(remoteUrl) {
850
1755
  const m = remoteUrl.trim().match(/^(?:https?:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(\.git)?\/?$/i);
851
1756
  if (!m) return null;
852
1757
  return `${m[1]}/${m[2]}`.toLowerCase();
853
1758
  }
854
1759
  async function resolveRepoFullName(path) {
855
- const res = await $({ reject: false })`git -C ${path} remote get-url origin`;
1760
+ const res = await $2({ reject: false })`git -C ${path} remote get-url origin`;
856
1761
  if (res.exitCode !== 0) return null;
857
1762
  return parseRepoFullName(res.stdout);
858
1763
  }
@@ -875,8 +1780,8 @@ var init_repoRegistry = __esm({
875
1780
  });
876
1781
 
877
1782
  // src/repoScan.ts
878
- import { readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
879
- import { join as join3 } from "path";
1783
+ import { readdirSync as readdirSync5, readFileSync as readFileSync5, statSync as statSync5 } from "fs";
1784
+ import { join as join6 } from "path";
880
1785
  var init_repoScan = __esm({
881
1786
  "src/repoScan.ts"() {
882
1787
  "use strict";
@@ -885,9 +1790,9 @@ var init_repoScan = __esm({
885
1790
  });
886
1791
 
887
1792
  // 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";
1793
+ import { homedir as homedir3 } from "os";
1794
+ import { readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "fs";
1795
+ import { isAbsolute, join as join7, resolve as resolve2 } from "path";
891
1796
  function repoMapLines(rows) {
892
1797
  if (!rows?.length) return [];
893
1798
  const missing = rows.filter((r) => !r.path).length;
@@ -932,7 +1837,7 @@ var init_package = __esm({
932
1837
  "package.json"() {
933
1838
  package_default = {
934
1839
  name: "@taskless-app/swarm",
935
- version: "0.5.0",
1840
+ version: "0.6.1",
936
1841
  type: "module",
937
1842
  description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
938
1843
  keywords: [
@@ -981,7 +1886,8 @@ var init_package = __esm({
981
1886
  "@modelcontextprotocol/sdk": "^1.29.0",
982
1887
  execa: "^10.0.0",
983
1888
  zod: "3.25.76"
984
- }
1889
+ },
1890
+ gitHead: "415ad928e18430894ae24ac2e4267073f5c6da14"
985
1891
  };
986
1892
  }
987
1893
  });
@@ -999,7 +1905,7 @@ var init_version = __esm({
999
1905
 
1000
1906
  // src/droneChannel.ts
1001
1907
  import { createHmac, randomBytes, timingSafeEqual } from "crypto";
1002
- import { chmodSync, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
1908
+ import { chmodSync, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
1003
1909
  import { dirname as dirname2 } from "path";
1004
1910
  import { serve } from "@hono/node-server";
1005
1911
  function loadChannelSecret(path) {
@@ -1009,8 +1915,8 @@ function loadChannelSecret(path) {
1009
1915
  } catch {
1010
1916
  }
1011
1917
  const secret = randomBytes(32).toString("hex");
1012
- mkdirSync3(dirname2(path), { recursive: true });
1013
- writeFileSync5(path, secret, { mode: 384 });
1918
+ mkdirSync5(dirname2(path), { recursive: true });
1919
+ writeFileSync6(path, secret, { mode: 384 });
1014
1920
  try {
1015
1921
  chmodSync(path, 384);
1016
1922
  } catch {
@@ -1049,87 +1955,9 @@ var init_droneChannel = __esm({
1049
1955
  }
1050
1956
  });
1051
1957
 
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
1958
  // src/agentToken.ts
1131
1959
  import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, unlinkSync as unlinkSync3 } from "fs";
1132
- import { join as join11 } from "path";
1960
+ import { join as join14 } from "path";
1133
1961
  function loadAgentToken() {
1134
1962
  try {
1135
1963
  const s = JSON.parse(readFileSync10(STORE_PATH2, "utf8"));
@@ -1170,12 +1998,20 @@ async function probeAgentToken(args, fetchFn = fetch) {
1170
1998
  return "unreachable";
1171
1999
  }
1172
2000
  }
2001
+ function isRevokedTokenError(e) {
2002
+ if (e && typeof e === "object" && e.code === 401) return true;
2003
+ const text = e instanceof Error ? e.message : typeof e === "string" ? e : "";
2004
+ return /invalid_token|(?:http|status|code)[ =:]*401\b|\(401\)|401 unauthorized/i.test(text);
2005
+ }
2006
+ function revokedTokenLine() {
2007
+ return "[taskless] \u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u043E\u0437\u0432\u0430\u043D (401 invalid_token) \u2014 \u0440\u043E\u0439 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442 \u0440\u0430\u0431\u043E\u0442\u0443. \u041F\u043E\u0447\u0438\u043D\u0438 \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: swarm login";
2008
+ }
1173
2009
  var STORE_PATH2, base2;
1174
2010
  var init_agentToken = __esm({
1175
2011
  "src/agentToken.ts"() {
1176
2012
  "use strict";
1177
2013
  init_config();
1178
- STORE_PATH2 = join11(SWARM_HOME, "agent.json");
2014
+ STORE_PATH2 = join14(SWARM_HOME, "agent.json");
1179
2015
  base2 = (api) => api.replace(/\/$/, "");
1180
2016
  }
1181
2017
  });
@@ -1385,7 +2221,7 @@ __export(db_exports, {
1385
2221
  SwarmDb: () => SwarmDb
1386
2222
  });
1387
2223
  import { createRequire as createRequire2 } from "module";
1388
- import { existsSync as existsSync9 } from "fs";
2224
+ import { existsSync as existsSync10 } from "fs";
1389
2225
  import { resolve as resolve3 } from "path";
1390
2226
  function rowToQuestion(row) {
1391
2227
  return {
@@ -1419,6 +2255,10 @@ function rowToRun(row) {
1419
2255
  depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
1420
2256
  model: row.model ?? null,
1421
2257
  cost_usd: row.cost_usd ?? 0,
2258
+ // Токены/время работы (TASWM-353) — старые прогоны без колонок → 0.
2259
+ tokens_in: row.tokens_in ?? 0,
2260
+ tokens_out: row.tokens_out ?? 0,
2261
+ duration_ms: row.duration_ms ?? 0,
1422
2262
  pr_url: row.pr_url ?? null,
1423
2263
  superseded_by: row.superseded_by ?? null,
1424
2264
  decision: row.decision ? JSON.parse(row.decision) : null,
@@ -1484,6 +2324,9 @@ CREATE TABLE IF NOT EXISTS runs (
1484
2324
  depends_on TEXT,
1485
2325
  model TEXT,
1486
2326
  cost_usd REAL NOT NULL DEFAULT 0,
2327
+ tokens_in INTEGER NOT NULL DEFAULT 0,
2328
+ tokens_out INTEGER NOT NULL DEFAULT 0,
2329
+ duration_ms INTEGER NOT NULL DEFAULT 0,
1487
2330
  pr_url TEXT,
1488
2331
  superseded_by INTEGER,
1489
2332
  decision TEXT,
@@ -1577,7 +2420,11 @@ CREATE TABLE IF NOT EXISTS imports (
1577
2420
  // Базовая ветка PR (TASWM-285): куда дрон открывает PR и от чего ответвляется.
1578
2421
  "ALTER TABLE runs ADD COLUMN base_branch TEXT",
1579
2422
  // Воркспейс задачи прогона (TASWM-288): им пинятся задачные MCP-вызовы прогона.
1580
- "ALTER TABLE runs ADD COLUMN workspace_id TEXT"
2423
+ "ALTER TABLE runs ADD COLUMN workspace_id TEXT",
2424
+ // Чем обошёлся прогон, кроме денег (TASWM-353): токены вход/выход и время работы.
2425
+ "ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
2426
+ "ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
2427
+ "ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0"
1581
2428
  ]) {
1582
2429
  try {
1583
2430
  this.db.exec(alter);
@@ -1870,7 +2717,7 @@ CREATE TABLE IF NOT EXISTS imports (
1870
2717
  const repoN = normalizeRepo(repo);
1871
2718
  const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
1872
2719
  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}`);
2720
+ if (!existsSync10(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
1874
2721
  const src = new this.databaseCtor(abs);
1875
2722
  try {
1876
2723
  const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
@@ -2197,8 +3044,8 @@ CREATE TABLE IF NOT EXISTS imports (
2197
3044
  });
2198
3045
 
2199
3046
  // src/platform.ts
2200
- import { existsSync as existsSync10, readFileSync as readFileSync14 } from "fs";
2201
- import { delimiter, join as join16 } from "path";
3047
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
3048
+ import { delimiter, join as join19 } from "path";
2202
3049
  function safeReadText(path) {
2203
3050
  try {
2204
3051
  return readFileSync14(path, "utf8");
@@ -2207,13 +3054,13 @@ function safeReadText(path) {
2207
3054
  }
2208
3055
  }
2209
3056
  function resolveCommandPath(name, env = process.env, platform = process.platform) {
2210
- if (name.includes("/") || name.includes("\\")) return existsSync10(name) ? name : null;
3057
+ if (name.includes("/") || name.includes("\\")) return existsSync11(name) ? name : null;
2211
3058
  const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
2212
3059
  const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
2213
3060
  for (const dir of dirs) {
2214
3061
  for (const ext of exts) {
2215
- const candidate = join16(dir, name + ext);
2216
- if (existsSync10(candidate)) return candidate;
3062
+ const candidate = join19(dir, name + ext);
3063
+ if (existsSync11(candidate)) return candidate;
2217
3064
  }
2218
3065
  }
2219
3066
  return null;
@@ -2316,13 +3163,13 @@ var init_platform = __esm({
2316
3163
  });
2317
3164
 
2318
3165
  // src/envGate.ts
2319
- import { existsSync as existsSync11 } from "fs";
2320
- import { join as join17 } from "path";
3166
+ import { existsSync as existsSync12 } from "fs";
3167
+ import { join as join20 } from "path";
2321
3168
  import { createConnection } from "net";
2322
3169
  import { execa as execa3 } from "execa";
2323
3170
  function detectRequirements(repo, io = {}) {
2324
- const exists = io.exists ?? existsSync11;
2325
- const marker = COMPOSE_MARKERS.find((f) => exists(join17(repo, f)));
3171
+ const exists = io.exists ?? existsSync12;
3172
+ const marker = COMPOSE_MARKERS.find((f) => exists(join20(repo, f)));
2326
3173
  return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
2327
3174
  }
2328
3175
  async function dockerAlive() {
@@ -2382,14 +3229,15 @@ var SWARM_PACKAGE, LATEST_TIMEOUT_MS;
2382
3229
  var init_release = __esm({
2383
3230
  "src/release.ts"() {
2384
3231
  "use strict";
3232
+ init_src();
2385
3233
  init_claudeBin();
2386
- SWARM_PACKAGE = "@taskless-app/swarm";
3234
+ SWARM_PACKAGE = SWARM_PACKAGE_NAME;
2387
3235
  LATEST_TIMEOUT_MS = 4e3;
2388
3236
  }
2389
3237
  });
2390
3238
 
2391
3239
  // src/doctor.ts
2392
- import { isAbsolute as isAbsolute4, join as join18 } from "path";
3240
+ import { isAbsolute as isAbsolute4, join as join21 } from "path";
2393
3241
  function ok(id, title, detail) {
2394
3242
  return { id, title, status: "ok", detail, remedies: [], critical: false };
2395
3243
  }
@@ -2410,21 +3258,20 @@ function checkNode(io) {
2410
3258
  critical: true
2411
3259
  };
2412
3260
  }
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
- }
3261
+ function checkExecutor(io, executor = resolveExecutor()) {
3262
+ const bin = executor.resolveBin();
3263
+ const where = /[/\\]/.test(bin) ? `\u043D\u0430\u0439\u0434\u0435\u043D: ${bin}` : "\u043D\u0430\u0439\u0434\u0435\u043D \u0432 PATH";
3264
+ if (io.hasCommand(bin)) return ok("claude", executor.title, where);
2418
3265
  const remedies = [
2419
3266
  {
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"),
3267
+ what: executor.install.what,
3268
+ command: globalInstallCommand(io.globalPm === "sources" ? "npm" : io.globalPm, executor.install.pkg),
2422
3269
  sudo: false,
2423
3270
  auto: true
2424
3271
  }
2425
3272
  ];
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 };
3273
+ 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`;
3274
+ return { id: "claude", title: executor.title, status: "fail", detail, remedies, critical: true };
2428
3275
  }
2429
3276
  async function checkGh(io) {
2430
3277
  if (!io.hasCommand("gh")) {
@@ -2597,7 +3444,7 @@ function checkRepos(io, snap) {
2597
3444
  critical: true
2598
3445
  };
2599
3446
  }
2600
- if (env && !io.exists(join18(env, ".git"))) {
3447
+ if (env && !io.exists(join21(env, ".git"))) {
2601
3448
  return {
2602
3449
  id: "repos",
2603
3450
  title,
@@ -2607,7 +3454,7 @@ function checkRepos(io, snap) {
2607
3454
  critical: true
2608
3455
  };
2609
3456
  }
2610
- const broken = snap.paths.filter((p) => !io.exists(join18(p, ".git")));
3457
+ const broken = snap.paths.filter((p) => !io.exists(join21(p, ".git")));
2611
3458
  if (broken.length) {
2612
3459
  return {
2613
3460
  id: "repos",
@@ -2697,7 +3544,7 @@ async function runCheck(id, io) {
2697
3544
  case "version":
2698
3545
  return checkSwarmVersion(io);
2699
3546
  case "claude":
2700
- return checkClaude(io);
3547
+ return checkExecutor(io);
2701
3548
  case "gh":
2702
3549
  return checkGh(io);
2703
3550
  case "screenshots":
@@ -2754,6 +3601,7 @@ var init_doctor = __esm({
2754
3601
  "src/doctor.ts"() {
2755
3602
  "use strict";
2756
3603
  init_claudeBin();
3604
+ init_executors();
2757
3605
  init_envGate();
2758
3606
  init_platform();
2759
3607
  init_release();
@@ -2771,11 +3619,11 @@ var init_doctor = __esm({
2771
3619
 
2772
3620
  // src/repoMemory.ts
2773
3621
  import { readFileSync as readFileSync15, writeFileSync as writeFileSync12 } from "fs";
2774
- import { join as join19 } from "path";
3622
+ import { join as join22 } from "path";
2775
3623
  function loadRememberedRepos(home) {
2776
3624
  let raw;
2777
3625
  try {
2778
- raw = readFileSync15(join19(home, FILE), "utf8");
3626
+ raw = readFileSync15(join22(home, FILE), "utf8");
2779
3627
  } catch {
2780
3628
  return [];
2781
3629
  }
@@ -2810,7 +3658,7 @@ __export(installCli_exports, {
2810
3658
  runInstallCli: () => runInstallCli,
2811
3659
  shortenError: () => shortenError
2812
3660
  });
2813
- import { existsSync as existsSync12 } from "fs";
3661
+ import { existsSync as existsSync13 } from "fs";
2814
3662
  function classifyChromiumError(message) {
2815
3663
  if (/playwright-core не установлен|Cannot find (module|package)/i.test(message)) return "no-module";
2816
3664
  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 +3713,7 @@ function defaultDoctorIO(packageVersion) {
2865
3713
  packageVersion,
2866
3714
  entry,
2867
3715
  hasCommand: (name) => hasCommandInPath(name),
2868
- exists: existsSync12,
3716
+ exists: existsSync13,
2869
3717
  run: (command) => runQuiet(command),
2870
3718
  latestVersion: () => latestMemo ??= fetchLatestVersion(),
2871
3719
  probeChromium: () => probeChromium(),
@@ -3075,12 +3923,12 @@ var init_updateCli = __esm({
3075
3923
  // src/cli.ts
3076
3924
  init_cliIo();
3077
3925
  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";
3926
+ import { existsSync as existsSync14, readdirSync as readdirSync9, statSync as statSync9, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
3079
3927
  import { setTimeout as sleep2 } from "timers/promises";
3080
3928
 
3081
3929
  // src/swarmClient.ts
3082
3930
  init_config();
3083
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3931
+ import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
3084
3932
  import { dirname } from "path";
3085
3933
  import { randomUUID } from "crypto";
3086
3934
 
@@ -3246,8 +4094,8 @@ init_names2();
3246
4094
  init_storm();
3247
4095
 
3248
4096
  // src/logTail.ts
3249
- import { closeSync, existsSync, openSync, readSync, readdirSync, statSync } from "fs";
3250
- import { join as join2 } from "path";
4097
+ import { closeSync, existsSync as existsSync3, openSync, readSync, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
4098
+ import { join as join5 } from "path";
3251
4099
  var TAIL_LINES_FALLBACK = 200;
3252
4100
  var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
3253
4101
 
@@ -3315,6 +4163,15 @@ var SwarmClient = class _SwarmClient {
3315
4163
  nextRunId = 1;
3316
4164
  nextEventId = 1;
3317
4165
  nextQuestionId = 1;
4166
+ // TASWM-350: ключ НЕЗАКРЫТОЙ попытки взять работу. Живёт от первого вызова claim до
4167
+ // первого настоящего ответа прода; всё это время ретраи идут с ним, и повтор после
4168
+ // обрыва связи не превращается во второе взятие. См. claim().
4169
+ pendingClaimKey = null;
4170
+ // TASWM-357: последнее НАПЕЧАТАННОЕ про пропущенные claim'ом задачи (родители и
4171
+ // заблокированные). Дедуп по тексту: claim идёт раз в ~60 c, а набор пропущенных меняется
4172
+ // редко — без этого лог был бы простынёй из одной и той же строки, и в ней потонуло бы
4173
+ // всё остальное. Меняется набор (эпик закрыли / блокер прошли) → печатаем снова.
4174
+ lastClaimSkipLog = null;
3318
4175
  // Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
3319
4176
  // Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
3320
4177
  // переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
@@ -3325,6 +4182,9 @@ var SwarmClient = class _SwarmClient {
3325
4182
  // ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
3326
4183
  // каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
3327
4184
  notifyLevelUnsupported = false;
4185
+ // TAS-485: то же для гашения разрешившихся тревог (swarm_notify_resolve) — прод старее
4186
+ // демона не знает тула; ретраить это каждый тик бессмысленно, см. resolveAnomalies.
4187
+ notifyResolveUnsupported = false;
3328
4188
  // Корреляция с продом.
3329
4189
  keyByLocal = /* @__PURE__ */ new Map();
3330
4190
  // local run id → runKey
@@ -3367,24 +4227,24 @@ var SwarmClient = class _SwarmClient {
3367
4227
  this.repoFullNames = registry.map((e) => e.repoFullName);
3368
4228
  this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
3369
4229
  this.host = opts.host ?? null;
3370
- this.repoExists = opts.repoExists ?? existsSync2;
4230
+ this.repoExists = opts.repoExists ?? existsSync4;
3371
4231
  this.questionSyncMs = opts.questionSyncMs ?? 3e3;
3372
4232
  this.outboxPath = opts.outboxPath;
3373
4233
  this.mapPath = `${opts.outboxPath}.prodmap.json`;
3374
4234
  this.stormPath = `${opts.outboxPath}.storm.jsonl`;
3375
- mkdirSync2(dirname(this.outboxPath), { recursive: true });
4235
+ mkdirSync4(dirname(this.outboxPath), { recursive: true });
3376
4236
  this.recover();
3377
4237
  }
3378
4238
  // ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
3379
4239
  recover() {
3380
- if (existsSync2(this.mapPath)) {
4240
+ if (existsSync4(this.mapPath)) {
3381
4241
  try {
3382
4242
  const obj = JSON.parse(readFileSync3(this.mapPath, "utf8"));
3383
4243
  for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
3384
4244
  } catch {
3385
4245
  }
3386
4246
  }
3387
- if (existsSync2(this.outboxPath)) {
4247
+ if (existsSync4(this.outboxPath)) {
3388
4248
  const txt = readFileSync3(this.outboxPath, "utf8");
3389
4249
  for (const line of txt.split("\n")) {
3390
4250
  const s = line.trim();
@@ -3405,7 +4265,7 @@ var SwarmClient = class _SwarmClient {
3405
4265
  // eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
3406
4266
  // переприсваиваем локально (id событий — только порядок, не корреляция с продом).
3407
4267
  replayGlobalEvents() {
3408
- if (!existsSync2(this.stormPath)) return;
4268
+ if (!existsSync4(this.stormPath)) return;
3409
4269
  let txt;
3410
4270
  try {
3411
4271
  txt = readFileSync3(this.stormPath, "utf8");
@@ -3474,8 +4334,8 @@ var SwarmClient = class _SwarmClient {
3474
4334
  this.scheduleFlush();
3475
4335
  }
3476
4336
  // Прод-патч прогона: локальные поля Run → поля swarm_run_upsert (см. TASWM-74).
3477
- // pid/worktree_path (рантайм), respawns (нет в схеме тула), снимок задачи (task_*,
3478
- // прод берёт join'ом из Task) и priority (локальная очередь ЭТОГО демона, TASWM-269)
4337
+ // pid/worktree_path (рантайм), снимок задачи (task_*, прод берёт join'ом из Task)
4338
+ // и priority (локальная очередь ЭТОГО демона, TASWM-269)
3479
4339
  // в прод НЕ уходят. kind → role (плоский enum). Self-ref
3480
4340
  // прогонов hero_run_id/resolves_run_id/superseded_by → heroId/resolvesRunId/
3481
4341
  // supersededById как ОТЛОЖЕННЫЕ ссылки (RunRef) — cuid резолвится на флаше.
@@ -3486,6 +4346,10 @@ var SwarmClient = class _SwarmClient {
3486
4346
  if ("model" in patch) out.model = patch.model ?? null;
3487
4347
  if ("name" in patch) out.droneName = patch.name ?? null;
3488
4348
  if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
4349
+ if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
4350
+ if ("tokens_out" in patch && patch.tokens_out !== void 0) out.tokensOut = patch.tokens_out;
4351
+ if ("duration_ms" in patch && patch.duration_ms !== void 0) out.durationMs = patch.duration_ms;
4352
+ if ("respawns" in patch && patch.respawns !== void 0) out.respawns = patch.respawns;
3489
4353
  if ("pr_url" in patch) out.prUrl = patch.pr_url ?? null;
3490
4354
  if ("branch" in patch) out.branch = patch.branch ?? null;
3491
4355
  if ("repo" in patch) out.repo = patch.repo ?? null;
@@ -3551,6 +4415,10 @@ var SwarmClient = class _SwarmClient {
3551
4415
  depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
3552
4416
  model: input.model ?? null,
3553
4417
  cost_usd: 0,
4418
+ tokens_in: 0,
4419
+ // копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
4420
+ tokens_out: 0,
4421
+ duration_ms: 0,
3554
4422
  pr_url: null,
3555
4423
  superseded_by: null,
3556
4424
  decision: input.decision ?? null,
@@ -3673,6 +4541,7 @@ var SwarmClient = class _SwarmClient {
3673
4541
  if (!d) return 0;
3674
4542
  d.respawns += 1;
3675
4543
  d.updated_at = stamp();
4544
+ this.enqueueRun(d, this.runFields({ respawns: d.respawns }));
3676
4545
  return d.respawns;
3677
4546
  }
3678
4547
  cancel(id, reason) {
@@ -4088,7 +4957,7 @@ var SwarmClient = class _SwarmClient {
4088
4957
  * транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
4089
4958
  */
4090
4959
  reconcileRuntime(opts) {
4091
- const wtExists = opts.worktreeExists ?? existsSync2;
4960
+ const wtExists = opts.worktreeExists ?? existsSync4;
4092
4961
  const pidByWorktree = /* @__PURE__ */ new Map();
4093
4962
  try {
4094
4963
  for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
@@ -4176,23 +5045,69 @@ var SwarmClient = class _SwarmClient {
4176
5045
  * шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
4177
5046
  */
4178
5047
  async claim() {
5048
+ this.pendingClaimKey ??= randomUUID();
4179
5049
  let res;
4180
5050
  try {
4181
5051
  res = await this.call("swarm_claim_work", {
4182
5052
  ...this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {},
4183
5053
  // TASWM-281: взятая из общей очереди задача сразу принадлежит ЭТОЙ машине —
4184
5054
  // прод помечает созданный прогон hostId, чужой демон его не увидит.
4185
- ...this.host ? { hostId: this.host.hostId } : {}
5055
+ ...this.host ? { hostId: this.host.hostId } : {},
5056
+ idempotencyKey: this.pendingClaimKey
4186
5057
  });
4187
5058
  } catch {
4188
5059
  return null;
4189
5060
  }
4190
5061
  if (res?.isError) return null;
5062
+ this.pendingClaimKey = null;
4191
5063
  const sc = res.structuredContent ?? {};
5064
+ this.logClaimSkips(sc);
4192
5065
  if (!sc.claimed || typeof sc.run !== "object" || sc.run === null) return null;
4193
5066
  const local = this.hydrateRun(sc.run);
4194
5067
  return local ? this.mat(local) : null;
4195
5068
  }
5069
+ /**
5070
+ * TASWM-357: назвать в логе задачи, которые прод НЕ отдал в работу осознанно — родителей с
5071
+ * незавершёнными подзадачами (работа всегда в листьях) и листья с незакрытым блокером
5072
+ * (явный порядок, связь задач «блокирует»). Печатаем ЗДЕСЬ, а не на проде: оба фильтра
5073
+ * живут SQL-условиями и молчат по природе, а `display` claim'а демон не логирует — без
5074
+ * этой строки «почему эпик стоит нетронутым» не видно ниоткуда, и оператор ровно так же
5075
+ * гадает, как гадал, пока дрон вставал на эпике с вопросом «меня спавнили на родителя».
5076
+ *
5077
+ * Формат прода — `data.skippedParents: [{slug,title,openChildren}]` и
5078
+ * `data.skippedBlocked: [{slug,title,blockers[]}]`. Читаем защищённо: у старого прода
5079
+ * полей нет вовсе (тогда молчим), а разбирать чужую строку `display` вместо структуры —
5080
+ * тот же класс хрупкости, что уже кусал нас в notifyAnomaly.
5081
+ */
5082
+ logClaimSkips(sc) {
5083
+ const rows = (raw) => (Array.isArray(raw) ? raw : []).map((p) => typeof p === "object" && p !== null ? p : null).filter((p) => p !== null && typeof p.slug === "string");
5084
+ const others = (flag) => flag === true ? " \u0438 \u0434\u0440\u0443\u0433\u0438\u0435" : "";
5085
+ const lines = [];
5086
+ const parents = rows(sc.skippedParents).map(
5087
+ (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)`
5088
+ );
5089
+ if (parents.length > 0) {
5090
+ lines.push(
5091
+ `[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)}`
5092
+ );
5093
+ }
5094
+ const blocked = rows(sc.skippedBlocked).map(
5095
+ (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"}`
5096
+ );
5097
+ if (blocked.length > 0) {
5098
+ lines.push(
5099
+ `[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)}`
5100
+ );
5101
+ }
5102
+ const text = lines.join("\n");
5103
+ if (text.length === 0) {
5104
+ this.lastClaimSkipLog = null;
5105
+ return;
5106
+ }
5107
+ if (text === this.lastClaimSkipLog) return;
5108
+ this.lastClaimSkipLog = text;
5109
+ for (const line of lines) console.log(line);
5110
+ }
4196
5111
  /**
4197
5112
  * TASWM-281: пометить своими прогоны, под которыми на ЭТОЙ машине нашёлся ЖИВОЙ
4198
5113
  * процесс (после reconcileRuntime на старте). Это ровно те прогоны, где сомнений в
@@ -4466,12 +5381,20 @@ var SwarmClient = class _SwarmClient {
4466
5381
  * Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
4467
5382
  * означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
4468
5383
  *
5384
+ * TASWM-352: тем же пульсом уходит ЁМКОСТЬ этой машины (`capacity` — живой потолок
5385
+ * дронов ЭТОГО демона: машинный оверрайд, иначе общая настройка владельца). По ней
5386
+ * планировщик роя раскладывает очередь между машинами: до этого у прода было ОДНО
5387
+ * число на владельца, и ноутбук с рабочей станцией делили его поровну, хотя тянут
5388
+ * разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
5389
+ * трогает прошлое значение — та же логика, что у repoFullNames.
5390
+ *
4469
5391
  * Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
4470
5392
  */
4471
- async heartbeat(daemonVersion, pause) {
5393
+ async heartbeat(daemonVersion, pause, capacity) {
4472
5394
  try {
4473
5395
  const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
4474
5396
  if (daemonVersion) args.daemonVersion = daemonVersion;
5397
+ if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
4475
5398
  if (pause !== void 0) {
4476
5399
  args.paused = !!pause?.paused;
4477
5400
  if (pause?.reason) args.pauseReason = pause.reason;
@@ -4577,6 +5500,35 @@ var SwarmClient = class _SwarmClient {
4577
5500
  const res = await this.call("swarm_notify", base3);
4578
5501
  if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
4579
5502
  }
5503
+ /**
5504
+ * TAS-485: погасить в инбоксе владельца тревоги, чьё условие ушло. Ключи — те же, с
5505
+ * которыми уходил notifyAnomaly (прод сам ставит им префикс `swarm:`).
5506
+ *
5507
+ * Бросает при сбое — СОЗНАТЕЛЬНО, как notifyAnomaly: вызывающий (tickOnce) вычёркивает
5508
+ * ключи из durable-очереди только после успеха, иначе «погашено» разошлось бы с тем,
5509
+ * что человек видит в инбоксе, и разошлось бы навсегда — повторить будет некому.
5510
+ *
5511
+ * ДЕПЛОЙ-ОКНО. Демон из свежего чекаута против ещё не задеплоенного прода получит
5512
+ * «неизвестный тул». Это не ошибка доставки, а отсутствие фичи на той стороне: ретраить
5513
+ * её каждые ~4 секунды бессмысленно, поэтому первую такую осечку запоминаем на жизнь
5514
+ * процесса и дальше молча пропускаем гашение (надзор в остальном работает как раньше).
5515
+ * Тот же приём, что у notifyAnomaly с аргументом `level`.
5516
+ */
5517
+ async resolveAnomalies(keys) {
5518
+ const tags = [...new Set(keys.filter((k) => k.length > 0 && k.length <= 120))];
5519
+ if (tags.length === 0 || this.notifyResolveUnsupported) return;
5520
+ const res = await this.call("swarm_notify_resolve", { tags });
5521
+ if (!res?.isError) return;
5522
+ const text = JSON.stringify(res);
5523
+ if (text.includes("swarm_notify_resolve") && (text.includes("Unknown tool") || text.includes("\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D"))) {
5524
+ this.notifyResolveUnsupported = true;
5525
+ console.warn(
5526
+ "[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."
5527
+ );
5528
+ return;
5529
+ }
5530
+ throw new Error(`swarm_notify_resolve \u043E\u0442\u0432\u0435\u0440\u0433 ${tags.length} \u043A\u043B\u044E\u0447(\u0435\u0439)`);
5531
+ }
4580
5532
  /**
4581
5533
  * TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
4582
5534
  * через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
@@ -4641,6 +5593,10 @@ var SwarmClient = class _SwarmClient {
4641
5593
  depends_on: [],
4642
5594
  model: null,
4643
5595
  cost_usd: 0,
5596
+ tokens_in: 0,
5597
+ // токены и время работы (TASWM-353) — прод-поля, заполняются ниже
5598
+ tokens_out: 0,
5599
+ duration_ms: 0,
4644
5600
  pr_url: null,
4645
5601
  superseded_by: null,
4646
5602
  decision: null,
@@ -4651,9 +5607,9 @@ var SwarmClient = class _SwarmClient {
4651
5607
  review_verdict: null,
4652
5608
  review_phase: null,
4653
5609
  // вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
4654
- // Локальная бухгалтерия (в прод-схему не уходит, как respawns) — на гидрации из
4655
- // прода дефолт 0; существующая локальная запись НЕ перетирается (поле не в field-copy
4656
- // ниже), в отличие от respawns, — авто-ревью v2, TASWM-162.
5610
+ // Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
5611
+ // существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
5612
+ // в отличие от respawns, — авто-ревью v2, TASWM-162.
4657
5613
  review_cycle: 0,
4658
5614
  task_title: null,
4659
5615
  task_desc: null,
@@ -4705,6 +5661,9 @@ var SwarmClient = class _SwarmClient {
4705
5661
  local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
4706
5662
  local.model = pr.model ?? null;
4707
5663
  local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
5664
+ local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
5665
+ local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
5666
+ local.duration_ms = typeof pr.durationMs === "number" ? pr.durationMs : 0;
4708
5667
  local.pr_url = pr.prUrl ?? null;
4709
5668
  local.stop_reason = pr.stopReason ?? null;
4710
5669
  local.respawns = typeof pr.respawns === "number" ? pr.respawns : 0;
@@ -4962,13 +5921,13 @@ var SwarmClient = class _SwarmClient {
4962
5921
  }
4963
5922
  persistPending() {
4964
5923
  try {
4965
- writeFileSync2(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
5924
+ writeFileSync3(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
4966
5925
  } catch {
4967
5926
  }
4968
5927
  }
4969
5928
  persistMap() {
4970
5929
  try {
4971
- writeFileSync2(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
5930
+ writeFileSync3(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
4972
5931
  } catch {
4973
5932
  }
4974
5933
  }
@@ -4993,6 +5952,97 @@ function parseDecision(v) {
4993
5952
  }
4994
5953
  }
4995
5954
 
5955
+ // src/cli.ts
5956
+ init_src();
5957
+
5958
+ // src/gitHistory.ts
5959
+ import { $ } from "execa";
5960
+ var REC = "";
5961
+ var FIELD = "";
5962
+ var LOG_FORMAT = `${REC}%H${FIELD}%s`;
5963
+ var SLUG_IN_SUBJECT = /\b([A-Z][A-Z0-9]{0,7}-\d+)\b/;
5964
+ function slugFromSubject(subject) {
5965
+ return SLUG_IN_SUBJECT.exec(subject)?.[1] ?? null;
5966
+ }
5967
+ var SCOPE_IN_SUBJECT = /(?:^|·\s*)[a-z]+\(([^)]+)\):/g;
5968
+ function scopeFromText(text) {
5969
+ if (!text) return null;
5970
+ const votes = /* @__PURE__ */ new Map();
5971
+ for (const m of text.matchAll(SCOPE_IN_SUBJECT)) {
5972
+ const scope = m[1].trim().toLowerCase();
5973
+ if (scope) votes.set(scope, (votes.get(scope) ?? 0) + 1);
5974
+ }
5975
+ let best = null;
5976
+ let bestN = 0;
5977
+ for (const [scope, n] of votes) {
5978
+ if (n > bestN) {
5979
+ best = scope;
5980
+ bestN = n;
5981
+ }
5982
+ }
5983
+ return best;
5984
+ }
5985
+ function unquotePath(raw) {
5986
+ const p = raw.trim();
5987
+ return p.length >= 2 && p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
5988
+ }
5989
+ function parseGitLog(out) {
5990
+ const commits = [];
5991
+ for (const chunk of out.split(REC)) {
5992
+ if (!chunk.trim()) continue;
5993
+ const [head, ...rest] = chunk.split("\n");
5994
+ const [sha, subject] = (head ?? "").split(FIELD);
5995
+ if (!sha) continue;
5996
+ const files = rest.map(unquotePath).filter(Boolean);
5997
+ commits.push({ sha, subject: subject ?? "", files });
5998
+ }
5999
+ return commits;
6000
+ }
6001
+ var NOISE_RE = /^(journal\/|changelog\/|docs\/|design\/|scratch\/|\.claude\/|.*\.lock$|pnpm-lock\.yaml$)/;
6002
+ function isNoisePath(path) {
6003
+ return NOISE_RE.test(path);
6004
+ }
6005
+ var GROUP_DEFAULTS = { maxFilesPerCommit: 120, ignore: isNoisePath };
6006
+ function groupTaskHistory(commits, opts = {}) {
6007
+ const o = { ...GROUP_DEFAULTS, ...opts };
6008
+ const bySlug = /* @__PURE__ */ new Map();
6009
+ for (const c of commits) {
6010
+ const slug = slugFromSubject(c.subject);
6011
+ if (!slug) continue;
6012
+ if (c.files.length > o.maxFilesPerCommit) continue;
6013
+ const files = c.files.filter((f) => !o.ignore(f));
6014
+ if (!files.length) continue;
6015
+ let acc = bySlug.get(slug);
6016
+ if (!acc) bySlug.set(slug, acc = { files: /* @__PURE__ */ new Set(), texts: [] });
6017
+ for (const f of files) acc.files.add(f);
6018
+ acc.texts.push(c.subject);
6019
+ }
6020
+ return [...bySlug.entries()].map(([slug, acc]) => ({
6021
+ slug,
6022
+ files: [...acc.files],
6023
+ text: acc.texts.join(" \xB7 ")
6024
+ }));
6025
+ }
6026
+ async function readCommitLog(repo, opts = {}) {
6027
+ const max = opts.maxCommits ?? 1500;
6028
+ try {
6029
+ const { stdout } = await $({ cwd: repo, reject: false })`git log --no-merges --name-only --pretty=format:${LOG_FORMAT} -n ${String(max)} ${opts.ref ?? "HEAD"}`;
6030
+ return parseGitLog(stdout ?? "");
6031
+ } catch {
6032
+ return [];
6033
+ }
6034
+ }
6035
+ async function readTaskHistory(repo, opts = {}) {
6036
+ return groupTaskHistory(await readCommitLog(repo, opts), opts);
6037
+ }
6038
+
6039
+ // src/zones.ts
6040
+ init_src();
6041
+ var DEFAULT_TTL_MS = 10 * 6e4;
6042
+ function withManualAreas(history, areas) {
6043
+ return history.map((e) => ({ ...e, area: normalizeZoneName(areas.get(e.slug) ?? null) }));
6044
+ }
6045
+
4996
6046
  // src/cli.ts
4997
6047
  init_config();
4998
6048
 
@@ -5064,11 +6114,11 @@ function tailFile(path, lines) {
5064
6114
  }
5065
6115
 
5066
6116
  // src/logRotate.ts
5067
- import { copyFileSync, renameSync as renameSync2, statSync as statSync2, fstatSync, truncateSync, unlinkSync, existsSync as existsSync3 } from "fs";
6117
+ import { copyFileSync, renameSync as renameSync2, statSync as statSync4, fstatSync, truncateSync, unlinkSync, existsSync as existsSync5 } from "fs";
5068
6118
  var realIo = {
5069
6119
  size: (path) => {
5070
6120
  try {
5071
- return statSync2(path).size;
6121
+ return statSync4(path).size;
5072
6122
  } catch {
5073
6123
  return null;
5074
6124
  }
@@ -5077,7 +6127,7 @@ var realIo = {
5077
6127
  rename: renameSync2,
5078
6128
  truncate: (path) => truncateSync(path, 0),
5079
6129
  unlink: unlinkSync,
5080
- exists: existsSync3
6130
+ exists: existsSync5
5081
6131
  };
5082
6132
  var STRAY_GENERATIONS = 8;
5083
6133
  function rotateDaemonLog(path, opts, io = realIo) {
@@ -5336,17 +6386,17 @@ init_repoDiscovery();
5336
6386
 
5337
6387
  // src/hostIdentity.ts
5338
6388
  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";
6389
+ import { existsSync as existsSync6, readFileSync as readFileSync7, renameSync as renameSync4, writeFileSync as writeFileSync5 } from "fs";
6390
+ import { join as join8 } from "path";
5341
6391
  import { randomUUID as randomUUID2 } from "crypto";
5342
6392
  function hostIdentityPath(home) {
5343
- return join5(home, "host.json");
6393
+ return join8(home, "host.json");
5344
6394
  }
5345
6395
  function loadHostIdentity(deps) {
5346
- const exists = deps.exists ?? existsSync4;
6396
+ const exists = deps.exists ?? existsSync6;
5347
6397
  const readFile2 = deps.readFile ?? ((p) => readFileSync7(p, "utf8"));
5348
6398
  const writeFile2 = deps.writeFile ?? ((p, data) => {
5349
- writeFileSync4(`${p}.tmp`, data);
6399
+ writeFileSync5(`${p}.tmp`, data);
5350
6400
  renameSync4(`${p}.tmp`, p);
5351
6401
  });
5352
6402
  const host = deps.hostname ?? hostname;
@@ -5458,9 +6508,9 @@ import { basename as basename2 } from "path";
5458
6508
  import { serve as serve2 } from "@hono/node-server";
5459
6509
 
5460
6510
  // src/diff.ts
5461
- import { statSync as statSync4 } from "fs";
5462
- import { join as join6 } from "path";
5463
- import { $ as $2 } from "execa";
6511
+ import { statSync as statSync6 } from "fs";
6512
+ import { join as join9 } from "path";
6513
+ import { $ as $3 } from "execa";
5464
6514
  var MAX_FILES = 200;
5465
6515
  var MAX_LINES_PER_FILE = 1500;
5466
6516
  var MAX_UNTRACKED = 60;
@@ -5580,12 +6630,12 @@ function truncateFile(f) {
5580
6630
  }
5581
6631
  async function resolveBase(wt, baseBranch) {
5582
6632
  const tryRef = async (ref) => {
5583
- const sha = (await $2({ reject: false })`git -C ${wt} rev-parse --verify --quiet ${ref + "^{commit}"}`).stdout.trim();
6633
+ const sha = (await $3({ reject: false })`git -C ${wt} rev-parse --verify --quiet ${ref + "^{commit}"}`).stdout.trim();
5584
6634
  if (!sha) return null;
5585
- const mb = (await $2({ reject: false })`git -C ${wt} merge-base HEAD ${ref}`).stdout.trim();
6635
+ const mb = (await $3({ reject: false })`git -C ${wt} merge-base HEAD ${ref}`).stdout.trim();
5586
6636
  return mb || null;
5587
6637
  };
5588
- const originHead = (await $2({ reject: false })`git -C ${wt} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
6638
+ const originHead = (await $3({ reject: false })`git -C ${wt} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
5589
6639
  const candidates = [
5590
6640
  ...baseBranch ? [`origin/${baseBranch}`, baseBranch] : [],
5591
6641
  originHead,
@@ -5602,7 +6652,7 @@ async function resolveBase(wt, baseBranch) {
5602
6652
  }
5603
6653
  function mtime(path) {
5604
6654
  try {
5605
- return statSync4(path).mtimeMs;
6655
+ return statSync6(path).mtimeMs;
5606
6656
  } catch {
5607
6657
  return 0;
5608
6658
  }
@@ -5611,23 +6661,23 @@ async function collectRunDiff(worktree, opts = {}) {
5611
6661
  const stub = (reason) => ({ ok: false, reason, pr: opts.pr ?? null, files: [], added: 0, removed: 0 });
5612
6662
  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
6663
  if (!worktree) return stub(gone);
5614
- const isWt = (await $2({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
6664
+ const isWt = (await $3({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
5615
6665
  if (isWt !== "true") return stub(gone);
5616
6666
  const base3 = await resolveBase(worktree, opts.baseBranch);
5617
6667
  const from = base3 ?? "HEAD";
5618
- const trackedTxt = (await $2({ reject: false })`git -C ${worktree} diff --no-color --find-renames ${from} --`).stdout;
6668
+ const trackedTxt = (await $3({ reject: false })`git -C ${worktree} diff --no-color --find-renames ${from} --`).stdout;
5619
6669
  let files = parseUnifiedDiff(trackedTxt);
5620
- const untrackedRaw = (await $2({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
6670
+ const untrackedRaw = (await $3({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
5621
6671
  const untracked = untrackedRaw.split("\0").filter(Boolean).slice(0, MAX_UNTRACKED);
5622
6672
  for (const rel of untracked) {
5623
- const t = (await $2({ reject: false })`git -C ${worktree} diff --no-color --no-index -- /dev/null ${rel}`).stdout;
6673
+ const t = (await $3({ reject: false })`git -C ${worktree} diff --no-color --no-index -- /dev/null ${rel}`).stdout;
5624
6674
  for (const pf of parseUnifiedDiff(t)) {
5625
6675
  pf.status = "added";
5626
6676
  if (!pf.path || pf.path === "/dev/null") pf.path = rel;
5627
6677
  files.push(pf);
5628
6678
  }
5629
6679
  }
5630
- files.sort((a, b) => mtime(join6(worktree, b.path)) - mtime(join6(worktree, a.path)));
6680
+ files.sort((a, b) => mtime(join9(worktree, b.path)) - mtime(join9(worktree, a.path)));
5631
6681
  const truncatedFiles = files.length > MAX_FILES;
5632
6682
  if (truncatedFiles) files = files.slice(0, MAX_FILES);
5633
6683
  const added = files.reduce((s, f) => s + f.added, 0);
@@ -5649,10 +6699,10 @@ init_storm();
5649
6699
 
5650
6700
  // src/gh.ts
5651
6701
  init_src();
5652
- import { $ as $3 } from "execa";
6702
+ import { $ as $4 } from "execa";
5653
6703
  var NO_CHECKS_STDERR = /no checks reported/i;
5654
6704
  var realChecksRunner = async (pr) => {
5655
- const r = await $3({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
6705
+ const r = await $4({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
5656
6706
  const stdout = r.stdout ?? "";
5657
6707
  if (!stdout.trim() && NO_CHECKS_STDERR.test(r.stderr ?? "")) return "[]";
5658
6708
  return stdout;
@@ -5701,14 +6751,14 @@ init_droneChannel();
5701
6751
  // src/oauth.ts
5702
6752
  init_config();
5703
6753
  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";
6754
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, unlinkSync as unlinkSync2 } from "fs";
6755
+ import { join as join10 } from "path";
5706
6756
  var REGISTER = "/api/oauth/register";
5707
6757
  var AUTHORIZE = "/api/oauth/authorize";
5708
6758
  var TOKEN = "/api/oauth/token";
5709
6759
  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");
6760
+ var STORE_PATH = join10(SWARM_HOME, "oauth.json");
6761
+ var CLIENT_PATH = join10(SWARM_HOME, "oauth-client.json");
5712
6762
  function makePkce() {
5713
6763
  const verifier = randomBytes2(48).toString("base64url");
5714
6764
  const challenge = createHash("sha256").update(verifier).digest("base64url");
@@ -5799,7 +6849,7 @@ function loadStore() {
5799
6849
  }
5800
6850
  }
5801
6851
  function saveStore(s) {
5802
- writeFileSync6(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
6852
+ writeFileSync7(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
5803
6853
  }
5804
6854
  function clearStore() {
5805
6855
  try {
@@ -5818,7 +6868,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
5818
6868
  const existing = loadClientId();
5819
6869
  if (existing) return existing;
5820
6870
  const clientId = await registerClient({ api, redirectUri }, fetchFn);
5821
- writeFileSync6(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
6871
+ writeFileSync7(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
5822
6872
  return clientId;
5823
6873
  }
5824
6874
  async function ensureAccess(store, opts = {}) {
@@ -5892,6 +6942,10 @@ function roySnapshot(db2, now = Date.now(), overmindLog = [], paused = false, da
5892
6942
  stopReason: r.stop_reason,
5893
6943
  reason,
5894
6944
  updated: r.updated_at,
6945
+ startedAt: r.created_at,
6946
+ // TASWM-351: возраст прогона на карточке
6947
+ costUsd: r.cost_usd || 0,
6948
+ // TASWM-351: то же поле, что уходит в прод (swarm_run_upsert{costUsd})
5895
6949
  step,
5896
6950
  hasDecision: withQ.has(r.id),
5897
6951
  rescued: evs.some((e) => e.kind === "rescued"),
@@ -7397,17 +8451,16 @@ function startWeb(deps, port = WEB_PORT) {
7397
8451
  import { cancelRun, respawnRun, runDaemon, wakeRun, OVERMIND_ENTRY, listDroneProcesses } from "./overmind.js";
7398
8452
 
7399
8453
  // src/launcher.ts
7400
- init_claudeBin();
7401
8454
  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";
8455
+ import { closeSync as closeSync3, existsSync as existsSync7, mkdirSync as mkdirSync7, openSync as openSync3, readdirSync as readdirSync7 } from "fs";
7403
8456
  import { createRequire } from "module";
7404
- import { homedir as homedir3 } from "os";
7405
- import { dirname as dirname3, join as join9 } from "path";
8457
+ import { homedir as homedir4 } from "os";
8458
+ import { dirname as dirname3, join as join12 } from "path";
7406
8459
  import { fileURLToPath } from "url";
7407
8460
 
7408
8461
  // 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";
8462
+ import { lstatSync, mkdirSync as mkdirSync6, readdirSync as readdirSync6, rmSync, statSync as statSync7, symlinkSync as symlinkSync2 } from "fs";
8463
+ import { join as join11, relative, sep } from "path";
7411
8464
  var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
7412
8465
  function droneHomeDeny(env = process.env) {
7413
8466
  const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -7425,34 +8478,34 @@ function buildDroneHome(opts) {
7425
8478
  const { realHome, droneHome } = opts;
7426
8479
  const deny = opts.deny ?? droneHomeDeny();
7427
8480
  const platform = opts.platform ?? process.platform;
7428
- mkdirSync4(droneHome, { recursive: true, mode: 448 });
7429
- const wanted = new Set(mirroredEntries(readdirSync4(realHome), deny, platform));
8481
+ mkdirSync6(droneHome, { recursive: true, mode: 448 });
8482
+ const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
7430
8483
  const rel = relative(realHome, droneHome);
7431
8484
  if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
7432
- for (const existing of readdirSync4(droneHome)) {
8485
+ for (const existing of readdirSync6(droneHome)) {
7433
8486
  if (wanted.has(existing)) continue;
7434
- const p = join8(droneHome, existing);
8487
+ const p = join11(droneHome, existing);
7435
8488
  try {
7436
8489
  if (lstatSync(p).isSymbolicLink()) rmSync(p);
7437
8490
  } catch {
7438
8491
  }
7439
8492
  }
7440
8493
  for (const name of wanted) {
7441
- const link = join8(droneHome, name);
8494
+ const link = join11(droneHome, name);
7442
8495
  try {
7443
8496
  lstatSync(link);
7444
8497
  continue;
7445
8498
  } catch {
7446
8499
  }
7447
- const target = join8(realHome, name);
8500
+ const target = join11(realHome, name);
7448
8501
  let isDir;
7449
8502
  try {
7450
- isDir = statSync5(target).isDirectory();
8503
+ isDir = statSync7(target).isDirectory();
7451
8504
  } catch {
7452
8505
  continue;
7453
8506
  }
7454
8507
  try {
7455
- symlinkSync(target, link, isDir ? "junction" : "file");
8508
+ symlinkSync2(target, link, isDir ? "junction" : "file");
7456
8509
  } catch (e) {
7457
8510
  if (e.code !== "EEXIST") throw e;
7458
8511
  }
@@ -7465,10 +8518,10 @@ var REAL_PATH_ENV_PINS = [
7465
8518
  { env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
7466
8519
  ];
7467
8520
  function realPathEnvPins(realHome) {
7468
- return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join8(realHome, ...segments)]));
8521
+ return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join11(realHome, ...segments)]));
7469
8522
  }
7470
- function droneDenyRules(realHome, swarmHome) {
7471
- const paths = [
8523
+ function dronePrivatePaths(realHome, swarmHome) {
8524
+ return [
7472
8525
  // НЕ `${swarmHome}/**`. Worktree дрона живёт ВНУТРИ swarmHome
7473
8526
  // (`<swarmHome>/worktrees.noindex/<repo>/<slug>`), и правило на весь каталог закрывало
7474
8527
  // дрону его собственный рабочий каталог: Read/Write/Edit отвечали «File is in a directory
@@ -7489,11 +8542,6 @@ function droneDenyRules(realHome, swarmHome) {
7489
8542
  `${realHome}/.claude/.credentials.json`,
7490
8543
  `${realHome}/.npmrc`
7491
8544
  ];
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
8545
  }
7498
8546
 
7499
8547
  // src/testForks.ts
@@ -7525,6 +8573,7 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
7525
8573
  }
7526
8574
 
7527
8575
  // src/launcher.ts
8576
+ init_executors();
7528
8577
  var requireFromHere = createRequire(import.meta.url);
7529
8578
  function resolveTsxCli() {
7530
8579
  try {
@@ -7535,29 +8584,12 @@ function resolveTsxCli() {
7535
8584
  );
7536
8585
  }
7537
8586
  }
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
- }
8587
+ var MCP_SERVER_NAME = "overmind";
7556
8588
  function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
7557
8589
  const env = { ...base3 };
7558
8590
  const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
7559
8591
  if (isolate && !base3.TURBO_CACHE_DIR) {
7560
- env.TURBO_CACHE_DIR = join9(worktree, ".turbo", "cache");
8592
+ env.TURBO_CACHE_DIR = join12(worktree, ".turbo", "cache");
7561
8593
  }
7562
8594
  Object.assign(env, droneTestEnv(droneCap, base3));
7563
8595
  for (const key of DAEMON_STATE_ENV) delete env[key];
@@ -7585,20 +8617,20 @@ var DAEMON_STATE_ENV = [
7585
8617
  "TASKLESS_TOKEN"
7586
8618
  ];
7587
8619
  function bundledRoleDir(kind) {
7588
- return join9(dirname3(fileURLToPath(import.meta.url)), "..", "roles", kind);
8620
+ return join12(dirname3(fileURLToPath(import.meta.url)), "..", "roles", kind);
7589
8621
  }
7590
8622
  function roleDir(worktree, kind) {
7591
- return join9(worktree, ".claude", "roles", kind);
8623
+ return join12(worktree, ".claude", "roles", kind);
7592
8624
  }
7593
8625
  function roleDirs(worktree, kind) {
7594
- return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync6(dir));
8626
+ return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync7(dir));
7595
8627
  }
7596
8628
  function nextRunLogPath(runLogsDir, runId) {
7597
- const dir = join9(runLogsDir, String(runId));
7598
- mkdirSync5(dir, { recursive: true });
8629
+ const dir = join12(runLogsDir, String(runId));
8630
+ mkdirSync7(dir, { recursive: true });
7599
8631
  let files = [];
7600
8632
  try {
7601
- files = readdirSync5(dir);
8633
+ files = readdirSync7(dir);
7602
8634
  } catch {
7603
8635
  }
7604
8636
  let next = 0;
@@ -7606,7 +8638,7 @@ function nextRunLogPath(runLogsDir, runId) {
7606
8638
  const m = /^(\d+)\.json/.exec(f);
7607
8639
  if (m) next = Math.max(next, Number(m[1]) + 1);
7608
8640
  }
7609
- return join9(dir, `${next}.json`);
8641
+ return join12(dir, `${next}.json`);
7610
8642
  }
7611
8643
  function openNextRunLog(runLogsDir, runId) {
7612
8644
  for (let attempt = 0; attempt < 50; attempt++) {
@@ -7641,57 +8673,63 @@ function makeLauncher(cfg) {
7641
8673
  );
7642
8674
  }
7643
8675
  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
- }
8676
+ const mcp = {
8677
+ name: MCP_SERVER_NAME,
8678
+ command: process.execPath,
8679
+ args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["--conditions=development", resolveTsxCli(), cfg.overmindEntry, "--drone", "--run", String(run.id)],
8680
+ // SWARM_DRONE_CHANNEL_URL/TOKEN (TASWM-295) ИЛИ SWARM_DB (откат) — см. channelEnv выше.
8681
+ // Канал: дрон говорит только с демоном, файл базы ему не отдаётся вовсе. Токен подписан
8682
+ // на СТАБИЛЬНЫЙ ключ прогона, поэтому переживает рестарт демона.
8683
+ // SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — доверенный
8684
+ // источник «чья это сессия» (read_task и отчёт о сдаче доверяют ему, а не строке,
8685
+ // найденной по номеру прогона).
8686
+ // SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID (TASWM-263) и SWARM_RUN_NAME
8687
+ // (TASWM-265): роль, связи и имя прогона той же спавн-фиксацией. Нужны РЕЖИМУ ОТКАТА —
8688
+ // ensureRunHydrated заводит ими стаб-строку в sqlite, если демон в client-режиме её там
8689
+ // не заводил. На канале прогон отдаёт демон целиком, и стаб не нужен.
8690
+ // SWARM_WORKSPACE_ID (TASWM-288): ВОРКСПЕЙС прогона — той же спавн-фиксацией: задачные
8691
+ // вызовы (read_task, отчёт, тулы Героя) дрон делает НЕ через канал, а прямо в Taskless,
8692
+ // и воркспейс им нужен верно сразу. Без него read_task/отчёт дрона
8693
+ // ушли бы с глобальным пином демона — ровно то, что чинит эта задача. Пусто
8694
+ // (прогон до фичи / sqlite-бэкенд) → ключ не кладём, дрон берёт глобальный пин.
8695
+ // SWARM_HOME (TASWM-289): дрон едет с ТЕНЕВЫМ HOME, в котором `.taskless-swarm`
8696
+ // нет вовсе, — а дрон-MCP из этого же дерева процессов читает oauth.json
8697
+ // (connectTaskless в runDroneServer), пульс овермайнда и workspace.json. Наследовать
8698
+ // ему нечего, поэтому реальный SWARM_HOME кладём явно. Дрон этот конфиг прочитать
8699
+ // может, но нового ему это не даёт: его собственный worktree лежит ВНУТРИ
8700
+ // SWARM_HOME, путь он и так знает (см. «чего этот модуль не делает» в droneHome.ts).
8701
+ env: {
8702
+ ...channelEnv,
8703
+ ...cfg.droneIsolation ? { SWARM_HOME: cfg.droneIsolation.swarmHome } : {},
8704
+ SWARM_TASK_SLUG: run.task_slug,
8705
+ SWARM_RUN_KIND: run.kind,
8706
+ SWARM_RUN_NAME: run.name,
8707
+ ...run.workspace_id ? { SWARM_WORKSPACE_ID: run.workspace_id } : {},
8708
+ ...run.reviews_run_id != null ? { SWARM_REVIEWS_RUN_ID: String(run.reviews_run_id) } : {},
8709
+ ...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
7681
8710
  }
7682
8711
  };
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();
8712
+ const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
8713
+ const realHome = cfg.droneIsolation?.realHome ?? homedir4();
7688
8714
  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"))) {
8715
+ if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync7(join12(cfg.droneIsolation.swarmHome, "token"))) {
7691
8716
  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.`
8717
+ `[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
8718
  );
7694
8719
  }
8720
+ const executor = cfg.executor ?? resolveExecutor();
8721
+ const plan = executor.plan({
8722
+ worktree,
8723
+ prompt,
8724
+ continueSession: !!continueSession,
8725
+ model,
8726
+ mcp,
8727
+ skillDirs: roleDirs(worktree, run.kind),
8728
+ // Закрытые дрону пути имеют смысл только при теневом HOME: без него дрон и так
8729
+ // сидит в HOME демона, и запирать половину дверей — самообман.
8730
+ denyPaths: cfg.droneIsolation ? dronePrivatePaths(realHome, cfg.droneIsolation.swarmHome) : [],
8731
+ realHome
8732
+ });
7695
8733
  let stdio = "ignore";
7696
8734
  let outFd;
7697
8735
  let errFd;
@@ -7701,15 +8739,16 @@ function makeLauncher(cfg) {
7701
8739
  errFd = fds.err ?? void 0;
7702
8740
  stdio = ["ignore", outFd, errFd ?? "ignore"];
7703
8741
  }
7704
- const child = spawn2(cfg.claudeBin ?? resolveClaudeBin(), claudeArgs(cfgPath, prompt, continueSession, model, pluginDirs, settings), {
8742
+ const child = spawn2(cfg.bin ?? plan.bin, plan.args, {
7705
8743
  cwd: worktree,
7706
8744
  detached: true,
7707
8745
  stdio,
7708
8746
  // Пер-worktree TURBO_CACHE_DIR (TASWM-232): дрон не отыгрывает кеш соседнего
7709
8747
  // worktree, проверки исполняются реально. Теневой HOME + чистка env от состояния
7710
8748
  // демона (TASWM-289). Доля тестовых форков от числа дронов на хосте (TASWM-246).
7711
- // Остальной env демона наследуется, как раньше.
7712
- env: droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null)
8749
+ // Остальной env демона наследуется, как раньше. Добавки адаптера — последними:
8750
+ // они про его собственное состояние, и перебивать их ройным нечем.
8751
+ env: { ...droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null), ...plan.env }
7713
8752
  });
7714
8753
  if (outFd !== void 0) closeSync3(outFd);
7715
8754
  if (errFd !== void 0) closeSync3(errFd);
@@ -7723,25 +8762,25 @@ function makeLauncher(cfg) {
7723
8762
  init_droneChannel();
7724
8763
 
7725
8764
  // src/git.ts
7726
- import { existsSync as existsSync7, readdirSync as readdirSync6, statSync as statSync7 } from "fs";
8765
+ import { existsSync as existsSync8, readdirSync as readdirSync8, statSync as statSync8 } from "fs";
7727
8766
  import { appendFile, mkdir, readFile, rm } from "fs/promises";
7728
- import { isAbsolute as isAbsolute2, join as join10 } from "path";
7729
- import { $ as $4 } from "execa";
8767
+ import { isAbsolute as isAbsolute2, join as join13 } from "path";
8768
+ import { $ as $5 } from "execa";
7730
8769
  function isLinkedWorktree(dir) {
7731
8770
  try {
7732
- return statSync7(join10(dir, ".git")).isFile();
8771
+ return statSync8(join13(dir, ".git")).isFile();
7733
8772
  } catch {
7734
8773
  return false;
7735
8774
  }
7736
8775
  }
7737
8776
  async function detachWorktree(repo, worktree) {
7738
- await $4({ reject: false })`git -C ${repo} worktree remove --force ${worktree}`;
8777
+ await $5({ reject: false })`git -C ${repo} worktree remove --force ${worktree}`;
7739
8778
  if (await ensureWorktreeDirGone(worktree)) {
7740
- await $4({ reject: false })`git -C ${repo} worktree prune`;
8779
+ await $5({ reject: false })`git -C ${repo} worktree prune`;
7741
8780
  }
7742
8781
  }
7743
8782
  async function ensureWorktreeDirGone(worktree) {
7744
- if (!existsSync7(worktree)) return false;
8783
+ if (!existsSync8(worktree)) return false;
7745
8784
  if (!isLinkedWorktree(worktree)) {
7746
8785
  console.error(
7747
8786
  `[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 +8795,16 @@ async function ensureWorktreeDirGone(worktree) {
7756
8795
  );
7757
8796
  return true;
7758
8797
  }
7759
- if (existsSync7(worktree)) {
8798
+ if (existsSync8(worktree)) {
7760
8799
  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
8800
  }
7762
8801
  return true;
7763
8802
  }
7764
8803
  async function removeWorktree(repo, path) {
7765
- const branch = (await $4({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
8804
+ const branch = (await $5({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
7766
8805
  await cleanupWorktreeProcesses(path);
7767
8806
  await detachWorktree(repo, path);
7768
- if (branch) await $4({ reject: false })`git -C ${repo} branch -D ${branch}`;
8807
+ if (branch) await $5({ reject: false })`git -C ${repo} branch -D ${branch}`;
7769
8808
  }
7770
8809
 
7771
8810
  // src/cli.ts
@@ -7778,7 +8817,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
7778
8817
 
7779
8818
  // src/attachments.ts
7780
8819
  import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
7781
- import { join as join12 } from "path";
8820
+ import { join as join15 } from "path";
7782
8821
  var ATTACH_MAX_TOTAL_BYTES = 24 * 1024 * 1024;
7783
8822
  var ATTACH_MAX_FILE_BYTES = 8 * 1024 * 1024;
7784
8823
  var IMAGE_EXT = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "avif", "heic"]);
@@ -7788,7 +8827,7 @@ var KNOWN_EXT = /* @__PURE__ */ new Set([...IMAGE_EXT, ...DOC_EXT, ...TEXT_EXT])
7788
8827
 
7789
8828
  // src/shotUpload.ts
7790
8829
  import { readFile as fsReadFile } from "fs/promises";
7791
- import { basename as basename3, isAbsolute as isAbsolute3, join as join13 } from "path";
8830
+ import { basename as basename3, isAbsolute as isAbsolute3, join as join16 } from "path";
7792
8831
  var SHOT_MAX_FILE_BYTES = 10 * 1024 * 1024;
7793
8832
 
7794
8833
  // src/taskless.ts
@@ -7798,8 +8837,8 @@ init_agentToken();
7798
8837
  // src/workspace.ts
7799
8838
  init_config();
7800
8839
  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");
8840
+ import { join as join17 } from "path";
8841
+ var WORKSPACE_PATH = join17(SWARM_HOME, "workspace.json");
7803
8842
  function loadWorkspace() {
7804
8843
  try {
7805
8844
  const w = JSON.parse(readFileSync11(WORKSPACE_PATH, "utf8"));
@@ -7883,7 +8922,9 @@ async function connectSwarmMcp(cfg) {
7883
8922
  try {
7884
8923
  await client.connect(transport);
7885
8924
  } catch (e) {
7886
- console.warn(`Swarm MCP: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 \u0431\u0443\u0444\u0435\u0440 \u0432 outbox.`);
8925
+ console.warn(
8926
+ isRevokedTokenError(e) ? `${revokedTokenLine()} (\u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442 \u2014 \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u043A\u043E\u043F\u044F\u0442\u0441\u044F \u0432 outbox)` : `Swarm MCP: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 \u0431\u0443\u0444\u0435\u0440 \u0432 outbox.`
8927
+ );
7887
8928
  return null;
7888
8929
  }
7889
8930
  return async (name, args) => await client.callTool({ name, arguments: swarmToolArgs(name, args, workspace) });
@@ -7959,6 +9000,7 @@ function resilientSwarmMcp(opts = {}) {
7959
9000
 
7960
9001
  // src/login.ts
7961
9002
  import { execa as execa2 } from "execa";
9003
+ import { createServer } from "net";
7962
9004
  async function waitForLogin(d) {
7963
9005
  const deadline = d.now() + d.timeoutMs;
7964
9006
  for (; ; ) {
@@ -7968,6 +9010,53 @@ async function waitForLogin(d) {
7968
9010
  await d.sleep(d.pollMs);
7969
9011
  }
7970
9012
  }
9013
+ async function withDaemonStopped(d, body) {
9014
+ const st = await d.status();
9015
+ if (!st) {
9016
+ if (!await d.portFree()) {
9017
+ d.err(
9018
+ `\u0432\u0445\u043E\u0434 \u041D\u0415 \u0441\u043E\u0441\u0442\u043E\u044F\u043B\u0441\u044F: \u043F\u043E\u0440\u0442 ${d.port} \u0437\u0430\u043D\u044F\u0442, \u043D\u043E \u044D\u0442\u043E \u043D\u0435 \u0434\u0435\u043C\u043E\u043D \u0440\u043E\u044F (\u0435\u0433\u043E \u043C\u043E\u0440\u0434\u0430 \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442). \u0421\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u0432\u0445\u043E\u0434\u0430 \u043E\u0431\u044F\u0437\u0430\u043D\u0430 \u043F\u043E\u0434\u043D\u044F\u0442\u044C\u0441\u044F \u0438\u043C\u0435\u043D\u043D\u043E \u043D\u0430 \u043D\u0451\u043C \u2014 \u043E\u043D \u0437\u0430\u0448\u0438\u0442 \u0432 redirect_uri. \u041E\u0441\u0432\u043E\u0431\u043E\u0434\u0438 \u043F\u043E\u0440\u0442 \u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0438: swarm login`
9019
+ );
9020
+ return 1;
9021
+ }
9022
+ const code2 = await body();
9023
+ if (code2 === 0) d.out("\u0434\u0435\u043C\u043E\u043D \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442 \u2014 \u0440\u043E\u0439 \u0436\u0434\u0451\u0442 \u0437\u0430\u043F\u0443\u0441\u043A\u0430: swarm start");
9024
+ return code2;
9025
+ }
9026
+ d.out(
9027
+ st.live.length ? `\u0434\u0435\u043C\u043E\u043D \u0440\u043E\u044F \u043F\u043E\u0434\u043D\u044F\u0442, \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439: ${st.live.length}. \u0413\u0430\u0448\u0443 \u0435\u0433\u043E \u0441\u0430\u043C \u2014 \u0432\u0445\u043E\u0434 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0443 \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 \u043F\u043E\u0440\u0442\u0443 ${d.port}.` : `\u0434\u0435\u043C\u043E\u043D \u0440\u043E\u044F \u043F\u043E\u0434\u043D\u044F\u0442 \u2014 \u0433\u0430\u0448\u0443 \u0435\u0433\u043E \u0441\u0430\u043C: \u0432\u0445\u043E\u0434 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0443 \u043D\u0430 \u0442\u043E\u043C \u0436\u0435 \u043F\u043E\u0440\u0442\u0443 ${d.port}.`
9028
+ );
9029
+ if (await d.stop() !== "stopped") {
9030
+ d.err(`\u0432\u0445\u043E\u0434 \u041D\u0415 \u0441\u043E\u0441\u0442\u043E\u044F\u043B\u0441\u044F: \u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u043F\u043E\u0433\u0430\u0448\u0435\u043D (\u0441\u043C. \u0432\u044B\u0448\u0435), \u043F\u043E\u0440\u0442 ${d.port} \u0437\u0430\u043D\u044F\u0442 \u0438\u043C. \u0420\u043E\u0439 \u043E\u0441\u0442\u0430\u043B\u0441\u044F \u043A\u0430\u043A \u0431\u044B\u043B.`);
9031
+ return 1;
9032
+ }
9033
+ if (!await d.portFree()) {
9034
+ d.err(`\u0432\u0445\u043E\u0434 \u041D\u0415 \u0441\u043E\u0441\u0442\u043E\u044F\u043B\u0441\u044F: \u0434\u0435\u043C\u043E\u043D \u043F\u043E\u0433\u0430\u0448\u0435\u043D, \u043D\u043E \u043F\u043E\u0440\u0442 ${d.port} \u0432\u0441\u0451 \u0440\u0430\u0432\u043D\u043E \u0437\u0430\u043D\u044F\u0442 \u043A\u0435\u043C-\u0442\u043E \u0435\u0449\u0451. \u041F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0440\u043E\u0439 \u043E\u0431\u0440\u0430\u0442\u043D\u043E.`);
9035
+ await restoreDaemon(d, false);
9036
+ return 1;
9037
+ }
9038
+ let code = 1;
9039
+ try {
9040
+ code = await body();
9041
+ return code;
9042
+ } finally {
9043
+ await restoreDaemon(d, code === 0);
9044
+ }
9045
+ }
9046
+ async function restoreDaemon(d, loginOk) {
9047
+ d.out(
9048
+ loginOk ? "\u0432\u0445\u043E\u0434 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0434\u0435\u043C\u043E\u043D\u0430 \u043E\u0431\u0440\u0430\u0442\u043D\u043E, \u0443\u0436\u0435 \u0441 \u043D\u043E\u0432\u044B\u043C \u0442\u043E\u043A\u0435\u043D\u043E\u043C\u2026" : "\u0432\u0445\u043E\u0434 \u043D\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043B\u0441\u044F \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E \u0434\u0435\u043C\u043E\u043D\u0430 \u0432 \u043F\u0440\u0435\u0436\u043D\u0435\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435\u2026"
9049
+ );
9050
+ if (await d.start()) return;
9051
+ d.err("\u0434\u0435\u043C\u043E\u043D \u043E\u0431\u0440\u0430\u0442\u043D\u043E \u041D\u0415 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F (\u043F\u0440\u0438\u0447\u0438\u043D\u0430 \u0432\u044B\u0448\u0435) \u2014 \u0440\u043E\u0439 \u0441\u0442\u043E\u0438\u0442. \u041F\u043E\u0434\u043D\u0438\u043C\u0438 \u0432\u0440\u0443\u0447\u043D\u0443\u044E: swarm start");
9052
+ }
9053
+ function isPortFree(port, host = "127.0.0.1") {
9054
+ return new Promise((resolve4) => {
9055
+ const probe = createServer();
9056
+ probe.once("error", () => resolve4(false));
9057
+ probe.listen(port, host, () => probe.close(() => resolve4(true)));
9058
+ });
9059
+ }
7971
9060
  async function chooseWorkspace(d) {
7972
9061
  if (d.list.length === 0) {
7973
9062
  d.out("\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u043E\u0432 (list_workspaces \u043F\u0443\u0441\u0442)");
@@ -8110,14 +9199,14 @@ async function runShotCli(argv, deps = {}) {
8110
9199
  init_daemonLink();
8111
9200
 
8112
9201
  // 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";
9202
+ import { existsSync as existsSync9, readFileSync as readFileSync12, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync10 } from "fs";
9203
+ import { join as join18 } from "path";
8115
9204
  function pauseStatePath(home) {
8116
- return join15(home, "pause.json");
9205
+ return join18(home, "pause.json");
8117
9206
  }
8118
9207
  var ACTIVE = { paused: false, at: "", source: "cli" };
8119
9208
  function loadPauseState(home, io = {}) {
8120
- const exists = io.exists ?? existsSync8;
9209
+ const exists = io.exists ?? existsSync9;
8121
9210
  const readFile2 = io.readFile ?? ((p) => readFileSync12(p, "utf8"));
8122
9211
  const path = pauseStatePath(home);
8123
9212
  if (!exists(path)) return ACTIVE;
@@ -8137,7 +9226,7 @@ function loadPauseState(home, io = {}) {
8137
9226
  function savePauseState(home, state, io = {}) {
8138
9227
  const path = pauseStatePath(home);
8139
9228
  if (!state.paused) {
8140
- const remove = io.remove ?? ((p) => existsSync8(p) && unlinkSync5(p));
9229
+ const remove = io.remove ?? ((p) => existsSync9(p) && unlinkSync5(p));
8141
9230
  remove(path);
8142
9231
  return;
8143
9232
  }
@@ -8239,6 +9328,59 @@ function takeWorkspace(argv) {
8239
9328
  return { workspace, rest: [...argv.slice(0, i), ...argv.slice(i + 2)] };
8240
9329
  }
8241
9330
  async function runLogin(db2, opts) {
9331
+ return withDaemonStopped(
9332
+ {
9333
+ status: async () => {
9334
+ const st = await daemonStatus2();
9335
+ return st ? { live: st.live } : null;
9336
+ },
9337
+ stop: stopDaemonForLogin,
9338
+ portFree: () => isPortFree(WEB_PORT),
9339
+ // Аргументы не передаём намеренно: пути к репозиториям демон помнит с прошлого
9340
+ // запуска (TASWM-335), а `swarm login` их и не знает — он про вход, не про репо.
9341
+ // А вот ОКРУЖЕНИЕ поднятого обратно демона — наше: если `swarm login` зовут из
9342
+ // шелла без SWARM_BACKEND, рой вернётся в sqlite-режиме, то есть слепым к
9343
+ // воркспейсу. В опубликованном пакете дефолт уже client (SWARM_DIST_BUILD) —
9344
+ // предупреждение достаётся монорепе, где это реальная разница.
9345
+ start: async () => {
9346
+ if (SWARM_BACKEND !== "client") {
9347
+ console.warn("SWARM_BACKEND=client \u043D\u0435 \u0437\u0430\u0434\u0430\u043D \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0434\u0435\u043C\u043E\u043D\u0430 \u043E\u0431\u0440\u0430\u0442\u043D\u043E \u0432 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u043C sqlite-\u0440\u0435\u0436\u0438\u043C\u0435, \u0411\u0415\u0417 \u0441\u0432\u044F\u0437\u0438 \u0441 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u043E\u043C Taskless.");
9348
+ }
9349
+ return await runStartDetached([]) === 0;
9350
+ },
9351
+ out: (s) => console.log(s),
9352
+ err: (s) => console.error(s),
9353
+ port: WEB_PORT
9354
+ },
9355
+ () => runLoginFlow(db2, opts)
9356
+ );
9357
+ }
9358
+ async function stopDaemonForLogin() {
9359
+ const res = await daemonStop({ force: false, wait: true });
9360
+ if (!res) {
9361
+ const beat = readHeartbeat(HEARTBEAT_PATH);
9362
+ if (beat != null && Date.now() - beat < 6e4) {
9363
+ console.error(
9364
+ `\u0434\u0435\u043C\u043E\u043D \u043F\u043E\u0434\u0430\u0451\u0442 \u043F\u0443\u043B\u044C\u0441 (${HEARTBEAT_PATH}), \u043D\u043E \u043C\u043E\u0440\u0434\u0430 127.0.0.1:${WEB_PORT} \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u2014 \u0441\u0430\u043C \u0441\u0435\u0431\u044F \u043F\u043E\u0433\u0430\u0441\u0438\u0442\u044C \u043E\u043D \u043D\u0435 \u043C\u043E\u0436\u0435\u0442. \u041F\u043E\u0445\u043E\u0436\u0435, \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0437\u0430\u0432\u0438\u0441: \u043D\u0430\u0439\u0434\u0438 \u0435\u0433\u043E (ps) \u0438 \u043F\u043E\u0433\u0430\u0441\u0438 \u0441\u0438\u0433\u043D\u0430\u043B\u043E\u043C \u0432\u0440\u0443\u0447\u043D\u0443\u044E, \u043F\u043E\u0442\u043E\u043C \u043F\u043E\u0432\u0442\u043E\u0440\u0438 swarm login.`
9365
+ );
9366
+ return "failed";
9367
+ }
9368
+ return "stopped";
9369
+ }
9370
+ if (res.waiting) {
9371
+ console.log(`\u0436\u0434\u0443 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 (${res.sessions.length}): ${await liveSessionNames(res.sessions)} \u2014 \u043D\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u0440\u043E\u0439 \u0443\u0436\u0435 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442`);
9372
+ console.log("\u043E\u0431\u043E\u0440\u0432\u0430\u0442\u044C \u0438\u0445 \u0438 \u0432\u043E\u0439\u0442\u0438 \u043F\u0440\u044F\u043C\u043E \u0441\u0435\u0439\u0447\u0430\u0441: swarm stop --force \u0432 \u0434\u0440\u0443\u0433\u043E\u043C \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435");
9373
+ } else {
9374
+ console.log("\u0436\u0438\u0432\u044B\u0445 \u0441\u0435\u0441\u0441\u0438\u0439 \u043D\u0435 \u0431\u044B\u043B\u043E \u2014 \u0434\u0435\u043C\u043E\u043D \u043F\u043E\u0433\u0430\u0448\u0435\u043D");
9375
+ }
9376
+ const down = res.waiting ? await waitDaemonDown({ timeoutMs: 6 * 60 * 6e4 }) : await waitDaemonDown({ timeoutMs: 3e4, pollMs: 500 });
9377
+ if (down) return "stopped";
9378
+ console.error(
9379
+ res.waiting ? "\u0436\u0434\u0443 \u0431\u043E\u043B\u044C\u0448\u0435 6 \u0447\u0430\u0441\u043E\u0432 \u2014 \u0434\u0435\u043C\u043E\u043D \u0432\u0441\u0451 \u0435\u0449\u0451 \u0433\u0430\u0441\u043D\u0435\u0442. \u0412\u0445\u043E\u0434 \u043D\u0435 \u043D\u0430\u0447\u0438\u043D\u0430\u044E: \u043F\u043E\u0440\u0442 \u0437\u0430\u043D\u044F\u0442 \u0438\u043C (swarm status \u043F\u043E\u043A\u0430\u0436\u0435\u0442, \u043A\u0442\u043E \u0435\u0449\u0451 \u0436\u0438\u0432)." : "\u0434\u0435\u043C\u043E\u043D \u043F\u0440\u0438\u043D\u044F\u043B \u043A\u043E\u043C\u0430\u043D\u0434\u0443, \u043D\u043E \u0437\u0430 30\u0441 \u043D\u0435 \u0432\u044B\u0448\u0435\u043B \u2014 \u0432\u0445\u043E\u0434 \u043D\u0435 \u043D\u0430\u0447\u0438\u043D\u0430\u044E (\u043F\u043E\u0440\u0442 \u0437\u0430\u043D\u044F\u0442 \u0438\u043C). \u041F\u0440\u043E\u0432\u0435\u0440\u044C: swarm status"
9380
+ );
9381
+ return "failed";
9382
+ }
9383
+ async function runLoginFlow(db2, opts) {
8242
9384
  const web = startWeb({ db: db2 });
8243
9385
  try {
8244
9386
  let agent = loadAgentToken();
@@ -8326,7 +9468,7 @@ async function runLogin(db2, opts) {
8326
9468
  console.log(
8327
9469
  others.length ? `\u0440\u043E\u0439 \u0431\u0435\u0440\u0451\u0442 \u0440\u0430\u0431\u043E\u0442\u0443 \u0418\u0417 \u0412\u0421\u0415\u0425 \u0442\u0432\u043E\u0438\u0445 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u043E\u0432 (\u0435\u0449\u0451 ${others.length}: ${others.map((w) => w.slug).join(", ")}) \u2014 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u043F\u0440\u0438\u0435\u0437\u0436\u0430\u0435\u0442 \u0441 \u043A\u0430\u0436\u0434\u044B\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u043E\u043C. \u0412\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0439 \u043D\u0443\u0436\u0435\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0432\u044B\u0437\u043E\u0432\u043E\u0432 \u043D\u0435 \u043F\u0440\u043E \u043F\u0440\u043E\u0433\u043E\u043D.` : "\u044D\u0442\u043E \u0435\u0434\u0438\u043D\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439 \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441 \u0442\u043E\u043A\u0435\u043D\u0430."
8328
9470
  );
8329
- console.log("\u0433\u043E\u0442\u043E\u0432\u043E. \u041F\u043E\u0434\u043D\u044F\u0442\u044C \u0434\u0435\u043C\u043E\u043D\u0430: swarm start --repo <\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u043F\u043E>");
9471
+ console.log("\u0433\u043E\u0442\u043E\u0432\u043E \u2014 \u0432\u0445\u043E\u0434 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D.");
8330
9472
  return 0;
8331
9473
  } finally {
8332
9474
  web.server.close();
@@ -8417,7 +9559,7 @@ async function runDaemonLogs(argv) {
8417
9559
  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
9560
  return 1;
8419
9561
  }
8420
- if (!existsSync13(DAEMON_LOG_PATH)) {
9562
+ if (!existsSync14(DAEMON_LOG_PATH)) {
8421
9563
  console.error(`\u043B\u043E\u0433\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442: ${DAEMON_LOG_PATH}`);
8422
9564
  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
9565
  return 1;
@@ -8425,12 +9567,12 @@ async function runDaemonLogs(argv) {
8425
9567
  const head = tailFile(DAEMON_LOG_PATH, lines);
8426
9568
  if (head) console.log(head);
8427
9569
  if (!follow) return 0;
8428
- let pos = statSync8(DAEMON_LOG_PATH).size;
9570
+ let pos = statSync9(DAEMON_LOG_PATH).size;
8429
9571
  for (; ; ) {
8430
9572
  await sleep2(500);
8431
9573
  let size;
8432
9574
  try {
8433
- size = statSync8(DAEMON_LOG_PATH).size;
9575
+ size = statSync9(DAEMON_LOG_PATH).size;
8434
9576
  } catch {
8435
9577
  continue;
8436
9578
  }
@@ -8602,7 +9744,9 @@ function usage() {
8602
9744
  swarm update \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0440\u043E\u0439 \u0434\u043E \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043D\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 (\u0442\u0435\u043C \u0436\u0435 \u043C\u0435\u043D\u0435\u0434\u0436\u0435\u0440\u043E\u043C,
8603
9745
  \u043A\u0430\u043A\u0438\u043C \u043E\u043D \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D)
8604
9746
  swarm login [--workspace <slug|id>] [--no-open]
8605
- \u0432\u043E\u0439\u0442\u0438 \u0432 Taskless \u0438 \u0432\u044B\u0431\u0440\u0430\u0442\u044C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441
9747
+ \u0432\u043E\u0439\u0442\u0438 \u0432 Taskless \u0438 \u0432\u044B\u0431\u0440\u0430\u0442\u044C \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441. \u0414\u0435\u043C\u043E\u043D\u0430 \u0433\u0430\u0441\u0438\u0442 \u0438 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u0442
9748
+ \u0441\u0430\u043C\u0430 (\u0436\u0438\u0432\u044B\u0435 \u0441\u0435\u0441\u0441\u0438\u0438 \u0434\u043E\u0438\u0433\u0440\u044B\u0432\u0430\u044E\u0442) \u2014 \u043F\u0435\u0440\u0435\u043B\u043E\u0433\u0438\u043D \u044D\u0442\u043E \u043E\u0434\u043D\u0430 \u043A\u043E\u043C\u0430\u043D\u0434\u0430,
9749
+ \u0431\u0435\u0437 stop/start \u0440\u0443\u043A\u0430\u043C\u0438
8606
9750
  swarm start [--repo <path>] [--foreground]
8607
9751
  \u043F\u043E\u0434\u043D\u044F\u0442\u044C \u0440\u043E\u0439 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435: \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0435\u0442 \u043E\u0447\u0435\u0440\u0435\u0434\u044C, \u0441\u043F\u0430\u0432\u043D\u0438\u0442 \u0438 \u0431\u0443\u0434\u0438\u0442 \u0434\u0440\u043E\u043D\u043E\u0432.
8608
9752
  \u0420\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0430\u0445\u043E\u0434\u0438\u0442 \u0441\u0430\u043C \u2014 \u043A\u043B\u043E\u043D\u044B \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 Taskless \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435;
@@ -8614,6 +9758,10 @@ function usage() {
8614
9758
  swarm spawn <slug> [area] [\u0444\u043B\u0430\u0433\u0438 \u043D\u0438\u0436\u0435]
8615
9759
  \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u044C
8616
9760
  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
9761
+ swarm zones [check|suggest <\u0442\u0435\u043A\u0441\u0442|slug>]
9762
+ \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
9763
+ \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
9764
+ \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
9765
  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
9766
  swarm cancel <slug> <\u043F\u0440\u0438\u0447\u0438\u043D\u0430\u2026>
8619
9767
  \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 +9814,10 @@ function effectiveWorktreesDir() {
8666
9814
  legacyDir: WORKTREES_LEGACY_DIR,
8667
9815
  targetDir: WORKTREES_DIR,
8668
9816
  envOverride: process.env.SWARM_WORKTREES || "",
8669
- exists: (d) => existsSync13(d),
9817
+ exists: (d) => existsSync14(d),
8670
9818
  isNonEmptyDir: (d) => {
8671
9819
  try {
8672
- return readdirSync7(d).length > 0;
9820
+ return readdirSync9(d).length > 0;
8673
9821
  } catch {
8674
9822
  return false;
8675
9823
  }
@@ -8694,6 +9842,80 @@ async function makeCliStore(repo) {
8694
9842
  client.reconcileRuntime({ worktreesDir: effectiveWorktreesDir(), listProcesses: listDroneProcesses });
8695
9843
  return client;
8696
9844
  }
9845
+ async function runZones(db2, repo, args) {
9846
+ const manual = new Map(db2.listRuns().map((r) => [r.task_slug, r.area]));
9847
+ const raw = await readTaskHistory(repo);
9848
+ let fromScope = 0;
9849
+ for (const e of raw) {
9850
+ if (manual.get(e.slug)) continue;
9851
+ const scope = scopeFromText(e.text);
9852
+ if (scope) {
9853
+ manual.set(e.slug, scope);
9854
+ fromScope++;
9855
+ }
9856
+ }
9857
+ const history = withManualAreas(raw, manual);
9858
+ const model = buildZoneModel(history);
9859
+ if (!hasZoneModel(model)) {
9860
+ 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:*`);
9861
+ return;
9862
+ }
9863
+ const sub = args[0];
9864
+ if (sub === "check") {
9865
+ const a = checkZoneAgreement(model);
9866
+ if (!a.checked) {
9867
+ 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)");
9868
+ return;
9869
+ }
9870
+ console.log(
9871
+ `\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)`
9872
+ );
9873
+ if (fromScope) {
9874
+ console.log(
9875
+ ` \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.`
9876
+ );
9877
+ }
9878
+ const bad = a.rows.filter((r) => r.verdict === "mismatch");
9879
+ if (bad.length) {
9880
+ console.log(`
9881
+ \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:`);
9882
+ 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}`);
9883
+ }
9884
+ return;
9885
+ }
9886
+ if (sub === "suggest") {
9887
+ const query = args.slice(1).join(" ").trim();
9888
+ if (!query) {
9889
+ 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"');
9890
+ process.exit(1);
9891
+ }
9892
+ const bySlug = db2.listRuns().find((r) => r.task_slug === query);
9893
+ const text = bySlug ? [bySlug.task_title ?? "", bySlug.task_desc ?? ""].join(" ").trim() || query : query;
9894
+ const guesses = suggestZonesFromText(model, text);
9895
+ if (!guesses.length) {
9896
+ 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");
9897
+ return;
9898
+ }
9899
+ for (const g of guesses) console.log(`${g.zone.padEnd(20)} ${String(Math.round(g.confidence * 100)).padStart(3)}% ${g.why}`);
9900
+ const files = predictFiles(model, { text });
9901
+ if (files.length) {
9902
+ console.log("\n\u0432\u0435\u0440\u043E\u044F\u0442\u043D\u043E \u0442\u0440\u043E\u043D\u0435\u0442:");
9903
+ for (const f of files.slice(0, 10)) console.log(` ${f.path} (${f.why})`);
9904
+ }
9905
+ return;
9906
+ }
9907
+ 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):
9908
+ `);
9909
+ for (const z of model.zones.filter((x) => x.zone !== SPINE_ZONE)) {
9910
+ 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(", ")}`);
9911
+ }
9912
+ if (model.spine.length) {
9913
+ console.log(`
9914
+ \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:`);
9915
+ for (const f of model.spine.slice(0, 15)) console.log(` ${f}`);
9916
+ }
9917
+ 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");
9918
+ }
8697
9919
  async function finishClient(db2) {
8698
9920
  if (!(db2 instanceof SwarmClient)) return;
8699
9921
  await db2.flush();
@@ -8993,6 +10215,9 @@ switch (cmd) {
8993
10215
  overmindEntry: OVERMIND_ENTRY,
8994
10216
  dbPath: DB_PATH,
8995
10217
  runLogsDir: RUN_LOGS_DIR,
10218
+ // Тот же исполнитель, что у демона (TASWM-349): `swarm wake` при мёртвом демоне не
10219
+ // должен внезапно поднимать дрона другим харнессом.
10220
+ executor: EXECUTOR,
8996
10221
  droneIsolation: DRONE_ISOLATION,
8997
10222
  ...DRONE_CHANNEL_ENABLED ? {
8998
10223
  channel: {
@@ -9088,6 +10313,20 @@ switch (cmd) {
9088
10313
  db.close();
9089
10314
  process.exit(code);
9090
10315
  }
10316
+ // TASWM-348: зоны кода, выведенные из истории репозитория.
10317
+ // swarm zones — какие зоны видит рой и что считает общим хребтом
10318
+ // swarm zones check — сверка с ручной разметкой area:* на прошлых задачах
10319
+ // swarm zones suggest <текст|slug> — какую зону предложить задаче ПРИ ПОСТАНОВКЕ
10320
+ case "zones": {
10321
+ const { repo: repoFlag, rest } = takeRepo(rawArgs);
10322
+ const repo = repoFlag ?? SWARM_REPO;
10323
+ if (!repo) {
10324
+ console.error("\u043D\u0435\u0442 \u0440\u0435\u043F\u043E: \u0437\u0430\u0434\u0430\u0439 --repo <path> \u0438\u043B\u0438 SWARM_REPO");
10325
+ process.exit(1);
10326
+ }
10327
+ await runZones(db, repo, rest);
10328
+ break;
10329
+ }
9091
10330
  case "web": {
9092
10331
  const web = startWeb({ db });
9093
10332
  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`);