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