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