@taskless-app/swarm 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/dist/cli.js +1427 -307
- package/dist/overmind.js +1912 -548
- package/package.json +1 -1
package/dist/overmind.js
CHANGED
|
@@ -720,6 +720,14 @@ var init_hosts = __esm({
|
|
|
720
720
|
}
|
|
721
721
|
});
|
|
722
722
|
|
|
723
|
+
// ../swarm-core/src/hostPool.ts
|
|
724
|
+
var init_hostPool = __esm({
|
|
725
|
+
"../swarm-core/src/hostPool.ts"() {
|
|
726
|
+
"use strict";
|
|
727
|
+
init_hosts();
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
723
731
|
// ../swarm-core/src/hostCommands.ts
|
|
724
732
|
function isHostCommandKind(v) {
|
|
725
733
|
return typeof v === "string" && HOST_COMMAND_KINDS.includes(v);
|
|
@@ -729,17 +737,20 @@ function isHostCommandExpired(expiresAt, nowMs) {
|
|
|
729
737
|
const at = Date.parse(expiresAt);
|
|
730
738
|
return Number.isFinite(at) && at <= nowMs;
|
|
731
739
|
}
|
|
740
|
+
function isRunScopedCommand(kind) {
|
|
741
|
+
return RUN_SCOPED.has(kind);
|
|
742
|
+
}
|
|
732
743
|
function hostCommandLabel(cmd) {
|
|
733
744
|
const base3 = HOST_COMMAND_LABELS[cmd.kind];
|
|
734
745
|
if (cmd.kind === "set_max_drones" && cmd.maxDrones != null) return `${base3} \u2192 ${cmd.maxDrones}`;
|
|
735
|
-
if (cmd.kind
|
|
746
|
+
if (isRunScopedCommand(cmd.kind) && cmd.runSlug) return `${base3} ${cmd.runSlug}`;
|
|
736
747
|
return base3;
|
|
737
748
|
}
|
|
738
|
-
var HOST_COMMAND_KINDS, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, UPDATE_STEP_LABELS, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_LABELS, MAX_DRONES_LIMIT;
|
|
749
|
+
var HOST_COMMAND_KINDS, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, UPDATE_STEP_LABELS, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_LABELS, RUN_SCOPED_COMMAND_KINDS, RUN_SCOPED, MAX_DRONES_LIMIT;
|
|
739
750
|
var init_hostCommands = __esm({
|
|
740
751
|
"../swarm-core/src/hostCommands.ts"() {
|
|
741
752
|
"use strict";
|
|
742
|
-
HOST_COMMAND_KINDS = ["pause", "resume", "restart", "update", "set_max_drones", "stop_run"];
|
|
753
|
+
HOST_COMMAND_KINDS = ["pause", "resume", "restart", "update", "set_max_drones", "stop_run", "wake_run"];
|
|
743
754
|
HOST_COMMAND_STATUSES = ["pending", "delivered", "running", "done", "failed", "rejected", "expired"];
|
|
744
755
|
TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "rejected", "expired"]);
|
|
745
756
|
HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
|
|
@@ -758,8 +769,11 @@ var init_hostCommands = __esm({
|
|
|
758
769
|
restart: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A",
|
|
759
770
|
update: "\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0434\u043E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u043A\u043E\u0434\u0430",
|
|
760
771
|
set_max_drones: "\u0441\u043C\u0435\u043D\u0430 \u0447\u0438\u0441\u043B\u0430 \u0434\u0440\u043E\u043D\u043E\u0432",
|
|
761
|
-
stop_run: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430"
|
|
772
|
+
stop_run: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
|
|
773
|
+
wake_run: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0441 \u0442\u043E\u0433\u043E \u0436\u0435 \u043C\u0435\u0441\u0442\u0430"
|
|
762
774
|
};
|
|
775
|
+
RUN_SCOPED_COMMAND_KINDS = ["stop_run", "wake_run"];
|
|
776
|
+
RUN_SCOPED = new Set(RUN_SCOPED_COMMAND_KINDS);
|
|
763
777
|
MAX_DRONES_LIMIT = 8;
|
|
764
778
|
}
|
|
765
779
|
});
|
|
@@ -774,6 +788,436 @@ var init_relief = __esm({
|
|
|
774
788
|
}
|
|
775
789
|
});
|
|
776
790
|
|
|
791
|
+
// ../swarm-core/src/cost.ts
|
|
792
|
+
var EMPTY_TASK_COST;
|
|
793
|
+
var init_cost = __esm({
|
|
794
|
+
"../swarm-core/src/cost.ts"() {
|
|
795
|
+
"use strict";
|
|
796
|
+
EMPTY_TASK_COST = Object.freeze({
|
|
797
|
+
costUsd: 0,
|
|
798
|
+
tokensIn: 0,
|
|
799
|
+
tokensOut: 0,
|
|
800
|
+
durationMs: 0,
|
|
801
|
+
runs: 0,
|
|
802
|
+
respawns: 0
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// ../swarm-core/src/runGuards.ts
|
|
808
|
+
var DEFAULT_RUN_LIMITS;
|
|
809
|
+
var init_runGuards = __esm({
|
|
810
|
+
"../swarm-core/src/runGuards.ts"() {
|
|
811
|
+
"use strict";
|
|
812
|
+
DEFAULT_RUN_LIMITS = {
|
|
813
|
+
maxCostUsd: 20,
|
|
814
|
+
runTimeoutMs: 8 * 60 * 6e4,
|
|
815
|
+
stepTimeoutMs: 90 * 6e4,
|
|
816
|
+
lostMs: 30 * 6e4
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
// ../swarm-core/src/codeZones.ts
|
|
822
|
+
function hasZoneModel(m) {
|
|
823
|
+
return m.tasks > 0 && m.zones.length > 0;
|
|
824
|
+
}
|
|
825
|
+
function testFileFeature(basename5) {
|
|
826
|
+
const head = basename5.replace(/\.(test|spec)\.[a-z]+$/i, "").split(".")[0].toLowerCase();
|
|
827
|
+
return head && !GENERIC_DIRS.has(head) ? head : null;
|
|
828
|
+
}
|
|
829
|
+
function pathZoneCandidate(path) {
|
|
830
|
+
const parts = path.split("/").filter(Boolean);
|
|
831
|
+
if (parts.length <= 1) return "root";
|
|
832
|
+
if (WORKSPACE_DIRS.has(parts[0]) && parts.length >= 3) {
|
|
833
|
+
const pkg = parts[1];
|
|
834
|
+
if (parts[2] === "src" && parts.length >= 5) {
|
|
835
|
+
const feature = parts[3];
|
|
836
|
+
if (!GENERIC_DIRS.has(feature)) return feature;
|
|
837
|
+
}
|
|
838
|
+
if (GENERIC_TEST_DIRS.has(parts[2])) {
|
|
839
|
+
const feature = testFileFeature(parts[parts.length - 1]);
|
|
840
|
+
if (feature) return feature;
|
|
841
|
+
}
|
|
842
|
+
return pkg;
|
|
843
|
+
}
|
|
844
|
+
if (parts[0] === "src" && parts.length >= 3 && !GENERIC_DIRS.has(parts[1])) return parts[1];
|
|
845
|
+
return parts[0];
|
|
846
|
+
}
|
|
847
|
+
function dirOf(path) {
|
|
848
|
+
const i = path.lastIndexOf("/");
|
|
849
|
+
return i < 0 ? "" : path.slice(0, i);
|
|
850
|
+
}
|
|
851
|
+
function tokenize(text) {
|
|
852
|
+
const out = [];
|
|
853
|
+
for (const raw of text.split(/[^\p{L}\p{N}]+/u)) {
|
|
854
|
+
if (!raw) continue;
|
|
855
|
+
for (const piece of raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")) {
|
|
856
|
+
const t = piece.toLowerCase();
|
|
857
|
+
if (t.length < 3 || STOP_TERMS.has(t) || /^\d+$/.test(t)) continue;
|
|
858
|
+
out.push(t);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return out;
|
|
862
|
+
}
|
|
863
|
+
function normalizeZoneName(raw) {
|
|
864
|
+
if (!raw) return null;
|
|
865
|
+
const s = raw.trim().toLowerCase().replace(/^area:/, "");
|
|
866
|
+
return s || null;
|
|
867
|
+
}
|
|
868
|
+
function bump(m, k, by = 1) {
|
|
869
|
+
m.set(k, (m.get(k) ?? 0) + by);
|
|
870
|
+
}
|
|
871
|
+
function topKey(m) {
|
|
872
|
+
let best = null;
|
|
873
|
+
let bestN = 0;
|
|
874
|
+
let total = 0;
|
|
875
|
+
for (const [k, n] of m) {
|
|
876
|
+
total += n;
|
|
877
|
+
if (n > bestN) {
|
|
878
|
+
best = k;
|
|
879
|
+
bestN = n;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return best === null || total === 0 ? null : { key: best, count: bestN, share: bestN / total };
|
|
883
|
+
}
|
|
884
|
+
function buildZoneModel(entries, opts = {}) {
|
|
885
|
+
const o = { ...DEFAULTS, ...opts };
|
|
886
|
+
const tasks = entries.filter((e) => e.files.length > 0);
|
|
887
|
+
if (tasks.length < o.minTasks) return EMPTY_ZONE_MODEL;
|
|
888
|
+
const fileTasks = /* @__PURE__ */ new Map();
|
|
889
|
+
const candidate = /* @__PURE__ */ new Map();
|
|
890
|
+
for (const t of tasks) {
|
|
891
|
+
for (const f of new Set(t.files)) {
|
|
892
|
+
bump(fileTasks, f);
|
|
893
|
+
if (!candidate.has(f)) candidate.set(f, pathZoneCandidate(f));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const zoneOf = new Map(candidate);
|
|
897
|
+
for (const [file, seen] of fileTasks) {
|
|
898
|
+
if (seen < 2) continue;
|
|
899
|
+
const votes = /* @__PURE__ */ new Map();
|
|
900
|
+
for (const t of tasks) {
|
|
901
|
+
const files = [...new Set(t.files)];
|
|
902
|
+
if (!files.includes(file)) continue;
|
|
903
|
+
const others = files.filter((f) => f !== file);
|
|
904
|
+
if (!others.length) continue;
|
|
905
|
+
for (const f of others) bump(votes, candidate.get(f), 1 / others.length);
|
|
906
|
+
}
|
|
907
|
+
const top = topKey(votes);
|
|
908
|
+
if (top && top.share >= 0.6 && top.key !== candidate.get(file)) zoneOf.set(file, top.key);
|
|
909
|
+
}
|
|
910
|
+
const voteWeight = (file) => 1 / Math.log2(2 + (fileTasks.get(file) ?? 1));
|
|
911
|
+
const taskZonePre = /* @__PURE__ */ new Map();
|
|
912
|
+
for (const t of tasks) {
|
|
913
|
+
const votes = /* @__PURE__ */ new Map();
|
|
914
|
+
for (const f of new Set(t.files)) bump(votes, zoneOf.get(f), voteWeight(f));
|
|
915
|
+
const top = topKey(votes);
|
|
916
|
+
if (top) taskZonePre.set(t.slug, top.key);
|
|
917
|
+
}
|
|
918
|
+
const fileZones = /* @__PURE__ */ new Map();
|
|
919
|
+
for (const t of tasks) {
|
|
920
|
+
const tz = taskZonePre.get(t.slug);
|
|
921
|
+
if (!tz) continue;
|
|
922
|
+
for (const f of new Set(t.files)) {
|
|
923
|
+
let m = fileZones.get(f);
|
|
924
|
+
if (!m) fileZones.set(f, m = /* @__PURE__ */ new Map());
|
|
925
|
+
bump(m, tz);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const spineSet = /* @__PURE__ */ new Set();
|
|
929
|
+
for (const [file, seen] of fileTasks) {
|
|
930
|
+
if (seen < o.spineMinTasks) continue;
|
|
931
|
+
const zonesOfFile = fileZones.get(file);
|
|
932
|
+
const dominant = zonesOfFile ? topKey(zonesOfFile) : null;
|
|
933
|
+
if ((zonesOfFile?.size ?? 0) < o.spineMinZones) continue;
|
|
934
|
+
if (dominant && dominant.share >= o.spineDominantShare) continue;
|
|
935
|
+
spineSet.add(file);
|
|
936
|
+
}
|
|
937
|
+
const fileZone = /* @__PURE__ */ new Map();
|
|
938
|
+
for (const [f, z2] of zoneOf) if (!spineSet.has(f)) fileZone.set(f, z2);
|
|
939
|
+
const taskZone = /* @__PURE__ */ new Map();
|
|
940
|
+
for (const t of tasks) {
|
|
941
|
+
const votes = /* @__PURE__ */ new Map();
|
|
942
|
+
for (const f of new Set(t.files)) {
|
|
943
|
+
const z2 = fileZone.get(f);
|
|
944
|
+
if (z2) bump(votes, z2, voteWeight(f));
|
|
945
|
+
}
|
|
946
|
+
const top = topKey(votes);
|
|
947
|
+
taskZone.set(t.slug, top ? top.key : SPINE_ZONE);
|
|
948
|
+
}
|
|
949
|
+
const labelVotes = /* @__PURE__ */ new Map();
|
|
950
|
+
for (const t of tasks) {
|
|
951
|
+
const manual = normalizeZoneName(t.area);
|
|
952
|
+
const z2 = taskZone.get(t.slug);
|
|
953
|
+
if (!manual || !z2 || z2 === SPINE_ZONE || manual === SPINE_ZONE) continue;
|
|
954
|
+
let m = labelVotes.get(z2);
|
|
955
|
+
if (!m) labelVotes.set(z2, m = /* @__PURE__ */ new Map());
|
|
956
|
+
bump(m, manual);
|
|
957
|
+
}
|
|
958
|
+
const rename = /* @__PURE__ */ new Map();
|
|
959
|
+
const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
|
|
960
|
+
const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
|
|
961
|
+
const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
|
|
962
|
+
return sum(b[1]) - sum(a[1]) || a[0].localeCompare(b[0]);
|
|
963
|
+
});
|
|
964
|
+
for (const [zone, votes] of clustersByEvidence) {
|
|
965
|
+
const top = topKey(votes);
|
|
966
|
+
if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
|
|
967
|
+
rename.set(zone, top.key);
|
|
968
|
+
takenNames.add(top.key);
|
|
969
|
+
}
|
|
970
|
+
const renamed = (z2) => rename.get(z2) ?? z2;
|
|
971
|
+
if (rename.size) {
|
|
972
|
+
for (const [f, z2] of fileZone) fileZone.set(f, renamed(z2));
|
|
973
|
+
for (const [s, z2] of taskZone) taskZone.set(s, renamed(z2));
|
|
974
|
+
}
|
|
975
|
+
const zoneTerms = /* @__PURE__ */ new Map();
|
|
976
|
+
const addTerm = (zone, term, w) => {
|
|
977
|
+
let m = zoneTerms.get(zone);
|
|
978
|
+
if (!m) zoneTerms.set(zone, m = /* @__PURE__ */ new Map());
|
|
979
|
+
bump(m, term, w);
|
|
980
|
+
};
|
|
981
|
+
for (const [f, z2] of fileZone) for (const term of tokenize(f)) addTerm(z2, term, 1);
|
|
982
|
+
for (const f of spineSet) for (const term of tokenize(f)) addTerm(SPINE_ZONE, term, 1);
|
|
983
|
+
for (const t of tasks) {
|
|
984
|
+
const z2 = taskZone.get(t.slug);
|
|
985
|
+
if (!z2 || !t.text) continue;
|
|
986
|
+
for (const term of tokenize(t.text)) addTerm(z2, term, 2);
|
|
987
|
+
}
|
|
988
|
+
const termZones = /* @__PURE__ */ new Map();
|
|
989
|
+
for (const terms of zoneTerms.values()) for (const term of terms.keys()) bump(termZones, term);
|
|
990
|
+
const zoneFiles = /* @__PURE__ */ new Map();
|
|
991
|
+
for (const [f, z2] of fileZone) {
|
|
992
|
+
const arr = zoneFiles.get(z2) ?? [];
|
|
993
|
+
arr.push(f);
|
|
994
|
+
zoneFiles.set(z2, arr);
|
|
995
|
+
}
|
|
996
|
+
const zoneTaskCount = /* @__PURE__ */ new Map();
|
|
997
|
+
for (const z2 of taskZone.values()) bump(zoneTaskCount, z2);
|
|
998
|
+
const byFreq = (a, b) => (fileTasks.get(b) ?? 0) - (fileTasks.get(a) ?? 0) || a.localeCompare(b);
|
|
999
|
+
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));
|
|
1000
|
+
const spine = [...spineSet].sort(byFreq);
|
|
1001
|
+
if (spineSet.size) zones.push({ zone: SPINE_ZONE, tasks: zoneTaskCount.get(SPINE_ZONE) ?? 0, files: spine });
|
|
1002
|
+
const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
|
|
1003
|
+
return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
|
|
1004
|
+
}
|
|
1005
|
+
function zoneForFiles(model, files) {
|
|
1006
|
+
if (!hasZoneModel(model) || !files.length) return null;
|
|
1007
|
+
const spineSet = new Set(model.spine);
|
|
1008
|
+
const touchedSpine = files.filter((f) => spineSet.has(f));
|
|
1009
|
+
const votes = /* @__PURE__ */ new Map();
|
|
1010
|
+
for (const f of new Set(files)) {
|
|
1011
|
+
const z2 = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
|
|
1012
|
+
if (z2) bump(votes, z2);
|
|
1013
|
+
}
|
|
1014
|
+
const top = topKey(votes);
|
|
1015
|
+
if (!top) {
|
|
1016
|
+
return {
|
|
1017
|
+
zone: SPINE_ZONE,
|
|
1018
|
+
confidence: 1,
|
|
1019
|
+
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(", ")})`,
|
|
1020
|
+
spine: true
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
zone: top.key,
|
|
1025
|
+
confidence: top.share,
|
|
1026
|
+
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}`,
|
|
1027
|
+
spine: touchedSpine.length > 0
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function suggestZonesFromText(model, text, limit = 3) {
|
|
1031
|
+
if (!hasZoneModel(model)) return [];
|
|
1032
|
+
const terms = tokenize(text);
|
|
1033
|
+
if (!terms.length) return [];
|
|
1034
|
+
const zoneCount = model.zoneTerms.size || 1;
|
|
1035
|
+
const idf = (term) => Math.log(1 + zoneCount / (model.termZones.get(term) ?? 1));
|
|
1036
|
+
const scores = /* @__PURE__ */ new Map();
|
|
1037
|
+
const hits = /* @__PURE__ */ new Map();
|
|
1038
|
+
const tasksOfZone = new Map(model.zones.map((z2) => [z2.zone, z2.tasks]));
|
|
1039
|
+
for (const [zone, dict] of model.zoneTerms) {
|
|
1040
|
+
if ((tasksOfZone.get(zone) ?? 0) < 2) continue;
|
|
1041
|
+
let dot = 0;
|
|
1042
|
+
let norm = 0;
|
|
1043
|
+
const matched = [];
|
|
1044
|
+
for (const [term, w] of dict) {
|
|
1045
|
+
const v = Math.log(1 + w) * idf(term);
|
|
1046
|
+
norm += v * v;
|
|
1047
|
+
}
|
|
1048
|
+
if (!norm) continue;
|
|
1049
|
+
for (const term of new Set(terms)) {
|
|
1050
|
+
const w = dict.get(term);
|
|
1051
|
+
if (!w) continue;
|
|
1052
|
+
dot += Math.log(1 + w) * idf(term) * idf(term);
|
|
1053
|
+
matched.push(term);
|
|
1054
|
+
}
|
|
1055
|
+
const score = dot / Math.sqrt(norm) * Math.log(1 + (tasksOfZone.get(zone) ?? 1));
|
|
1056
|
+
if (score > 0) {
|
|
1057
|
+
scores.set(zone, score);
|
|
1058
|
+
hits.set(zone, matched);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const total = [...scores.values()].reduce((a, b) => a + b, 0);
|
|
1062
|
+
if (!total) return [];
|
|
1063
|
+
return [...scores.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([zone, s]) => ({
|
|
1064
|
+
zone,
|
|
1065
|
+
confidence: s / total,
|
|
1066
|
+
why: `\u0441\u043E\u0432\u043F\u0430\u043B\u043E \u043F\u043E \u0441\u043B\u043E\u0432\u0430\u043C: ${(hits.get(zone) ?? []).slice(0, 5).join(", ")}`,
|
|
1067
|
+
spine: zone === SPINE_ZONE
|
|
1068
|
+
}));
|
|
1069
|
+
}
|
|
1070
|
+
function similarTasks(model, text, limit = 5) {
|
|
1071
|
+
const terms = new Set(tokenize(text));
|
|
1072
|
+
if (!terms.size) return [];
|
|
1073
|
+
const scored = [];
|
|
1074
|
+
for (const [i, e] of model.entries.entries()) {
|
|
1075
|
+
const other = model.entryTerms[i];
|
|
1076
|
+
if (!other?.size) continue;
|
|
1077
|
+
let inter = 0;
|
|
1078
|
+
for (const t of terms) if (other.has(t)) inter += Math.log(1 + (model.zoneTerms.size || 1) / (model.termZones.get(t) ?? 1));
|
|
1079
|
+
if (inter <= 0) continue;
|
|
1080
|
+
scored.push({ entry: e, score: inter / Math.sqrt(terms.size * other.size) });
|
|
1081
|
+
}
|
|
1082
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1083
|
+
}
|
|
1084
|
+
function predictFiles(model, opts) {
|
|
1085
|
+
if (!hasZoneModel(model)) return [];
|
|
1086
|
+
const limit = opts.limit ?? 40;
|
|
1087
|
+
const best = /* @__PURE__ */ new Map();
|
|
1088
|
+
const put = (path, weight, why) => {
|
|
1089
|
+
const cur = best.get(path);
|
|
1090
|
+
if (!cur || cur.weight < weight) best.set(path, { path, weight, why });
|
|
1091
|
+
};
|
|
1092
|
+
if (opts.text) {
|
|
1093
|
+
const sim = similarTasks(model, opts.text, opts.similar ?? 5);
|
|
1094
|
+
const topScore = sim[0]?.score ?? 0;
|
|
1095
|
+
for (const { entry, score } of sim) {
|
|
1096
|
+
const w = topScore > 0 ? Math.min(1, score / topScore) : 0;
|
|
1097
|
+
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}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
const zone = normalizeZoneName(opts.zone) ?? (opts.text ? suggestZonesFromText(model, opts.text)[0]?.zone ?? null : null);
|
|
1101
|
+
if (zone) {
|
|
1102
|
+
const stat = model.zones.find((z2) => z2.zone === zone);
|
|
1103
|
+
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}`));
|
|
1104
|
+
}
|
|
1105
|
+
return [...best.values()].sort((a, b) => b.weight - a.weight || a.path.localeCompare(b.path)).slice(0, limit);
|
|
1106
|
+
}
|
|
1107
|
+
function overlapRisk(predicted, actual, opts = {}) {
|
|
1108
|
+
const o = { ...OVERLAP_DEFAULTS, ...opts };
|
|
1109
|
+
const actualSet = new Set(actual);
|
|
1110
|
+
const actualDirs = new Set([...actualSet].map(dirOf).filter(Boolean));
|
|
1111
|
+
const totalWeight = predicted.reduce((a, p) => a + p.weight, 0);
|
|
1112
|
+
if (!totalWeight) return { overlap: false, score: 0, sharedFiles: [], sharedDirs: [] };
|
|
1113
|
+
let hit = 0;
|
|
1114
|
+
let strong = false;
|
|
1115
|
+
const sharedFiles = [];
|
|
1116
|
+
const sharedDirs = /* @__PURE__ */ new Set();
|
|
1117
|
+
for (const p of predicted) {
|
|
1118
|
+
if (actualSet.has(p.path)) {
|
|
1119
|
+
hit += p.weight;
|
|
1120
|
+
sharedFiles.push(p.path);
|
|
1121
|
+
if (p.weight >= o.strongFile) strong = true;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const d = dirOf(p.path);
|
|
1125
|
+
if (d && actualDirs.has(d)) {
|
|
1126
|
+
hit += p.weight * 0.25;
|
|
1127
|
+
sharedDirs.add(d);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const score = Math.min(1, hit / totalWeight);
|
|
1131
|
+
return {
|
|
1132
|
+
overlap: strong || score >= o.threshold,
|
|
1133
|
+
score,
|
|
1134
|
+
sharedFiles: sharedFiles.slice(0, 10),
|
|
1135
|
+
sharedDirs: [...sharedDirs].slice(0, 5)
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS, OVERLAP_DEFAULTS;
|
|
1139
|
+
var init_codeZones = __esm({
|
|
1140
|
+
"../swarm-core/src/codeZones.ts"() {
|
|
1141
|
+
"use strict";
|
|
1142
|
+
SPINE_ZONE = "core";
|
|
1143
|
+
DEFAULTS = {
|
|
1144
|
+
minTasks: 5,
|
|
1145
|
+
spineMinTasks: 3,
|
|
1146
|
+
spineMinZones: 3,
|
|
1147
|
+
spineDominantShare: 0.6
|
|
1148
|
+
};
|
|
1149
|
+
EMPTY_ZONE_MODEL = {
|
|
1150
|
+
tasks: 0,
|
|
1151
|
+
zones: [],
|
|
1152
|
+
fileZone: /* @__PURE__ */ new Map(),
|
|
1153
|
+
spine: [],
|
|
1154
|
+
taskZone: /* @__PURE__ */ new Map(),
|
|
1155
|
+
zoneTerms: /* @__PURE__ */ new Map(),
|
|
1156
|
+
termZones: /* @__PURE__ */ new Map(),
|
|
1157
|
+
entries: [],
|
|
1158
|
+
entryTerms: []
|
|
1159
|
+
};
|
|
1160
|
+
GENERIC_DIRS = /* @__PURE__ */ new Set([
|
|
1161
|
+
"src",
|
|
1162
|
+
"source",
|
|
1163
|
+
"lib",
|
|
1164
|
+
"libs",
|
|
1165
|
+
"util",
|
|
1166
|
+
"utils",
|
|
1167
|
+
"common",
|
|
1168
|
+
"internal",
|
|
1169
|
+
"app",
|
|
1170
|
+
"test",
|
|
1171
|
+
"tests",
|
|
1172
|
+
"__tests__",
|
|
1173
|
+
"spec",
|
|
1174
|
+
"specs",
|
|
1175
|
+
"index"
|
|
1176
|
+
]);
|
|
1177
|
+
WORKSPACE_DIRS = /* @__PURE__ */ new Set(["packages", "apps", "services", "libs", "modules"]);
|
|
1178
|
+
GENERIC_TEST_DIRS = /* @__PURE__ */ new Set(["tests", "test", "__tests__", "spec"]);
|
|
1179
|
+
STOP_TERMS = /* @__PURE__ */ new Set([
|
|
1180
|
+
"the",
|
|
1181
|
+
"and",
|
|
1182
|
+
"for",
|
|
1183
|
+
"with",
|
|
1184
|
+
"from",
|
|
1185
|
+
"into",
|
|
1186
|
+
"src",
|
|
1187
|
+
"test",
|
|
1188
|
+
"tests",
|
|
1189
|
+
"index",
|
|
1190
|
+
"packages",
|
|
1191
|
+
"package",
|
|
1192
|
+
"json",
|
|
1193
|
+
"yml",
|
|
1194
|
+
"yaml",
|
|
1195
|
+
"tsx",
|
|
1196
|
+
"mjs",
|
|
1197
|
+
"cjs",
|
|
1198
|
+
"\u0447\u0442\u043E",
|
|
1199
|
+
"\u043A\u0430\u043A",
|
|
1200
|
+
"\u0434\u043B\u044F",
|
|
1201
|
+
"\u043F\u0440\u0438",
|
|
1202
|
+
"\u044D\u0442\u043E",
|
|
1203
|
+
"\u0438\u043B\u0438",
|
|
1204
|
+
"\u0435\u0441\u043B\u0438",
|
|
1205
|
+
"\u0447\u0442\u043E\u0431\u044B",
|
|
1206
|
+
"\u0442\u043E\u0436\u0435",
|
|
1207
|
+
"\u0435\u0433\u043E",
|
|
1208
|
+
"\u0432\u0441\u0435",
|
|
1209
|
+
"feat",
|
|
1210
|
+
"fix",
|
|
1211
|
+
"chore",
|
|
1212
|
+
"docs",
|
|
1213
|
+
"refactor",
|
|
1214
|
+
"\u043F\u0435\u0440\u0444",
|
|
1215
|
+
"wip"
|
|
1216
|
+
]);
|
|
1217
|
+
OVERLAP_DEFAULTS = { threshold: 0.2, strongFile: 0.35 };
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
777
1221
|
// ../swarm-core/src/index.ts
|
|
778
1222
|
var init_src = __esm({
|
|
779
1223
|
"../swarm-core/src/index.ts"() {
|
|
@@ -790,8 +1234,483 @@ var init_src = __esm({
|
|
|
790
1234
|
init_names();
|
|
791
1235
|
init_baseBranch();
|
|
792
1236
|
init_hosts();
|
|
1237
|
+
init_hostPool();
|
|
793
1238
|
init_hostCommands();
|
|
794
1239
|
init_relief();
|
|
1240
|
+
init_cost();
|
|
1241
|
+
init_runGuards();
|
|
1242
|
+
init_codeZones();
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
// src/claudeBin.ts
|
|
1247
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
1248
|
+
import { win32 as winPath } from "path";
|
|
1249
|
+
function compareVersionDirs(a, b) {
|
|
1250
|
+
const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
|
|
1251
|
+
const pa = parts(a);
|
|
1252
|
+
const pb = parts(b);
|
|
1253
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1254
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1255
|
+
if (d !== 0) return d;
|
|
1256
|
+
}
|
|
1257
|
+
return 0;
|
|
1258
|
+
}
|
|
1259
|
+
function realListDir(dir) {
|
|
1260
|
+
try {
|
|
1261
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1262
|
+
} catch {
|
|
1263
|
+
return [];
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function winVersionRoots(env, listDir) {
|
|
1267
|
+
const roots = [];
|
|
1268
|
+
const appData = env.APPDATA;
|
|
1269
|
+
if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
|
|
1270
|
+
const localAppData = env.LOCALAPPDATA;
|
|
1271
|
+
if (localAppData) {
|
|
1272
|
+
const packages = winPath.join(localAppData, "Packages");
|
|
1273
|
+
for (const name of listDir(packages)) {
|
|
1274
|
+
if (!name.startsWith("Claude")) continue;
|
|
1275
|
+
roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return roots;
|
|
1279
|
+
}
|
|
1280
|
+
function findWindowsClaude(opts = {}) {
|
|
1281
|
+
const env = opts.env ?? process.env;
|
|
1282
|
+
const listDir = opts.listDir ?? realListDir;
|
|
1283
|
+
const fileExists = opts.fileExists ?? existsSync2;
|
|
1284
|
+
let best = null;
|
|
1285
|
+
for (const root of winVersionRoots(env, listDir)) {
|
|
1286
|
+
for (const version of listDir(root)) {
|
|
1287
|
+
const candidate = winPath.join(root, version, "claude.exe");
|
|
1288
|
+
if (!fileExists(candidate)) continue;
|
|
1289
|
+
if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return best?.path ?? null;
|
|
1293
|
+
}
|
|
1294
|
+
function resolveClaudeBin(opts = {}) {
|
|
1295
|
+
const env = opts.env ?? process.env;
|
|
1296
|
+
const platform = opts.platform ?? process.platform;
|
|
1297
|
+
const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
|
|
1298
|
+
if (explicit) return explicit;
|
|
1299
|
+
if (platform !== "win32") return CLAUDE_FALLBACK;
|
|
1300
|
+
if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
|
|
1301
|
+
windowsScanDone = true;
|
|
1302
|
+
const found = findWindowsClaude({ ...opts, env });
|
|
1303
|
+
cachedWindowsBin = found;
|
|
1304
|
+
if (found) return found;
|
|
1305
|
+
if (!warnedNoWindowsClaude) {
|
|
1306
|
+
warnedNoWindowsClaude = true;
|
|
1307
|
+
console.warn(
|
|
1308
|
+
"[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."
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
return CLAUDE_FALLBACK;
|
|
1312
|
+
}
|
|
1313
|
+
var CLAUDE_FALLBACK, warnedNoWindowsClaude, cachedWindowsBin, windowsScanDone;
|
|
1314
|
+
var init_claudeBin = __esm({
|
|
1315
|
+
"src/claudeBin.ts"() {
|
|
1316
|
+
"use strict";
|
|
1317
|
+
CLAUDE_FALLBACK = "claude";
|
|
1318
|
+
warnedNoWindowsClaude = false;
|
|
1319
|
+
cachedWindowsBin = null;
|
|
1320
|
+
windowsScanDone = false;
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
// src/pricing.ts
|
|
1325
|
+
function costFromUsage(usage, model, table = MODEL_PRICING) {
|
|
1326
|
+
const pricing = model ? table[model] : void 0;
|
|
1327
|
+
if (!pricing) {
|
|
1328
|
+
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` };
|
|
1329
|
+
}
|
|
1330
|
+
const input = usage.input_tokens ?? 0;
|
|
1331
|
+
const output = usage.output_tokens ?? 0;
|
|
1332
|
+
const cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
1333
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
1334
|
+
const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
|
|
1335
|
+
return { costUsd };
|
|
1336
|
+
}
|
|
1337
|
+
var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
|
|
1338
|
+
var init_pricing = __esm({
|
|
1339
|
+
"src/pricing.ts"() {
|
|
1340
|
+
"use strict";
|
|
1341
|
+
MODEL_PRICING = {
|
|
1342
|
+
// Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
|
|
1343
|
+
// Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
|
|
1344
|
+
// ветвление по времени внутри расчёта стоимости.
|
|
1345
|
+
"claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
|
|
1346
|
+
"claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1347
|
+
// Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
|
|
1348
|
+
"claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1349
|
+
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
|
|
1350
|
+
};
|
|
1351
|
+
CACHE_READ_FACTOR = 0.1;
|
|
1352
|
+
CACHE_WRITE_5M_FACTOR = 1.25;
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
// src/claudeResult.ts
|
|
1357
|
+
function parseClaudeResultJson(raw) {
|
|
1358
|
+
const trimmed = raw.trim();
|
|
1359
|
+
if (!trimmed) return null;
|
|
1360
|
+
try {
|
|
1361
|
+
const parsed = JSON.parse(trimmed);
|
|
1362
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
1363
|
+
} catch {
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function launchCostUsd(result, model) {
|
|
1368
|
+
if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
|
|
1369
|
+
return { costUsd: result.total_cost_usd };
|
|
1370
|
+
}
|
|
1371
|
+
if (!result.usage) return { costUsd: 0 };
|
|
1372
|
+
return costFromUsage(result.usage, model);
|
|
1373
|
+
}
|
|
1374
|
+
function launchSpend(result) {
|
|
1375
|
+
const u = result.usage ?? {};
|
|
1376
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
1377
|
+
return {
|
|
1378
|
+
tokensIn: num(u.input_tokens) + num(u.cache_creation_input_tokens) + num(u.cache_read_input_tokens),
|
|
1379
|
+
tokensOut: num(u.output_tokens),
|
|
1380
|
+
durationMs: num(result.duration_ms)
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
var init_claudeResult = __esm({
|
|
1384
|
+
"src/claudeResult.ts"() {
|
|
1385
|
+
"use strict";
|
|
1386
|
+
init_pricing();
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
// src/executors/claudeCode.ts
|
|
1391
|
+
import { readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1392
|
+
import { homedir } from "os";
|
|
1393
|
+
import { join as join2 } from "path";
|
|
1394
|
+
function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
|
|
1395
|
+
const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
|
|
1396
|
+
const base3 = [
|
|
1397
|
+
"--dangerously-skip-permissions",
|
|
1398
|
+
...ISOLATION_FLAGS,
|
|
1399
|
+
"--mcp-config",
|
|
1400
|
+
cfgPath,
|
|
1401
|
+
...settings ? ["--settings", settings] : [],
|
|
1402
|
+
...pluginFlags,
|
|
1403
|
+
"--output-format",
|
|
1404
|
+
"json",
|
|
1405
|
+
"-p",
|
|
1406
|
+
prompt
|
|
1407
|
+
];
|
|
1408
|
+
const withModel = model ? ["--model", model, ...base3] : base3;
|
|
1409
|
+
return continueSession ? ["--continue", ...withModel] : withModel;
|
|
1410
|
+
}
|
|
1411
|
+
function droneDenyRules(paths) {
|
|
1412
|
+
const abs = (p) => `//${p.replace(/^\/+/, "")}`;
|
|
1413
|
+
return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
|
|
1414
|
+
}
|
|
1415
|
+
function droneSettingsJson(paths) {
|
|
1416
|
+
return JSON.stringify({ permissions: { deny: droneDenyRules(paths) } });
|
|
1417
|
+
}
|
|
1418
|
+
function transcriptDir(worktree, projectsDir) {
|
|
1419
|
+
return join2(projectsDir, worktree.replace(/[/.]/g, "-"));
|
|
1420
|
+
}
|
|
1421
|
+
function sessionTranscriptExists(worktree, projectsDir = join2(homedir(), ".claude", "projects")) {
|
|
1422
|
+
try {
|
|
1423
|
+
return readdirSync2(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
|
|
1424
|
+
} catch {
|
|
1425
|
+
return false;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
function transcriptMtimeMs(worktree, projectsDir = join2(homedir(), ".claude", "projects")) {
|
|
1429
|
+
const dir = transcriptDir(worktree, projectsDir);
|
|
1430
|
+
let files;
|
|
1431
|
+
try {
|
|
1432
|
+
files = readdirSync2(dir);
|
|
1433
|
+
} catch {
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
let latest = null;
|
|
1437
|
+
for (const f of files) {
|
|
1438
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
1439
|
+
try {
|
|
1440
|
+
const mtime2 = statSync(join2(dir, f)).mtimeMs;
|
|
1441
|
+
if (latest === null || mtime2 > latest) latest = mtime2;
|
|
1442
|
+
} catch {
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return latest;
|
|
1446
|
+
}
|
|
1447
|
+
function writeMcpConfig(spec) {
|
|
1448
|
+
const cfg = {
|
|
1449
|
+
mcpServers: {
|
|
1450
|
+
[spec.mcp.name]: { command: spec.mcp.command, args: spec.mcp.args, env: spec.mcp.env }
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
const path = join2(spec.worktree, MCP_CONFIG_FILE);
|
|
1454
|
+
writeFileSync2(path, JSON.stringify(cfg, null, 2));
|
|
1455
|
+
return path;
|
|
1456
|
+
}
|
|
1457
|
+
var ISOLATION_FLAGS, MCP_CONFIG_FILE, claudeCodeExecutor;
|
|
1458
|
+
var init_claudeCode = __esm({
|
|
1459
|
+
"src/executors/claudeCode.ts"() {
|
|
1460
|
+
"use strict";
|
|
1461
|
+
init_claudeBin();
|
|
1462
|
+
init_claudeResult();
|
|
1463
|
+
ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
|
|
1464
|
+
MCP_CONFIG_FILE = ".swarm-mcp.json";
|
|
1465
|
+
claudeCodeExecutor = {
|
|
1466
|
+
id: "claude-code",
|
|
1467
|
+
title: "Claude Code (claude)",
|
|
1468
|
+
binEnv: "SWARM_CLAUDE_BIN",
|
|
1469
|
+
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" },
|
|
1470
|
+
resolveBin: () => resolveClaudeBin(),
|
|
1471
|
+
// Стрелка, а не метод: `plan` могут вызвать в отрыве от объекта (деструктуризация в тестах,
|
|
1472
|
+
// проброс колбэком), и `this` тогда уедет в undefined.
|
|
1473
|
+
plan: (spec) => {
|
|
1474
|
+
const cfgPath = writeMcpConfig(spec);
|
|
1475
|
+
const settings = spec.denyPaths.length > 0 ? droneSettingsJson(spec.denyPaths) : void 0;
|
|
1476
|
+
return {
|
|
1477
|
+
bin: resolveClaudeBin(),
|
|
1478
|
+
args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings)
|
|
1479
|
+
};
|
|
1480
|
+
},
|
|
1481
|
+
parseResult(raw, model) {
|
|
1482
|
+
const result = parseClaudeResultJson(raw);
|
|
1483
|
+
if (!result) return null;
|
|
1484
|
+
const est = launchCostUsd(result, model);
|
|
1485
|
+
const spend = launchSpend(result);
|
|
1486
|
+
return {
|
|
1487
|
+
costUsd: est.costUsd,
|
|
1488
|
+
...est.warning ? { warning: est.warning } : {},
|
|
1489
|
+
tokensIn: spend.tokensIn,
|
|
1490
|
+
tokensOut: spend.tokensOut,
|
|
1491
|
+
durationMs: spend.durationMs
|
|
1492
|
+
};
|
|
1493
|
+
},
|
|
1494
|
+
session: {
|
|
1495
|
+
exists: (worktree) => sessionTranscriptExists(worktree),
|
|
1496
|
+
mtimeMs: (worktree) => transcriptMtimeMs(worktree)
|
|
1497
|
+
},
|
|
1498
|
+
// `claude -p "ok" --max-turns 1` (TASWM-23) — прямее, чем HEAD на статус-хост: дёргает ТОТ
|
|
1499
|
+
// ЖЕ путь (auth + реальный ответ модели), что и дрон. ISOLATION_FLAGS те же, что у дрона
|
|
1500
|
+
// (TASWM-90): SessionStart-хуки юзера могли бы подвесить пробу и дать ложный шторм.
|
|
1501
|
+
probe: {
|
|
1502
|
+
args: [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"],
|
|
1503
|
+
// TASWM-214: исчерпанный лимит подписки — НЕ шторм провайдера, и путать их нельзя.
|
|
1504
|
+
limitPattern: /(weekly limit|usage limit|hit your .*limit)/i
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
// src/swarmHome.ts
|
|
1511
|
+
import { homedir as homedir2 } from "os";
|
|
1512
|
+
import { join as join3 } from "path";
|
|
1513
|
+
import { mkdirSync } from "fs";
|
|
1514
|
+
var SWARM_HOME;
|
|
1515
|
+
var init_swarmHome = __esm({
|
|
1516
|
+
"src/swarmHome.ts"() {
|
|
1517
|
+
"use strict";
|
|
1518
|
+
SWARM_HOME = process.env.SWARM_HOME || join3(homedir2(), ".taskless-swarm");
|
|
1519
|
+
mkdirSync(SWARM_HOME, { recursive: true });
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
// src/executors/codex.ts
|
|
1524
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync3, statSync as statSync2, symlinkSync } from "fs";
|
|
1525
|
+
import { join as join4 } from "path";
|
|
1526
|
+
function resolveCodexBin(env = process.env) {
|
|
1527
|
+
return (env.SWARM_CODEX_BIN ?? "").trim() || "codex";
|
|
1528
|
+
}
|
|
1529
|
+
function codexHomePath(worktree, swarmHome = SWARM_HOME) {
|
|
1530
|
+
return join4(swarmHome, "executor-state", "codex", worktree.replace(/[/.]/g, "-"));
|
|
1531
|
+
}
|
|
1532
|
+
function ensureCodexHome(worktree, realHome, env = process.env, swarmHome = SWARM_HOME) {
|
|
1533
|
+
const home = codexHomePath(worktree, swarmHome);
|
|
1534
|
+
mkdirSync2(home, { recursive: true });
|
|
1535
|
+
const real = (env.CODEX_HOME ?? "").trim() || join4(realHome, ".codex");
|
|
1536
|
+
for (const file of ["auth.json", "config.toml"]) {
|
|
1537
|
+
const target = join4(real, file);
|
|
1538
|
+
const link = join4(home, file);
|
|
1539
|
+
if (!existsSync3(target) || existsSync3(link)) continue;
|
|
1540
|
+
try {
|
|
1541
|
+
symlinkSync(target, link);
|
|
1542
|
+
} catch {
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return home;
|
|
1546
|
+
}
|
|
1547
|
+
function tomlValue(v) {
|
|
1548
|
+
if (typeof v === "string") {
|
|
1549
|
+
const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
1550
|
+
return `"${escaped}"`;
|
|
1551
|
+
}
|
|
1552
|
+
if (Array.isArray(v)) return `[${v.map((x) => tomlValue(x)).join(", ")}]`;
|
|
1553
|
+
return `{${Object.entries(v).map(([k, val]) => `${k} = ${tomlValue(val)}`).join(", ")}}`;
|
|
1554
|
+
}
|
|
1555
|
+
function mcpConfigArgs(spec) {
|
|
1556
|
+
const inline = `{command = ${tomlValue(spec.mcp.command)}, args = ${tomlValue(spec.mcp.args)}, env = ${tomlValue(spec.mcp.env)}}`;
|
|
1557
|
+
return ["-c", `mcp_servers.${spec.mcp.name}=${inline}`];
|
|
1558
|
+
}
|
|
1559
|
+
function skillsNotice(skillDirs) {
|
|
1560
|
+
if (skillDirs.length === 0) return "";
|
|
1561
|
+
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.";
|
|
1562
|
+
}
|
|
1563
|
+
function readUsage(node) {
|
|
1564
|
+
if (typeof node !== "object" || node === null) return null;
|
|
1565
|
+
const rec = node;
|
|
1566
|
+
const direct = rec.total_token_usage ?? rec.usage ?? rec.token_usage;
|
|
1567
|
+
if (direct && typeof direct === "object") return direct;
|
|
1568
|
+
const info = rec.info;
|
|
1569
|
+
if (info && typeof info === "object") return readUsage(info);
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
function parseCodexStream(raw) {
|
|
1573
|
+
let usage = null;
|
|
1574
|
+
let completed = false;
|
|
1575
|
+
for (const line of raw.split("\n")) {
|
|
1576
|
+
const trimmed = line.trim();
|
|
1577
|
+
if (!trimmed) continue;
|
|
1578
|
+
let parsed;
|
|
1579
|
+
try {
|
|
1580
|
+
parsed = JSON.parse(trimmed);
|
|
1581
|
+
} catch {
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
if (typeof parsed !== "object" || parsed === null) continue;
|
|
1585
|
+
const rec = parsed;
|
|
1586
|
+
const msg = typeof rec.msg === "object" && rec.msg !== null ? rec.msg : rec;
|
|
1587
|
+
const type = typeof msg.type === "string" ? msg.type : null;
|
|
1588
|
+
const found = readUsage(msg);
|
|
1589
|
+
if (found) usage = found;
|
|
1590
|
+
if (type && TERMINAL_EVENTS.has(type)) completed = true;
|
|
1591
|
+
}
|
|
1592
|
+
return { usage, completed };
|
|
1593
|
+
}
|
|
1594
|
+
function newestRolloutMtime(dir, depth = 4) {
|
|
1595
|
+
let entries;
|
|
1596
|
+
try {
|
|
1597
|
+
entries = readdirSync3(dir);
|
|
1598
|
+
} catch {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
let latest = null;
|
|
1602
|
+
for (const name of entries) {
|
|
1603
|
+
const full = join4(dir, name);
|
|
1604
|
+
let st;
|
|
1605
|
+
try {
|
|
1606
|
+
st = statSync2(full);
|
|
1607
|
+
} catch {
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (st.isDirectory()) {
|
|
1611
|
+
if (depth <= 0) continue;
|
|
1612
|
+
const nested = newestRolloutMtime(full, depth - 1);
|
|
1613
|
+
if (nested !== null && (latest === null || nested > latest)) latest = nested;
|
|
1614
|
+
} else if (name.endsWith(".jsonl")) {
|
|
1615
|
+
if (latest === null || st.mtimeMs > latest) latest = st.mtimeMs;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return latest;
|
|
1619
|
+
}
|
|
1620
|
+
var BYPASS_FLAG, SKIP_GIT_CHECK_FLAG, CODEX_MODEL_PRICING, TERMINAL_EVENTS, codexExecutor;
|
|
1621
|
+
var init_codex = __esm({
|
|
1622
|
+
"src/executors/codex.ts"() {
|
|
1623
|
+
"use strict";
|
|
1624
|
+
init_swarmHome();
|
|
1625
|
+
init_pricing();
|
|
1626
|
+
BYPASS_FLAG = "--dangerously-bypass-approvals-and-sandbox";
|
|
1627
|
+
SKIP_GIT_CHECK_FLAG = "--skip-git-repo-check";
|
|
1628
|
+
CODEX_MODEL_PRICING = {};
|
|
1629
|
+
TERMINAL_EVENTS = /* @__PURE__ */ new Set(["task_complete", "turn.completed", "thread.completed", "session.completed"]);
|
|
1630
|
+
codexExecutor = {
|
|
1631
|
+
id: "codex",
|
|
1632
|
+
title: "Codex CLI (codex)",
|
|
1633
|
+
binEnv: "SWARM_CODEX_BIN",
|
|
1634
|
+
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" },
|
|
1635
|
+
resolveBin: () => resolveCodexBin(),
|
|
1636
|
+
// Стрелка, а не метод — см. тот же комментарий в claudeCode.ts.
|
|
1637
|
+
plan: (spec) => {
|
|
1638
|
+
const notice = skillsNotice(spec.skillDirs);
|
|
1639
|
+
const prompt = notice ? `${spec.prompt}
|
|
1640
|
+
|
|
1641
|
+
${notice}` : spec.prompt;
|
|
1642
|
+
const args = [
|
|
1643
|
+
"exec",
|
|
1644
|
+
// Продолжение оборванной сессии — ПОДКОМАНДА, а не флаг: `exec resume --last` берёт
|
|
1645
|
+
// последнюю сессию этого CODEX_HOME (то есть этого worktree, см. codexHome).
|
|
1646
|
+
...spec.continueSession ? ["resume", "--last"] : [],
|
|
1647
|
+
"--json",
|
|
1648
|
+
SKIP_GIT_CHECK_FLAG,
|
|
1649
|
+
BYPASS_FLAG,
|
|
1650
|
+
...spec.model ? ["-m", spec.model] : [],
|
|
1651
|
+
...mcpConfigArgs(spec),
|
|
1652
|
+
prompt
|
|
1653
|
+
];
|
|
1654
|
+
return { bin: resolveCodexBin(), args, env: { CODEX_HOME: ensureCodexHome(spec.worktree, spec.realHome) } };
|
|
1655
|
+
},
|
|
1656
|
+
parseResult(raw, model) {
|
|
1657
|
+
const { usage, completed } = parseCodexStream(raw);
|
|
1658
|
+
if (!completed) return null;
|
|
1659
|
+
if (!usage) return { costUsd: 0, warning: "codex: \u043F\u043E\u0442\u043E\u043A \u0431\u0435\u0437 usage \u2014 cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D" };
|
|
1660
|
+
const cached = usage.cached_input_tokens ?? 0;
|
|
1661
|
+
const common = {
|
|
1662
|
+
input_tokens: Math.max(0, (usage.input_tokens ?? 0) - cached),
|
|
1663
|
+
output_tokens: usage.output_tokens ?? 0,
|
|
1664
|
+
cache_read_input_tokens: cached
|
|
1665
|
+
};
|
|
1666
|
+
const est = costFromUsage(common, model, CODEX_MODEL_PRICING);
|
|
1667
|
+
return est.warning ? { costUsd: est.costUsd, warning: est.warning } : { costUsd: est.costUsd };
|
|
1668
|
+
},
|
|
1669
|
+
session: {
|
|
1670
|
+
exists: (worktree) => newestRolloutMtime(join4(codexHomePath(worktree), "sessions")) !== null,
|
|
1671
|
+
mtimeMs: (worktree) => newestRolloutMtime(join4(codexHomePath(worktree), "sessions"))
|
|
1672
|
+
},
|
|
1673
|
+
probe: {
|
|
1674
|
+
// Тот же принцип, что у claude: дешёвый реальный прогон — он проверяет и auth, и живость
|
|
1675
|
+
// провайдера разом. Один короткий ответ, без тулов и без сети сверх модели.
|
|
1676
|
+
args: ["exec", "--json", SKIP_GIT_CHECK_FLAG, BYPASS_FLAG, "ok"],
|
|
1677
|
+
limitPattern: /(usage limit|rate limit|quota exceeded|insufficient_quota)/i
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
// src/executors/types.ts
|
|
1684
|
+
var init_types2 = __esm({
|
|
1685
|
+
"src/executors/types.ts"() {
|
|
1686
|
+
"use strict";
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
// src/executors/index.ts
|
|
1691
|
+
function resolveExecutor(id) {
|
|
1692
|
+
const key = (id ?? "").trim();
|
|
1693
|
+
if (!key) return EXECUTORS[DEFAULT_EXECUTOR_ID];
|
|
1694
|
+
const found = EXECUTORS[key];
|
|
1695
|
+
if (!found) {
|
|
1696
|
+
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(", ")}`);
|
|
1697
|
+
}
|
|
1698
|
+
return found;
|
|
1699
|
+
}
|
|
1700
|
+
var DEFAULT_EXECUTOR_ID, EXECUTORS;
|
|
1701
|
+
var init_executors = __esm({
|
|
1702
|
+
"src/executors/index.ts"() {
|
|
1703
|
+
"use strict";
|
|
1704
|
+
init_claudeCode();
|
|
1705
|
+
init_codex();
|
|
1706
|
+
init_types2();
|
|
1707
|
+
init_claudeCode();
|
|
1708
|
+
init_codex();
|
|
1709
|
+
DEFAULT_EXECUTOR_ID = "claude-code";
|
|
1710
|
+
EXECUTORS = {
|
|
1711
|
+
[claudeCodeExecutor.id]: claudeCodeExecutor,
|
|
1712
|
+
[codexExecutor.id]: codexExecutor
|
|
1713
|
+
};
|
|
795
1714
|
}
|
|
796
1715
|
});
|
|
797
1716
|
|
|
@@ -811,6 +1730,7 @@ __export(config_exports, {
|
|
|
811
1730
|
DRONE_HOME: () => DRONE_HOME,
|
|
812
1731
|
DRONE_HOME_ISOLATION: () => DRONE_HOME_ISOLATION,
|
|
813
1732
|
DRONE_ISOLATION: () => DRONE_ISOLATION,
|
|
1733
|
+
EXECUTOR: () => EXECUTOR,
|
|
814
1734
|
HEARTBEAT_PATH: () => HEARTBEAT_PATH,
|
|
815
1735
|
HEARTBEAT_STALE_MS: () => HEARTBEAT_STALE_MS,
|
|
816
1736
|
REPO_MAP_PATH: () => REPO_MAP_PATH,
|
|
@@ -880,9 +1800,8 @@ __export(config_exports, {
|
|
|
880
1800
|
resolveEffectiveWorktreesDir: () => resolveEffectiveWorktreesDir,
|
|
881
1801
|
reviewCutoffAt: () => reviewCutoffAt
|
|
882
1802
|
});
|
|
883
|
-
import {
|
|
884
|
-
import {
|
|
885
|
-
import { mkdirSync, readFileSync as readFileSync2, realpathSync, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1803
|
+
import { basename, join as join5, resolve } from "path";
|
|
1804
|
+
import { readFileSync as readFileSync2, realpathSync, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
886
1805
|
function normalizeRepo(repo) {
|
|
887
1806
|
const r = (repo ?? "").trim();
|
|
888
1807
|
if (!r) return "";
|
|
@@ -893,7 +1812,7 @@ function normalizeRepo(repo) {
|
|
|
893
1812
|
}
|
|
894
1813
|
}
|
|
895
1814
|
function droneWorktreePath(worktreesDir, repo, slug) {
|
|
896
|
-
return
|
|
1815
|
+
return join5(worktreesDir, basename(normalizeRepo(repo)), slug);
|
|
897
1816
|
}
|
|
898
1817
|
function resolveEffectiveWorktreesDir(deps) {
|
|
899
1818
|
if (deps.envOverride) return deps.envOverride;
|
|
@@ -935,7 +1854,7 @@ function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
|
|
|
935
1854
|
}
|
|
936
1855
|
function tokenFromFile() {
|
|
937
1856
|
try {
|
|
938
|
-
return readFileSync2(
|
|
1857
|
+
return readFileSync2(join5(SWARM_HOME, "token"), "utf8").trim();
|
|
939
1858
|
} catch {
|
|
940
1859
|
return "";
|
|
941
1860
|
}
|
|
@@ -949,34 +1868,36 @@ function reviewCutoffAt() {
|
|
|
949
1868
|
const at = stamp();
|
|
950
1869
|
try {
|
|
951
1870
|
const tmp = `${REVIEW_CUTOFF_PATH}.tmp`;
|
|
952
|
-
|
|
1871
|
+
writeFileSync3(tmp, JSON.stringify({ at }));
|
|
953
1872
|
renameSync2(tmp, REVIEW_CUTOFF_PATH);
|
|
954
1873
|
} catch {
|
|
955
1874
|
}
|
|
956
1875
|
return at;
|
|
957
1876
|
}
|
|
958
|
-
var
|
|
1877
|
+
var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_WATCHDOG_STALL_MS;
|
|
959
1878
|
var init_config = __esm({
|
|
960
1879
|
"src/config.ts"() {
|
|
961
1880
|
"use strict";
|
|
962
1881
|
init_src();
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1882
|
+
init_executors();
|
|
1883
|
+
init_swarmHome();
|
|
1884
|
+
init_swarmHome();
|
|
1885
|
+
DB_PATH = process.env.SWARM_DB || join5(SWARM_HOME, "swarm.db");
|
|
1886
|
+
EXECUTOR = resolveExecutor(process.env.SWARM_EXECUTOR);
|
|
1887
|
+
DRONE_HOME = process.env.SWARM_DRONE_HOME || join5(SWARM_HOME, "drone-home");
|
|
967
1888
|
DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
|
|
968
1889
|
DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
|
|
969
|
-
WORKTREES_NOINDEX_DIR =
|
|
970
|
-
WORKTREES_LEGACY_DIR =
|
|
1890
|
+
WORKTREES_NOINDEX_DIR = join5(SWARM_HOME, "worktrees.noindex");
|
|
1891
|
+
WORKTREES_LEGACY_DIR = join5(SWARM_HOME, "worktrees");
|
|
971
1892
|
WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
|
|
972
|
-
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS ||
|
|
973
|
-
DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG ||
|
|
1893
|
+
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join5(SWARM_HOME, "run-logs");
|
|
1894
|
+
DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join5(SWARM_HOME, "overmind.log");
|
|
974
1895
|
DAEMON_LOG_MAX_BYTES = envNum("SWARM_DAEMON_LOG_MAX_BYTES", 16 * 1024 * 1024);
|
|
975
1896
|
DAEMON_LOG_KEEP = envNum("SWARM_DAEMON_LOG_KEEP", 2);
|
|
976
1897
|
DAEMON_LOG_CHECK_MS = envNum("SWARM_DAEMON_LOG_CHECK_MS", 6e4);
|
|
977
1898
|
DIST_BUILD = true;
|
|
978
1899
|
SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
|
|
979
|
-
SWARM_OUTBOX = process.env.SWARM_OUTBOX ||
|
|
1900
|
+
SWARM_OUTBOX = process.env.SWARM_OUTBOX || join5(SWARM_HOME, "outbox.jsonl");
|
|
980
1901
|
SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
|
|
981
1902
|
SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
|
|
982
1903
|
SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
|
|
@@ -1001,15 +1922,15 @@ var init_config = __esm({
|
|
|
1001
1922
|
DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
|
|
1002
1923
|
DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
|
|
1003
1924
|
DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
|
|
1004
|
-
DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET ||
|
|
1925
|
+
DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join5(SWARM_HOME, "drone-channel.key");
|
|
1005
1926
|
DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
|
|
1006
|
-
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT ||
|
|
1927
|
+
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join5(SWARM_HOME, "overmind.heartbeat");
|
|
1007
1928
|
HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
1008
1929
|
SWARM_REPO = process.env.SWARM_REPO || "";
|
|
1009
1930
|
SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
|
|
1010
1931
|
SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
|
|
1011
1932
|
SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
|
|
1012
|
-
REPO_MAP_PATH = process.env.SWARM_REPO_MAP ||
|
|
1933
|
+
REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join5(SWARM_HOME, "repo-map.json");
|
|
1013
1934
|
SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
|
|
1014
1935
|
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
1015
1936
|
SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
@@ -1020,7 +1941,7 @@ var init_config = __esm({
|
|
|
1020
1941
|
TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
|
|
1021
1942
|
SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
|
|
1022
1943
|
SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
|
|
1023
|
-
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF ||
|
|
1944
|
+
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join5(SWARM_HOME, "review-cutoff.json");
|
|
1024
1945
|
SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
|
|
1025
1946
|
SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
|
|
1026
1947
|
WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
|
|
@@ -1377,10 +2298,10 @@ var init_storm = __esm({
|
|
|
1377
2298
|
});
|
|
1378
2299
|
|
|
1379
2300
|
// src/heartbeat.ts
|
|
1380
|
-
import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as
|
|
2301
|
+
import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
1381
2302
|
function writeHeartbeat(path, now) {
|
|
1382
2303
|
const tmp = `${path}.tmp`;
|
|
1383
|
-
|
|
2304
|
+
writeFileSync5(tmp, String(now));
|
|
1384
2305
|
renameSync3(tmp, path);
|
|
1385
2306
|
}
|
|
1386
2307
|
function readHeartbeat(path) {
|
|
@@ -1721,16 +2642,21 @@ var init_procCleanup = __esm({
|
|
|
1721
2642
|
});
|
|
1722
2643
|
|
|
1723
2644
|
// src/git.ts
|
|
1724
|
-
import { existsSync as
|
|
2645
|
+
import { existsSync as existsSync9, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
|
|
1725
2646
|
import { appendFile, mkdir, readFile, rm } from "fs/promises";
|
|
1726
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2647
|
+
import { isAbsolute as isAbsolute2, join as join15 } from "path";
|
|
1727
2648
|
import { $ as $2 } from "execa";
|
|
2649
|
+
function isUnrecoverableRefError(e) {
|
|
2650
|
+
if (e instanceof MissingRefError) return true;
|
|
2651
|
+
const msg = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
|
2652
|
+
return MISSING_REF_RE.test(msg);
|
|
2653
|
+
}
|
|
1728
2654
|
async function reclaimStaleWorktree(repo, worktree) {
|
|
1729
2655
|
await detachWorktree(repo, worktree);
|
|
1730
2656
|
}
|
|
1731
2657
|
function isLinkedWorktree(dir) {
|
|
1732
2658
|
try {
|
|
1733
|
-
return
|
|
2659
|
+
return statSync7(join15(dir, ".git")).isFile();
|
|
1734
2660
|
} catch {
|
|
1735
2661
|
return false;
|
|
1736
2662
|
}
|
|
@@ -1742,7 +2668,7 @@ async function detachWorktree(repo, worktree) {
|
|
|
1742
2668
|
}
|
|
1743
2669
|
}
|
|
1744
2670
|
async function ensureWorktreeDirGone(worktree) {
|
|
1745
|
-
if (!
|
|
2671
|
+
if (!existsSync9(worktree)) return false;
|
|
1746
2672
|
if (!isLinkedWorktree(worktree)) {
|
|
1747
2673
|
console.error(
|
|
1748
2674
|
`[git] ${worktree} \u2014 \u043D\u0435 \u043F\u0440\u0438\u0441\u0442\u0451\u0433\u043D\u0443\u0442\u044B\u0439 worktree (.git \u043D\u0435 \u0444\u0430\u0439\u043B): \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u041D\u0415 \u0442\u0440\u043E\u0433\u0430\u044E. \u0415\u0441\u043B\u0438 \u044D\u0442\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E worktree \u0434\u0440\u043E\u043D\u0430 \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043E \u0440\u0443\u043A\u0430\u043C\u0438; \u0435\u0441\u043B\u0438 \u044D\u0442\u043E \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u0447\u0435\u043A\u0430\u0443\u0442, \u0437\u043D\u0430\u0447\u0438\u0442 \u0432\u0435\u0442\u043A\u0430 \u0441\u043B\u0430\u0433\u0430 \u0447\u0435\u043A\u043D\u0443\u0442\u0430 \u0432 \u043D\u0451\u043C (git checkout <slug>) \u2014 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0435\u0433\u043E \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u0432\u0435\u0442\u043A\u0443.`
|
|
@@ -1757,7 +2683,7 @@ async function ensureWorktreeDirGone(worktree) {
|
|
|
1757
2683
|
);
|
|
1758
2684
|
return true;
|
|
1759
2685
|
}
|
|
1760
|
-
if (
|
|
2686
|
+
if (existsSync9(worktree)) {
|
|
1761
2687
|
console.error(`[git] \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree ${worktree} \u043F\u0435\u0440\u0435\u0436\u0438\u043B rm \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u043A\u0430\u043C\u0438, \u0438\u043D\u0430\u0447\u0435 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u0441\u043B\u0430\u0433\u0430 \u0432\u0441\u0442\u0430\u043D\u0435\u0442.`);
|
|
1762
2688
|
}
|
|
1763
2689
|
return true;
|
|
@@ -1800,7 +2726,7 @@ async function addWorktree(repo, path, branch, opts = {}) {
|
|
|
1800
2726
|
}
|
|
1801
2727
|
const busyAt = await worktreeForBranch(repo, branch);
|
|
1802
2728
|
if (busyAt) {
|
|
1803
|
-
const exists =
|
|
2729
|
+
const exists = existsSync9(busyAt);
|
|
1804
2730
|
const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
|
|
1805
2731
|
if (live) throw new WorktreeBusyError(branch, busyAt);
|
|
1806
2732
|
if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
|
|
@@ -1836,9 +2762,9 @@ async function ensureGitExcluded(worktree, pattern) {
|
|
|
1836
2762
|
const out = await $2({ reject: false })`git -C ${worktree} rev-parse --git-common-dir`;
|
|
1837
2763
|
const raw = out.stdout.trim();
|
|
1838
2764
|
if (out.exitCode !== 0 || !raw) return false;
|
|
1839
|
-
const commonDir = isAbsolute2(raw) ? raw :
|
|
1840
|
-
const infoDir =
|
|
1841
|
-
const excludeFile =
|
|
2765
|
+
const commonDir = isAbsolute2(raw) ? raw : join15(worktree, raw);
|
|
2766
|
+
const infoDir = join15(commonDir, "info");
|
|
2767
|
+
const excludeFile = join15(infoDir, "exclude");
|
|
1842
2768
|
let current = "";
|
|
1843
2769
|
try {
|
|
1844
2770
|
current = await readFile(excludeFile, "utf8");
|
|
@@ -1871,7 +2797,7 @@ async function worktreeDirty(path) {
|
|
|
1871
2797
|
async function repairMovedWorktrees(targetDir) {
|
|
1872
2798
|
const subdirs = (dir) => {
|
|
1873
2799
|
try {
|
|
1874
|
-
return
|
|
2800
|
+
return readdirSync10(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join15(dir, d.name));
|
|
1875
2801
|
} catch {
|
|
1876
2802
|
return [];
|
|
1877
2803
|
}
|
|
@@ -1915,7 +2841,9 @@ async function journalForBranch(repo, branch, slug) {
|
|
|
1915
2841
|
}
|
|
1916
2842
|
async function prepareDetachedWorktree(repo, branch, worktree) {
|
|
1917
2843
|
await $2`git -C ${repo} fetch --quiet origin`;
|
|
1918
|
-
|
|
2844
|
+
const ref = `origin/${branch}`;
|
|
2845
|
+
if (!await revParse(repo, `${ref}^{commit}`)) throw new MissingRefError(ref, repo);
|
|
2846
|
+
await $2`git -C ${repo} worktree add --detach --force ${worktree} ${ref}`;
|
|
1919
2847
|
}
|
|
1920
2848
|
async function mergeRef(worktree, ref) {
|
|
1921
2849
|
const before = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
|
|
@@ -1948,7 +2876,7 @@ async function worktreeLastCommitMs(worktree) {
|
|
|
1948
2876
|
const sec = Number(out);
|
|
1949
2877
|
return Number.isInteger(sec) && sec > 0 ? sec * 1e3 : null;
|
|
1950
2878
|
}
|
|
1951
|
-
var WorktreeBusyError, GITHUB_REMOTE_RE;
|
|
2879
|
+
var WorktreeBusyError, MissingRefError, MISSING_REF_RE, GITHUB_REMOTE_RE;
|
|
1952
2880
|
var init_git = __esm({
|
|
1953
2881
|
"src/git.ts"() {
|
|
1954
2882
|
"use strict";
|
|
@@ -1963,6 +2891,17 @@ var init_git = __esm({
|
|
|
1963
2891
|
branch;
|
|
1964
2892
|
worktreePath;
|
|
1965
2893
|
};
|
|
2894
|
+
MissingRefError = class extends Error {
|
|
2895
|
+
constructor(ref, repo) {
|
|
2896
|
+
super(`ref ${ref} \u043D\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 ${repo} \u2014 \u0432\u0435\u0442\u043A\u0443 \u043D\u0435\u043B\u044C\u0437\u044F \u0432\u044B\u0447\u0435\u043A\u0430\u0442\u044C (\u043F\u0440\u043E\u0432\u0435\u0440\u044C, \u0438\u0437 \u043A\u0430\u043A\u043E\u0439 \u0432\u0435\u0442\u043A\u0438 \u043E\u0442\u043A\u0440\u044B\u0442 PR)`);
|
|
2897
|
+
this.ref = ref;
|
|
2898
|
+
this.repo = repo;
|
|
2899
|
+
this.name = "MissingRefError";
|
|
2900
|
+
}
|
|
2901
|
+
ref;
|
|
2902
|
+
repo;
|
|
2903
|
+
};
|
|
2904
|
+
MISSING_REF_RE = /invalid reference|unknown revision|not a valid object name|couldn't find remote ref/i;
|
|
1966
2905
|
GITHUB_REMOTE_RE = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/i;
|
|
1967
2906
|
}
|
|
1968
2907
|
});
|
|
@@ -1983,7 +2922,7 @@ __export(droneChannel_exports, {
|
|
|
1983
2922
|
verifyLocalCliToken: () => verifyLocalCliToken
|
|
1984
2923
|
});
|
|
1985
2924
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
1986
|
-
import { chmodSync, mkdirSync as
|
|
2925
|
+
import { chmodSync, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
|
|
1987
2926
|
import { dirname as dirname3 } from "path";
|
|
1988
2927
|
import { serve } from "@hono/node-server";
|
|
1989
2928
|
function loadChannelSecret(path) {
|
|
@@ -1993,7 +2932,7 @@ function loadChannelSecret(path) {
|
|
|
1993
2932
|
} catch {
|
|
1994
2933
|
}
|
|
1995
2934
|
const secret = randomBytes(32).toString("hex");
|
|
1996
|
-
|
|
2935
|
+
mkdirSync7(dirname3(path), { recursive: true });
|
|
1997
2936
|
writeFileSync9(path, secret, { mode: 384 });
|
|
1998
2937
|
try {
|
|
1999
2938
|
chmodSync(path, 384);
|
|
@@ -2324,7 +3263,7 @@ var init_droneChannel = __esm({
|
|
|
2324
3263
|
|
|
2325
3264
|
// src/attachments.ts
|
|
2326
3265
|
import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
|
|
2327
|
-
import { join as
|
|
3266
|
+
import { join as join17 } from "path";
|
|
2328
3267
|
function attachmentsFromDoc(doc, origin) {
|
|
2329
3268
|
const out = [];
|
|
2330
3269
|
const walk2 = (node) => {
|
|
@@ -2415,7 +3354,7 @@ async function materializeAttachments(opts) {
|
|
|
2415
3354
|
};
|
|
2416
3355
|
const result = { files: [], skipped: [] };
|
|
2417
3356
|
if (opts.refs.length === 0) return result;
|
|
2418
|
-
const dir =
|
|
3357
|
+
const dir = join17(opts.worktree, ATTACHMENTS_DIR);
|
|
2419
3358
|
await io.removeDir(dir);
|
|
2420
3359
|
let dirReady = false;
|
|
2421
3360
|
let total = 0;
|
|
@@ -2461,7 +3400,7 @@ async function materializeAttachments(opts) {
|
|
|
2461
3400
|
await io.mkdir(dir);
|
|
2462
3401
|
dirReady = true;
|
|
2463
3402
|
}
|
|
2464
|
-
await io.writeFile(
|
|
3403
|
+
await io.writeFile(join17(dir, name), file.bytes);
|
|
2465
3404
|
} catch (e) {
|
|
2466
3405
|
result.skipped.push(`${label} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText(e)})`);
|
|
2467
3406
|
continue;
|
|
@@ -2614,7 +3553,7 @@ var init_attachments = __esm({
|
|
|
2614
3553
|
|
|
2615
3554
|
// src/shotUpload.ts
|
|
2616
3555
|
import { readFile as fsReadFile } from "fs/promises";
|
|
2617
|
-
import { basename as basename2, isAbsolute as isAbsolute3, join as
|
|
3556
|
+
import { basename as basename2, isAbsolute as isAbsolute3, join as join18 } from "path";
|
|
2618
3557
|
function shotMimetype(path) {
|
|
2619
3558
|
const dot = path.lastIndexOf(".");
|
|
2620
3559
|
if (dot < 0) return null;
|
|
@@ -2640,7 +3579,7 @@ async function readShots(paths, opts) {
|
|
|
2640
3579
|
}
|
|
2641
3580
|
let bytes;
|
|
2642
3581
|
try {
|
|
2643
|
-
bytes = await read(isAbsolute3(path) ? path :
|
|
3582
|
+
bytes = await read(isAbsolute3(path) ? path : join18(opts.worktree, path));
|
|
2644
3583
|
} catch (e) {
|
|
2645
3584
|
skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
|
|
2646
3585
|
continue;
|
|
@@ -2719,7 +3658,7 @@ var init_shotUpload = __esm({
|
|
|
2719
3658
|
// src/oauth.ts
|
|
2720
3659
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
2721
3660
|
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
|
|
2722
|
-
import { join as
|
|
3661
|
+
import { join as join19 } from "path";
|
|
2723
3662
|
function makePkce() {
|
|
2724
3663
|
const verifier = randomBytes2(48).toString("base64url");
|
|
2725
3664
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
@@ -2854,15 +3793,15 @@ var init_oauth = __esm({
|
|
|
2854
3793
|
AUTHORIZE = "/api/oauth/authorize";
|
|
2855
3794
|
TOKEN = "/api/oauth/token";
|
|
2856
3795
|
SCOPE = "openid profile email taskless:full";
|
|
2857
|
-
STORE_PATH =
|
|
2858
|
-
CLIENT_PATH =
|
|
3796
|
+
STORE_PATH = join19(SWARM_HOME, "oauth.json");
|
|
3797
|
+
CLIENT_PATH = join19(SWARM_HOME, "oauth-client.json");
|
|
2859
3798
|
base = (api) => api.replace(/\/$/, "");
|
|
2860
3799
|
}
|
|
2861
3800
|
});
|
|
2862
3801
|
|
|
2863
3802
|
// src/agentToken.ts
|
|
2864
3803
|
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
|
|
2865
|
-
import { join as
|
|
3804
|
+
import { join as join20 } from "path";
|
|
2866
3805
|
function loadAgentToken() {
|
|
2867
3806
|
try {
|
|
2868
3807
|
const s = JSON.parse(readFileSync15(STORE_PATH2, "utf8"));
|
|
@@ -2902,14 +3841,14 @@ var init_agentToken = __esm({
|
|
|
2902
3841
|
"src/agentToken.ts"() {
|
|
2903
3842
|
"use strict";
|
|
2904
3843
|
init_config();
|
|
2905
|
-
STORE_PATH2 =
|
|
3844
|
+
STORE_PATH2 = join20(SWARM_HOME, "agent.json");
|
|
2906
3845
|
base2 = (api) => api.replace(/\/$/, "");
|
|
2907
3846
|
}
|
|
2908
3847
|
});
|
|
2909
3848
|
|
|
2910
3849
|
// src/workspace.ts
|
|
2911
3850
|
import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
|
|
2912
|
-
import { join as
|
|
3851
|
+
import { join as join21 } from "path";
|
|
2913
3852
|
function loadWorkspace() {
|
|
2914
3853
|
try {
|
|
2915
3854
|
const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
|
|
@@ -2926,7 +3865,7 @@ var init_workspace = __esm({
|
|
|
2926
3865
|
"src/workspace.ts"() {
|
|
2927
3866
|
"use strict";
|
|
2928
3867
|
init_config();
|
|
2929
|
-
WORKSPACE_PATH =
|
|
3868
|
+
WORKSPACE_PATH = join21(SWARM_HOME, "workspace.json");
|
|
2930
3869
|
}
|
|
2931
3870
|
});
|
|
2932
3871
|
|
|
@@ -3009,6 +3948,11 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
|
|
|
3009
3948
|
if (!doc) return null;
|
|
3010
3949
|
return { title: doc.title ?? "", body: extractText(doc.body) };
|
|
3011
3950
|
}
|
|
3951
|
+
async function getTaskContext(idOrSlug) {
|
|
3952
|
+
const res = await safeCall("get_task_context", { id: idOrSlug });
|
|
3953
|
+
const text = res?.content?.find((c) => c.type === "text")?.text;
|
|
3954
|
+
return typeof text === "string" && text.trim() ? text : null;
|
|
3955
|
+
}
|
|
3012
3956
|
async function resolveId(idOrSlug) {
|
|
3013
3957
|
const cached = idCache.get(idOrSlug);
|
|
3014
3958
|
if (cached) return cached;
|
|
@@ -3076,6 +4020,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
|
|
|
3076
4020
|
},
|
|
3077
4021
|
backlogStatusId,
|
|
3078
4022
|
getDocument,
|
|
4023
|
+
getTaskContext,
|
|
3079
4024
|
createTask: async (input) => {
|
|
3080
4025
|
if (!input.projectId) {
|
|
3081
4026
|
throw new Error("createTask: projectId \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u2014 \u0430\u0432\u0442\u043E-\u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0440\u043E\u0435\u043C \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u043E (TASWM-204)");
|
|
@@ -3115,7 +4060,19 @@ function requireTaskBody(task, slug, workspaceId) {
|
|
|
3115
4060
|
if (!task) throw new TaskBodyUnavailableError(slug, workspaceId);
|
|
3116
4061
|
return task;
|
|
3117
4062
|
}
|
|
4063
|
+
function wrapUntrusted(parts) {
|
|
4064
|
+
if (parts.length === 0) return "";
|
|
4065
|
+
return [
|
|
4066
|
+
UNTRUSTED_TASK_INPUT_NOTICE,
|
|
4067
|
+
"",
|
|
4068
|
+
"\u2500\u2500\u2500\u2500\u2500 \u041D\u0415\u0414\u041E\u0412\u0415\u0420\u0415\u041D\u041D\u042B\u0419 \u0412\u0412\u041E\u0414: \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438/\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B \u0437\u0430\u0434\u0430\u0447\u0438 \u2500\u2500\u2500\u2500\u2500",
|
|
4069
|
+
parts.join("\n\n"),
|
|
4070
|
+
"\u2500\u2500\u2500\u2500\u2500 \u041A\u041E\u041D\u0415\u0426 \u041D\u0415\u0414\u041E\u0412\u0415\u0420\u0415\u041D\u041D\u041E\u0413\u041E \u0412\u0412\u041E\u0414\u0410 \u2500\u2500\u2500\u2500\u2500"
|
|
4071
|
+
].join("\n");
|
|
4072
|
+
}
|
|
3118
4073
|
async function fetchTaskContext(client, task) {
|
|
4074
|
+
const brief = await client.getTaskContext(task.id);
|
|
4075
|
+
if (brief) return wrapUntrusted([brief]);
|
|
3119
4076
|
const parts = [];
|
|
3120
4077
|
const comments = task.comments ?? [];
|
|
3121
4078
|
if (comments.length > 0) {
|
|
@@ -3151,14 +4108,7 @@ ${text || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
|
|
3151
4108
|
)
|
|
3152
4109
|
);
|
|
3153
4110
|
}
|
|
3154
|
-
|
|
3155
|
-
return [
|
|
3156
|
-
UNTRUSTED_TASK_INPUT_NOTICE,
|
|
3157
|
-
"",
|
|
3158
|
-
"\u2500\u2500\u2500\u2500\u2500 \u041D\u0415\u0414\u041E\u0412\u0415\u0420\u0415\u041D\u041D\u042B\u0419 \u0412\u0412\u041E\u0414: \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438/\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B \u0437\u0430\u0434\u0430\u0447\u0438 \u2500\u2500\u2500\u2500\u2500",
|
|
3159
|
-
parts.join("\n\n"),
|
|
3160
|
-
"\u2500\u2500\u2500\u2500\u2500 \u041A\u041E\u041D\u0415\u0426 \u041D\u0415\u0414\u041E\u0412\u0415\u0420\u0415\u041D\u041D\u041E\u0413\u041E \u0412\u0412\u041E\u0414\u0410 \u2500\u2500\u2500\u2500\u2500"
|
|
3161
|
-
].join("\n");
|
|
4111
|
+
return wrapUntrusted(parts);
|
|
3162
4112
|
}
|
|
3163
4113
|
function commentIdOf(res) {
|
|
3164
4114
|
const comment = res?.structuredContent?.comment;
|
|
@@ -3353,7 +4303,7 @@ __export(db_exports, {
|
|
|
3353
4303
|
SwarmDb: () => SwarmDb
|
|
3354
4304
|
});
|
|
3355
4305
|
import { createRequire as createRequire2 } from "module";
|
|
3356
|
-
import { existsSync as
|
|
4306
|
+
import { existsSync as existsSync11 } from "fs";
|
|
3357
4307
|
import { resolve as resolve3 } from "path";
|
|
3358
4308
|
function rowToQuestion(row) {
|
|
3359
4309
|
return {
|
|
@@ -3387,6 +4337,10 @@ function rowToRun(row) {
|
|
|
3387
4337
|
depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
|
|
3388
4338
|
model: row.model ?? null,
|
|
3389
4339
|
cost_usd: row.cost_usd ?? 0,
|
|
4340
|
+
// Токены/время работы (TASWM-353) — старые прогоны без колонок → 0.
|
|
4341
|
+
tokens_in: row.tokens_in ?? 0,
|
|
4342
|
+
tokens_out: row.tokens_out ?? 0,
|
|
4343
|
+
duration_ms: row.duration_ms ?? 0,
|
|
3390
4344
|
pr_url: row.pr_url ?? null,
|
|
3391
4345
|
superseded_by: row.superseded_by ?? null,
|
|
3392
4346
|
decision: row.decision ? JSON.parse(row.decision) : null,
|
|
@@ -3452,6 +4406,9 @@ CREATE TABLE IF NOT EXISTS runs (
|
|
|
3452
4406
|
depends_on TEXT,
|
|
3453
4407
|
model TEXT,
|
|
3454
4408
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
4409
|
+
tokens_in INTEGER NOT NULL DEFAULT 0,
|
|
4410
|
+
tokens_out INTEGER NOT NULL DEFAULT 0,
|
|
4411
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
3455
4412
|
pr_url TEXT,
|
|
3456
4413
|
superseded_by INTEGER,
|
|
3457
4414
|
decision TEXT,
|
|
@@ -3545,7 +4502,11 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
3545
4502
|
// Базовая ветка PR (TASWM-285): куда дрон открывает PR и от чего ответвляется.
|
|
3546
4503
|
"ALTER TABLE runs ADD COLUMN base_branch TEXT",
|
|
3547
4504
|
// Воркспейс задачи прогона (TASWM-288): им пинятся задачные MCP-вызовы прогона.
|
|
3548
|
-
"ALTER TABLE runs ADD COLUMN workspace_id TEXT"
|
|
4505
|
+
"ALTER TABLE runs ADD COLUMN workspace_id TEXT",
|
|
4506
|
+
// Чем обошёлся прогон, кроме денег (TASWM-353): токены вход/выход и время работы.
|
|
4507
|
+
"ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
|
|
4508
|
+
"ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
|
|
4509
|
+
"ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0"
|
|
3549
4510
|
]) {
|
|
3550
4511
|
try {
|
|
3551
4512
|
this.db.exec(alter);
|
|
@@ -3838,7 +4799,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
3838
4799
|
const repoN = normalizeRepo(repo);
|
|
3839
4800
|
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
3840
4801
|
if (already) return { imported: false, runs: already.run_count };
|
|
3841
|
-
if (!
|
|
4802
|
+
if (!existsSync11(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
3842
4803
|
const src = new this.databaseCtor(abs);
|
|
3843
4804
|
try {
|
|
3844
4805
|
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
@@ -4206,9 +5167,9 @@ var init_droneStore = __esm({
|
|
|
4206
5167
|
});
|
|
4207
5168
|
|
|
4208
5169
|
// src/overmind.ts
|
|
4209
|
-
import { homedir as
|
|
4210
|
-
import { existsSync as
|
|
4211
|
-
import { basename as basename4, dirname as dirname5, join as
|
|
5170
|
+
import { homedir as homedir5 } from "os";
|
|
5171
|
+
import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync12, renameSync as renameSync10 } from "fs";
|
|
5172
|
+
import { basename as basename4, dirname as dirname5, join as join28 } from "path";
|
|
4212
5173
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
4213
5174
|
import { spawn as spawn3 } from "child_process";
|
|
4214
5175
|
|
|
@@ -4288,13 +5249,13 @@ init_config();
|
|
|
4288
5249
|
init_resources();
|
|
4289
5250
|
init_names2();
|
|
4290
5251
|
init_storm();
|
|
4291
|
-
import { appendFileSync, existsSync as
|
|
5252
|
+
import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
4292
5253
|
import { dirname } from "path";
|
|
4293
5254
|
import { randomUUID } from "crypto";
|
|
4294
5255
|
|
|
4295
5256
|
// src/logTail.ts
|
|
4296
|
-
import { closeSync, existsSync as
|
|
4297
|
-
import { join as
|
|
5257
|
+
import { closeSync, existsSync as existsSync4, openSync, readSync, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
|
|
5258
|
+
import { join as join6 } from "path";
|
|
4298
5259
|
var TAIL_LINES_FALLBACK = 200;
|
|
4299
5260
|
var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
|
|
4300
5261
|
function fileHeader(name, size, shown) {
|
|
@@ -4304,7 +5265,7 @@ function fileHeader(name, size, shown) {
|
|
|
4304
5265
|
function readTail(path, opts) {
|
|
4305
5266
|
let size;
|
|
4306
5267
|
try {
|
|
4307
|
-
size =
|
|
5268
|
+
size = statSync3(path).size;
|
|
4308
5269
|
} catch {
|
|
4309
5270
|
return null;
|
|
4310
5271
|
}
|
|
@@ -4335,10 +5296,10 @@ function readTail(path, opts) {
|
|
|
4335
5296
|
return { text, size, shown: Buffer.byteLength(text, "utf8") };
|
|
4336
5297
|
}
|
|
4337
5298
|
function latestRunLaunch(runLogsDir, localRunId) {
|
|
4338
|
-
const dir =
|
|
5299
|
+
const dir = join6(runLogsDir, String(localRunId));
|
|
4339
5300
|
let files;
|
|
4340
5301
|
try {
|
|
4341
|
-
files =
|
|
5302
|
+
files = readdirSync4(dir);
|
|
4342
5303
|
} catch {
|
|
4343
5304
|
return null;
|
|
4344
5305
|
}
|
|
@@ -4349,13 +5310,13 @@ function latestRunLaunch(runLogsDir, localRunId) {
|
|
|
4349
5310
|
}
|
|
4350
5311
|
if (indexes.size === 0) return null;
|
|
4351
5312
|
const index = Math.max(...indexes);
|
|
4352
|
-
const out = [
|
|
4353
|
-
const err =
|
|
5313
|
+
const out = [join6(dir, `${index}.json`), join6(dir, `${index}.json.done`)].find((p) => existsSync4(p)) ?? null;
|
|
5314
|
+
const err = existsSync4(join6(dir, `${index}.err.log`)) ? join6(dir, `${index}.err.log`) : null;
|
|
4354
5315
|
return { index, total: indexes.size, out, err };
|
|
4355
5316
|
}
|
|
4356
5317
|
function runLogTail(runLogsDir, localRunId, opts) {
|
|
4357
5318
|
const launch = latestRunLaunch(runLogsDir, localRunId);
|
|
4358
|
-
if (!launch) return { error: `\u043B\u043E\u0433\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 (${
|
|
5319
|
+
if (!launch) return { error: `\u043B\u043E\u0433\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 (${join6(runLogsDir, String(localRunId))} \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442)` };
|
|
4359
5320
|
const parts = [];
|
|
4360
5321
|
let budget = opts.maxBytes;
|
|
4361
5322
|
if (launch.err) {
|
|
@@ -4488,6 +5449,15 @@ var SwarmClient = class _SwarmClient {
|
|
|
4488
5449
|
nextRunId = 1;
|
|
4489
5450
|
nextEventId = 1;
|
|
4490
5451
|
nextQuestionId = 1;
|
|
5452
|
+
// TASWM-350: ключ НЕЗАКРЫТОЙ попытки взять работу. Живёт от первого вызова claim до
|
|
5453
|
+
// первого настоящего ответа прода; всё это время ретраи идут с ним, и повтор после
|
|
5454
|
+
// обрыва связи не превращается во второе взятие. См. claim().
|
|
5455
|
+
pendingClaimKey = null;
|
|
5456
|
+
// TASWM-357: последнее НАПЕЧАТАННОЕ про пропущенные claim'ом задачи (родители и
|
|
5457
|
+
// заблокированные). Дедуп по тексту: claim идёт раз в ~60 c, а набор пропущенных меняется
|
|
5458
|
+
// редко — без этого лог был бы простынёй из одной и той же строки, и в ней потонуло бы
|
|
5459
|
+
// всё остальное. Меняется набор (эпик закрыли / блокер прошли) → печатаем снова.
|
|
5460
|
+
lastClaimSkipLog = null;
|
|
4491
5461
|
// Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
|
|
4492
5462
|
// Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
|
|
4493
5463
|
// переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
|
|
@@ -4498,6 +5468,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
4498
5468
|
// ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
|
|
4499
5469
|
// каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
|
|
4500
5470
|
notifyLevelUnsupported = false;
|
|
5471
|
+
// TAS-485: то же для гашения разрешившихся тревог (swarm_notify_resolve) — прод старее
|
|
5472
|
+
// демона не знает тула; ретраить это каждый тик бессмысленно, см. resolveAnomalies.
|
|
5473
|
+
notifyResolveUnsupported = false;
|
|
4501
5474
|
// Корреляция с продом.
|
|
4502
5475
|
keyByLocal = /* @__PURE__ */ new Map();
|
|
4503
5476
|
// local run id → runKey
|
|
@@ -4540,24 +5513,24 @@ var SwarmClient = class _SwarmClient {
|
|
|
4540
5513
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
4541
5514
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
4542
5515
|
this.host = opts.host ?? null;
|
|
4543
|
-
this.repoExists = opts.repoExists ??
|
|
5516
|
+
this.repoExists = opts.repoExists ?? existsSync5;
|
|
4544
5517
|
this.questionSyncMs = opts.questionSyncMs ?? 3e3;
|
|
4545
5518
|
this.outboxPath = opts.outboxPath;
|
|
4546
5519
|
this.mapPath = `${opts.outboxPath}.prodmap.json`;
|
|
4547
5520
|
this.stormPath = `${opts.outboxPath}.storm.jsonl`;
|
|
4548
|
-
|
|
5521
|
+
mkdirSync4(dirname(this.outboxPath), { recursive: true });
|
|
4549
5522
|
this.recover();
|
|
4550
5523
|
}
|
|
4551
5524
|
// ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
|
|
4552
5525
|
recover() {
|
|
4553
|
-
if (
|
|
5526
|
+
if (existsSync5(this.mapPath)) {
|
|
4554
5527
|
try {
|
|
4555
5528
|
const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
|
|
4556
5529
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
4557
5530
|
} catch {
|
|
4558
5531
|
}
|
|
4559
5532
|
}
|
|
4560
|
-
if (
|
|
5533
|
+
if (existsSync5(this.outboxPath)) {
|
|
4561
5534
|
const txt = readFileSync4(this.outboxPath, "utf8");
|
|
4562
5535
|
for (const line of txt.split("\n")) {
|
|
4563
5536
|
const s = line.trim();
|
|
@@ -4578,7 +5551,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
4578
5551
|
// eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
|
|
4579
5552
|
// переприсваиваем локально (id событий — только порядок, не корреляция с продом).
|
|
4580
5553
|
replayGlobalEvents() {
|
|
4581
|
-
if (!
|
|
5554
|
+
if (!existsSync5(this.stormPath)) return;
|
|
4582
5555
|
let txt;
|
|
4583
5556
|
try {
|
|
4584
5557
|
txt = readFileSync4(this.stormPath, "utf8");
|
|
@@ -4647,8 +5620,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
4647
5620
|
this.scheduleFlush();
|
|
4648
5621
|
}
|
|
4649
5622
|
// Прод-патч прогона: локальные поля Run → поля swarm_run_upsert (см. TASWM-74).
|
|
4650
|
-
// pid/worktree_path (рантайм),
|
|
4651
|
-
//
|
|
5623
|
+
// pid/worktree_path (рантайм), снимок задачи (task_*, прод берёт join'ом из Task)
|
|
5624
|
+
// и priority (локальная очередь ЭТОГО демона, TASWM-269)
|
|
4652
5625
|
// в прод НЕ уходят. kind → role (плоский enum). Self-ref
|
|
4653
5626
|
// прогонов hero_run_id/resolves_run_id/superseded_by → heroId/resolvesRunId/
|
|
4654
5627
|
// supersededById как ОТЛОЖЕННЫЕ ссылки (RunRef) — cuid резолвится на флаше.
|
|
@@ -4659,6 +5632,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
4659
5632
|
if ("model" in patch) out.model = patch.model ?? null;
|
|
4660
5633
|
if ("name" in patch) out.droneName = patch.name ?? null;
|
|
4661
5634
|
if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
|
|
5635
|
+
if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
|
|
5636
|
+
if ("tokens_out" in patch && patch.tokens_out !== void 0) out.tokensOut = patch.tokens_out;
|
|
5637
|
+
if ("duration_ms" in patch && patch.duration_ms !== void 0) out.durationMs = patch.duration_ms;
|
|
5638
|
+
if ("respawns" in patch && patch.respawns !== void 0) out.respawns = patch.respawns;
|
|
4662
5639
|
if ("pr_url" in patch) out.prUrl = patch.pr_url ?? null;
|
|
4663
5640
|
if ("branch" in patch) out.branch = patch.branch ?? null;
|
|
4664
5641
|
if ("repo" in patch) out.repo = patch.repo ?? null;
|
|
@@ -4724,6 +5701,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
4724
5701
|
depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
|
|
4725
5702
|
model: input.model ?? null,
|
|
4726
5703
|
cost_usd: 0,
|
|
5704
|
+
tokens_in: 0,
|
|
5705
|
+
// копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
|
|
5706
|
+
tokens_out: 0,
|
|
5707
|
+
duration_ms: 0,
|
|
4727
5708
|
pr_url: null,
|
|
4728
5709
|
superseded_by: null,
|
|
4729
5710
|
decision: input.decision ?? null,
|
|
@@ -4846,6 +5827,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
4846
5827
|
if (!d) return 0;
|
|
4847
5828
|
d.respawns += 1;
|
|
4848
5829
|
d.updated_at = stamp();
|
|
5830
|
+
this.enqueueRun(d, this.runFields({ respawns: d.respawns }));
|
|
4849
5831
|
return d.respawns;
|
|
4850
5832
|
}
|
|
4851
5833
|
cancel(id, reason) {
|
|
@@ -5261,7 +6243,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
5261
6243
|
* транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
|
|
5262
6244
|
*/
|
|
5263
6245
|
reconcileRuntime(opts) {
|
|
5264
|
-
const wtExists = opts.worktreeExists ??
|
|
6246
|
+
const wtExists = opts.worktreeExists ?? existsSync5;
|
|
5265
6247
|
const pidByWorktree = /* @__PURE__ */ new Map();
|
|
5266
6248
|
try {
|
|
5267
6249
|
for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
|
|
@@ -5349,23 +6331,69 @@ var SwarmClient = class _SwarmClient {
|
|
|
5349
6331
|
* шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
|
|
5350
6332
|
*/
|
|
5351
6333
|
async claim() {
|
|
6334
|
+
this.pendingClaimKey ??= randomUUID();
|
|
5352
6335
|
let res;
|
|
5353
6336
|
try {
|
|
5354
6337
|
res = await this.call("swarm_claim_work", {
|
|
5355
6338
|
...this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {},
|
|
5356
6339
|
// TASWM-281: взятая из общей очереди задача сразу принадлежит ЭТОЙ машине —
|
|
5357
6340
|
// прод помечает созданный прогон hostId, чужой демон его не увидит.
|
|
5358
|
-
...this.host ? { hostId: this.host.hostId } : {}
|
|
6341
|
+
...this.host ? { hostId: this.host.hostId } : {},
|
|
6342
|
+
idempotencyKey: this.pendingClaimKey
|
|
5359
6343
|
});
|
|
5360
6344
|
} catch {
|
|
5361
6345
|
return null;
|
|
5362
6346
|
}
|
|
5363
6347
|
if (res?.isError) return null;
|
|
6348
|
+
this.pendingClaimKey = null;
|
|
5364
6349
|
const sc = res.structuredContent ?? {};
|
|
6350
|
+
this.logClaimSkips(sc);
|
|
5365
6351
|
if (!sc.claimed || typeof sc.run !== "object" || sc.run === null) return null;
|
|
5366
6352
|
const local = this.hydrateRun(sc.run);
|
|
5367
6353
|
return local ? this.mat(local) : null;
|
|
5368
6354
|
}
|
|
6355
|
+
/**
|
|
6356
|
+
* TASWM-357: назвать в логе задачи, которые прод НЕ отдал в работу осознанно — родителей с
|
|
6357
|
+
* незавершёнными подзадачами (работа всегда в листьях) и листья с незакрытым блокером
|
|
6358
|
+
* (явный порядок, связь задач «блокирует»). Печатаем ЗДЕСЬ, а не на проде: оба фильтра
|
|
6359
|
+
* живут SQL-условиями и молчат по природе, а `display` claim'а демон не логирует — без
|
|
6360
|
+
* этой строки «почему эпик стоит нетронутым» не видно ниоткуда, и оператор ровно так же
|
|
6361
|
+
* гадает, как гадал, пока дрон вставал на эпике с вопросом «меня спавнили на родителя».
|
|
6362
|
+
*
|
|
6363
|
+
* Формат прода — `data.skippedParents: [{slug,title,openChildren}]` и
|
|
6364
|
+
* `data.skippedBlocked: [{slug,title,blockers[]}]`. Читаем защищённо: у старого прода
|
|
6365
|
+
* полей нет вовсе (тогда молчим), а разбирать чужую строку `display` вместо структуры —
|
|
6366
|
+
* тот же класс хрупкости, что уже кусал нас в notifyAnomaly.
|
|
6367
|
+
*/
|
|
6368
|
+
logClaimSkips(sc) {
|
|
6369
|
+
const rows = (raw) => (Array.isArray(raw) ? raw : []).map((p) => typeof p === "object" && p !== null ? p : null).filter((p) => p !== null && typeof p.slug === "string");
|
|
6370
|
+
const others = (flag) => flag === true ? " \u0438 \u0434\u0440\u0443\u0433\u0438\u0435" : "";
|
|
6371
|
+
const lines = [];
|
|
6372
|
+
const parents = rows(sc.skippedParents).map(
|
|
6373
|
+
(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)`
|
|
6374
|
+
);
|
|
6375
|
+
if (parents.length > 0) {
|
|
6376
|
+
lines.push(
|
|
6377
|
+
`[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)}`
|
|
6378
|
+
);
|
|
6379
|
+
}
|
|
6380
|
+
const blocked = rows(sc.skippedBlocked).map(
|
|
6381
|
+
(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"}`
|
|
6382
|
+
);
|
|
6383
|
+
if (blocked.length > 0) {
|
|
6384
|
+
lines.push(
|
|
6385
|
+
`[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)}`
|
|
6386
|
+
);
|
|
6387
|
+
}
|
|
6388
|
+
const text = lines.join("\n");
|
|
6389
|
+
if (text.length === 0) {
|
|
6390
|
+
this.lastClaimSkipLog = null;
|
|
6391
|
+
return;
|
|
6392
|
+
}
|
|
6393
|
+
if (text === this.lastClaimSkipLog) return;
|
|
6394
|
+
this.lastClaimSkipLog = text;
|
|
6395
|
+
for (const line of lines) console.log(line);
|
|
6396
|
+
}
|
|
5369
6397
|
/**
|
|
5370
6398
|
* TASWM-281: пометить своими прогоны, под которыми на ЭТОЙ машине нашёлся ЖИВОЙ
|
|
5371
6399
|
* процесс (после reconcileRuntime на старте). Это ровно те прогоны, где сомнений в
|
|
@@ -5639,12 +6667,20 @@ var SwarmClient = class _SwarmClient {
|
|
|
5639
6667
|
* Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
|
|
5640
6668
|
* означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
|
|
5641
6669
|
*
|
|
6670
|
+
* TASWM-352: тем же пульсом уходит ЁМКОСТЬ этой машины (`capacity` — живой потолок
|
|
6671
|
+
* дронов ЭТОГО демона: машинный оверрайд, иначе общая настройка владельца). По ней
|
|
6672
|
+
* планировщик роя раскладывает очередь между машинами: до этого у прода было ОДНО
|
|
6673
|
+
* число на владельца, и ноутбук с рабочей станцией делили его поровну, хотя тянут
|
|
6674
|
+
* разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
|
|
6675
|
+
* трогает прошлое значение — та же логика, что у repoFullNames.
|
|
6676
|
+
*
|
|
5642
6677
|
* Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
|
|
5643
6678
|
*/
|
|
5644
|
-
async heartbeat(daemonVersion, pause) {
|
|
6679
|
+
async heartbeat(daemonVersion, pause, capacity) {
|
|
5645
6680
|
try {
|
|
5646
6681
|
const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
|
|
5647
6682
|
if (daemonVersion) args.daemonVersion = daemonVersion;
|
|
6683
|
+
if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
|
|
5648
6684
|
if (pause !== void 0) {
|
|
5649
6685
|
args.paused = !!pause?.paused;
|
|
5650
6686
|
if (pause?.reason) args.pauseReason = pause.reason;
|
|
@@ -5750,6 +6786,35 @@ var SwarmClient = class _SwarmClient {
|
|
|
5750
6786
|
const res = await this.call("swarm_notify", base3);
|
|
5751
6787
|
if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
|
|
5752
6788
|
}
|
|
6789
|
+
/**
|
|
6790
|
+
* TAS-485: погасить в инбоксе владельца тревоги, чьё условие ушло. Ключи — те же, с
|
|
6791
|
+
* которыми уходил notifyAnomaly (прод сам ставит им префикс `swarm:`).
|
|
6792
|
+
*
|
|
6793
|
+
* Бросает при сбое — СОЗНАТЕЛЬНО, как notifyAnomaly: вызывающий (tickOnce) вычёркивает
|
|
6794
|
+
* ключи из durable-очереди только после успеха, иначе «погашено» разошлось бы с тем,
|
|
6795
|
+
* что человек видит в инбоксе, и разошлось бы навсегда — повторить будет некому.
|
|
6796
|
+
*
|
|
6797
|
+
* ДЕПЛОЙ-ОКНО. Демон из свежего чекаута против ещё не задеплоенного прода получит
|
|
6798
|
+
* «неизвестный тул». Это не ошибка доставки, а отсутствие фичи на той стороне: ретраить
|
|
6799
|
+
* её каждые ~4 секунды бессмысленно, поэтому первую такую осечку запоминаем на жизнь
|
|
6800
|
+
* процесса и дальше молча пропускаем гашение (надзор в остальном работает как раньше).
|
|
6801
|
+
* Тот же приём, что у notifyAnomaly с аргументом `level`.
|
|
6802
|
+
*/
|
|
6803
|
+
async resolveAnomalies(keys) {
|
|
6804
|
+
const tags = [...new Set(keys.filter((k) => k.length > 0 && k.length <= 120))];
|
|
6805
|
+
if (tags.length === 0 || this.notifyResolveUnsupported) return;
|
|
6806
|
+
const res = await this.call("swarm_notify_resolve", { tags });
|
|
6807
|
+
if (!res?.isError) return;
|
|
6808
|
+
const text = JSON.stringify(res);
|
|
6809
|
+
if (text.includes("swarm_notify_resolve") && (text.includes("Unknown tool") || text.includes("\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D"))) {
|
|
6810
|
+
this.notifyResolveUnsupported = true;
|
|
6811
|
+
console.warn(
|
|
6812
|
+
"[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."
|
|
6813
|
+
);
|
|
6814
|
+
return;
|
|
6815
|
+
}
|
|
6816
|
+
throw new Error(`swarm_notify_resolve \u043E\u0442\u0432\u0435\u0440\u0433 ${tags.length} \u043A\u043B\u044E\u0447(\u0435\u0439)`);
|
|
6817
|
+
}
|
|
5753
6818
|
/**
|
|
5754
6819
|
* TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
|
|
5755
6820
|
* через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
|
|
@@ -5814,6 +6879,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
5814
6879
|
depends_on: [],
|
|
5815
6880
|
model: null,
|
|
5816
6881
|
cost_usd: 0,
|
|
6882
|
+
tokens_in: 0,
|
|
6883
|
+
// токены и время работы (TASWM-353) — прод-поля, заполняются ниже
|
|
6884
|
+
tokens_out: 0,
|
|
6885
|
+
duration_ms: 0,
|
|
5817
6886
|
pr_url: null,
|
|
5818
6887
|
superseded_by: null,
|
|
5819
6888
|
decision: null,
|
|
@@ -5824,9 +6893,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
5824
6893
|
review_verdict: null,
|
|
5825
6894
|
review_phase: null,
|
|
5826
6895
|
// вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
|
|
5827
|
-
// Локальная бухгалтерия (в прод-схему не
|
|
5828
|
-
//
|
|
5829
|
-
//
|
|
6896
|
+
// Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
|
|
6897
|
+
// существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
|
|
6898
|
+
// в отличие от respawns, — авто-ревью v2, TASWM-162.
|
|
5830
6899
|
review_cycle: 0,
|
|
5831
6900
|
task_title: null,
|
|
5832
6901
|
task_desc: null,
|
|
@@ -5878,6 +6947,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
5878
6947
|
local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
|
|
5879
6948
|
local.model = pr.model ?? null;
|
|
5880
6949
|
local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
|
|
6950
|
+
local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
|
|
6951
|
+
local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
|
|
6952
|
+
local.duration_ms = typeof pr.durationMs === "number" ? pr.durationMs : 0;
|
|
5881
6953
|
local.pr_url = pr.prUrl ?? null;
|
|
5882
6954
|
local.stop_reason = pr.stopReason ?? null;
|
|
5883
6955
|
local.respawns = typeof pr.respawns === "number" ? pr.respawns : 0;
|
|
@@ -6135,13 +7207,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
6135
7207
|
}
|
|
6136
7208
|
persistPending() {
|
|
6137
7209
|
try {
|
|
6138
|
-
|
|
7210
|
+
writeFileSync4(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
|
|
6139
7211
|
} catch {
|
|
6140
7212
|
}
|
|
6141
7213
|
}
|
|
6142
7214
|
persistMap() {
|
|
6143
7215
|
try {
|
|
6144
|
-
|
|
7216
|
+
writeFileSync4(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
|
|
6145
7217
|
} catch {
|
|
6146
7218
|
}
|
|
6147
7219
|
}
|
|
@@ -6171,11 +7243,11 @@ init_config();
|
|
|
6171
7243
|
init_heartbeat();
|
|
6172
7244
|
|
|
6173
7245
|
// src/logRotate.ts
|
|
6174
|
-
import { copyFileSync, renameSync as renameSync4, statSync as
|
|
7246
|
+
import { copyFileSync, renameSync as renameSync4, statSync as statSync4, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
|
|
6175
7247
|
var realIo = {
|
|
6176
7248
|
size: (path) => {
|
|
6177
7249
|
try {
|
|
6178
|
-
return
|
|
7250
|
+
return statSync4(path).size;
|
|
6179
7251
|
} catch {
|
|
6180
7252
|
return null;
|
|
6181
7253
|
}
|
|
@@ -6184,7 +7256,7 @@ var realIo = {
|
|
|
6184
7256
|
rename: renameSync4,
|
|
6185
7257
|
truncate: (path) => truncateSync(path, 0),
|
|
6186
7258
|
unlink: unlinkSync2,
|
|
6187
|
-
exists:
|
|
7259
|
+
exists: existsSync6
|
|
6188
7260
|
};
|
|
6189
7261
|
var realOwnerIo = {
|
|
6190
7262
|
statFd: (fd) => {
|
|
@@ -6197,7 +7269,7 @@ var realOwnerIo = {
|
|
|
6197
7269
|
},
|
|
6198
7270
|
statPath: (path) => {
|
|
6199
7271
|
try {
|
|
6200
|
-
const s =
|
|
7272
|
+
const s = statSync4(path);
|
|
6201
7273
|
return { dev: s.dev, ino: s.ino };
|
|
6202
7274
|
} catch {
|
|
6203
7275
|
return null;
|
|
@@ -6268,13 +7340,13 @@ async function buildRepoRegistry(paths) {
|
|
|
6268
7340
|
}
|
|
6269
7341
|
|
|
6270
7342
|
// src/repoMemory.ts
|
|
6271
|
-
import { readFileSync as readFileSync6, writeFileSync as
|
|
6272
|
-
import { join as
|
|
7343
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
7344
|
+
import { join as join7 } from "path";
|
|
6273
7345
|
var FILE = "repos.json";
|
|
6274
7346
|
function loadRememberedRepos(home) {
|
|
6275
7347
|
let raw;
|
|
6276
7348
|
try {
|
|
6277
|
-
raw = readFileSync6(
|
|
7349
|
+
raw = readFileSync6(join7(home, FILE), "utf8");
|
|
6278
7350
|
} catch {
|
|
6279
7351
|
return [];
|
|
6280
7352
|
}
|
|
@@ -6289,7 +7361,7 @@ function loadRememberedRepos(home) {
|
|
|
6289
7361
|
function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
|
|
6290
7362
|
const clean = paths.filter((p) => p.trim().length > 0);
|
|
6291
7363
|
if (clean.length === 0) return;
|
|
6292
|
-
|
|
7364
|
+
writeFileSync6(join7(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
|
|
6293
7365
|
`);
|
|
6294
7366
|
}
|
|
6295
7367
|
function pickRepoPaths(args) {
|
|
@@ -6302,13 +7374,13 @@ function pickRepoPaths(args) {
|
|
|
6302
7374
|
// src/repoDiscovery.ts
|
|
6303
7375
|
init_src();
|
|
6304
7376
|
init_config();
|
|
6305
|
-
import { homedir as
|
|
6306
|
-
import { readFileSync as readFileSync8, renameSync as renameSync5, writeFileSync as
|
|
6307
|
-
import { isAbsolute, join as
|
|
7377
|
+
import { homedir as homedir3 } from "os";
|
|
7378
|
+
import { readFileSync as readFileSync8, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
7379
|
+
import { isAbsolute, join as join9, resolve as resolve2 } from "path";
|
|
6308
7380
|
|
|
6309
7381
|
// src/repoScan.ts
|
|
6310
|
-
import { readdirSync as
|
|
6311
|
-
import { join as
|
|
7382
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
|
|
7383
|
+
import { join as join8 } from "path";
|
|
6312
7384
|
var SKIP = /* @__PURE__ */ new Set([
|
|
6313
7385
|
"node_modules",
|
|
6314
7386
|
"vendor",
|
|
@@ -6338,7 +7410,7 @@ function parseOriginFromGitConfig(text) {
|
|
|
6338
7410
|
}
|
|
6339
7411
|
function originOf(dir) {
|
|
6340
7412
|
try {
|
|
6341
|
-
return parseOriginFromGitConfig(readFileSync7(
|
|
7413
|
+
return parseOriginFromGitConfig(readFileSync7(join8(dir, ".git", "config"), "utf8"));
|
|
6342
7414
|
} catch {
|
|
6343
7415
|
return null;
|
|
6344
7416
|
}
|
|
@@ -6367,15 +7439,15 @@ function walk(dir, depth, maxDepth, found) {
|
|
|
6367
7439
|
}
|
|
6368
7440
|
let entries;
|
|
6369
7441
|
try {
|
|
6370
|
-
entries =
|
|
7442
|
+
entries = readdirSync5(dir);
|
|
6371
7443
|
} catch {
|
|
6372
7444
|
return;
|
|
6373
7445
|
}
|
|
6374
7446
|
for (const name of entries) {
|
|
6375
7447
|
if (name.startsWith(".") || SKIP.has(name)) continue;
|
|
6376
|
-
const child =
|
|
7448
|
+
const child = join8(dir, name);
|
|
6377
7449
|
try {
|
|
6378
|
-
if (!
|
|
7450
|
+
if (!statSync5(child).isDirectory()) continue;
|
|
6379
7451
|
} catch {
|
|
6380
7452
|
continue;
|
|
6381
7453
|
}
|
|
@@ -6395,13 +7467,13 @@ function defaultScanRoots(home, knownPaths = []) {
|
|
|
6395
7467
|
if (parent && parent !== p) add(parent);
|
|
6396
7468
|
}
|
|
6397
7469
|
for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
|
|
6398
|
-
add(
|
|
7470
|
+
add(join8(home, name));
|
|
6399
7471
|
}
|
|
6400
7472
|
return out;
|
|
6401
7473
|
}
|
|
6402
7474
|
|
|
6403
7475
|
// src/repoDiscovery.ts
|
|
6404
|
-
function resolveScanRoots(raw, home =
|
|
7476
|
+
function resolveScanRoots(raw, home = homedir3(), knownPaths = []) {
|
|
6405
7477
|
const v = (raw ?? "").trim();
|
|
6406
7478
|
if (!v) return defaultScanRoots(home, knownPaths);
|
|
6407
7479
|
if (["off", "none", "0"].includes(v.toLowerCase())) return [];
|
|
@@ -6409,7 +7481,7 @@ function resolveScanRoots(raw, home = homedir2(), knownPaths = []) {
|
|
|
6409
7481
|
for (const part of v.split(",")) {
|
|
6410
7482
|
const p = part.trim();
|
|
6411
7483
|
if (!p) continue;
|
|
6412
|
-
const abs = p === "~" ? home : p.startsWith("~/") ?
|
|
7484
|
+
const abs = p === "~" ? home : p.startsWith("~/") ? join9(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
|
|
6413
7485
|
if (!out.includes(abs)) out.push(abs);
|
|
6414
7486
|
}
|
|
6415
7487
|
return out;
|
|
@@ -6547,7 +7619,7 @@ function readRepoMapCache(path) {
|
|
|
6547
7619
|
function writeRepoMapCache(path, entries) {
|
|
6548
7620
|
try {
|
|
6549
7621
|
const tmp = `${path}.tmp`;
|
|
6550
|
-
|
|
7622
|
+
writeFileSync7(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
|
|
6551
7623
|
renameSync5(tmp, path);
|
|
6552
7624
|
} catch {
|
|
6553
7625
|
}
|
|
@@ -6555,17 +7627,17 @@ function writeRepoMapCache(path, entries) {
|
|
|
6555
7627
|
|
|
6556
7628
|
// src/hostIdentity.ts
|
|
6557
7629
|
import { hostname } from "os";
|
|
6558
|
-
import { existsSync as
|
|
6559
|
-
import { join as
|
|
7630
|
+
import { existsSync as existsSync7, readFileSync as readFileSync9, renameSync as renameSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
7631
|
+
import { join as join10 } from "path";
|
|
6560
7632
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6561
7633
|
function hostIdentityPath(home) {
|
|
6562
|
-
return
|
|
7634
|
+
return join10(home, "host.json");
|
|
6563
7635
|
}
|
|
6564
7636
|
function loadHostIdentity(deps) {
|
|
6565
|
-
const exists = deps.exists ??
|
|
7637
|
+
const exists = deps.exists ?? existsSync7;
|
|
6566
7638
|
const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
|
|
6567
7639
|
const writeFile2 = deps.writeFile ?? ((p, data) => {
|
|
6568
|
-
|
|
7640
|
+
writeFileSync8(`${p}.tmp`, data);
|
|
6569
7641
|
renameSync6(`${p}.tmp`, p);
|
|
6570
7642
|
});
|
|
6571
7643
|
const host = deps.hostname ?? hostname;
|
|
@@ -6706,129 +7778,57 @@ function capList(items) {
|
|
|
6706
7778
|
}
|
|
6707
7779
|
function reliefAdvice(ctx) {
|
|
6708
7780
|
const steps = [];
|
|
6709
|
-
if (!Number.isFinite(ctx.maxDrones)) steps.push("\u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0442\u044C \u0440\u043E\u0439 \u043E\u0434\u043D\u0438\u043C \u0434\u0440\u043E\u043D\u043E\u043C (maxDrones=1) \u2014 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u0440\u043E\u044F \u0432 /swarm");
|
|
6710
|
-
else if (ctx.maxDrones > 1) steps.push(`\u0441\u043D\u0438\u0437\u0438\u0442\u044C maxDrones \u0434\u043E 1 (\u0441\u0435\u0439\u0447\u0430\u0441 ${ctx.maxDrones}) \u2014 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u0440\u043E\u044F \u0432 /swarm`);
|
|
6711
|
-
if (ctx.exclusiveRepos.length) steps.push(`\u043F\u043E\u043C\u0435\u0442\u0438\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442 \u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u044B\u043C (\u043E\u0434\u0438\u043D \u0434\u0440\u043E\u043D \u043D\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439): ${capList(ctx.exclusiveRepos)}`);
|
|
6712
|
-
if (ctx.otherHosts.length) steps.push(`\u0443\u0432\u0435\u0437\u0442\u0438 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u043C\u0430\u0448\u0438\u043D\u0443 \u0440\u043E\u044F (spawn --host): ${capList(ctx.otherHosts)}`);
|
|
6713
|
-
steps.push(
|
|
6714
|
-
ctx.totalGb > 0 ? `\u043C\u0430\u0448\u0438\u043D\u0430 \u043C\u043E\u0449\u043D\u0435\u0435: ${ctx.totalGb.toFixed(0)} \u0413\u0411 \u043D\u0430 ${Math.max(ctx.activeDrones, 2)} \u0434\u0440\u043E\u043D\u0430 Claude Code \u043F\u043B\u044E\u0441 \u0441\u0431\u043E\u0440\u043A\u0438 \u2014 \u0432\u043F\u0440\u0438\u0442\u044B\u043A` : `\u043C\u0430\u0448\u0438\u043D\u0430 \u043C\u043E\u0449\u043D\u0435\u0435: ${Math.max(ctx.activeDrones, 2)} \u0434\u0440\u043E\u043D\u0430 Claude Code \u043F\u043B\u044E\u0441 \u0441\u0431\u043E\u0440\u043A\u0438 \u044D\u0442\u0430 \u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u0442\u044F\u043D\u0435\u0442`
|
|
6715
|
-
);
|
|
6716
|
-
return steps;
|
|
6717
|
-
}
|
|
6718
|
-
function humanMinutes(ms) {
|
|
6719
|
-
const total = Math.round(ms / 6e4);
|
|
6720
|
-
if (total < 60) return `${total} \u043C\u0438\u043D`;
|
|
6721
|
-
return `${Math.floor(total / 60)} \u0447 ${String(total % 60).padStart(2, "0")} \u043C\u0438\u043D`;
|
|
6722
|
-
}
|
|
6723
|
-
function reliefAlertBody(cost, pressure, advice, windowMs, minSteps) {
|
|
6724
|
-
const window = humanMinutes(windowMs);
|
|
6725
|
-
const perPause = cost.pauses > 0 ? humanMinutes(cost.lostMs / cost.pauses) : null;
|
|
6726
|
-
const parts = [
|
|
6727
|
-
`\u0417\u0430 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${window} \u0434\u0440\u043E\u043D\u043E\u0432 \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u043B\u0438 ${cost.pauses} \u0440\u0430\u0437${cost.runs > 1 ? ` (${cost.runs} \u043F\u0440\u043E\u0433\u043E\u043D\u0430)` : ""}, \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E \u043E\u043A\u043E\u043B\u043E ${humanMinutes(cost.lostMs)}${perPause ? `, \u0432 \u0441\u0440\u0435\u0434\u043D\u0435\u043C ${perPause} \u043C\u0435\u0436\u0434\u0443 \u043F\u0430\u0443\u0437\u043E\u0439 \u0438 \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u043E\u043C` : ""}.`
|
|
6728
|
-
];
|
|
6729
|
-
if (cost.stepsBetween != null) {
|
|
6730
|
-
parts.push(
|
|
6731
|
-
cost.stepsBetween < minSteps ? `\u041C\u0435\u0436\u0434\u0443 \u043F\u0430\u0443\u0437\u0430\u043C\u0438 \u0434\u0440\u043E\u043D \u043D\u0435 \u0443\u0441\u043F\u0435\u0432\u0430\u0435\u0442 \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C \u0448\u0430\u0433 (\u043C\u0435\u0434\u0438\u0430\u043D\u0430 ${cost.stepsBetween}) \u2014 \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u0438\u0434\u0451\u0442, \u0434\u0440\u043E\u043D \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F \u043D\u0430 \u043C\u0435\u0441\u0442\u0435.` : `\u041C\u0435\u0436\u0434\u0443 \u043F\u0430\u0443\u0437\u0430\u043C\u0438 \u0434\u0440\u043E\u043D \u0443\u0441\u043F\u0435\u0432\u0430\u0435\u0442 ${cost.stepsBetween} \u0448\u0430\u0433(\u0430).`
|
|
6732
|
-
);
|
|
6733
|
-
}
|
|
6734
|
-
if (cost.worst && cost.runs > 1) parts.push(`\u0411\u043E\u043B\u044C\u0448\u0435 \u0432\u0441\u0435\u0445 \u0434\u043E\u0441\u0442\u0430\u043B\u043E\u0441\u044C ${cost.worst.label} \u2014 ${cost.worst.pauses} \u0440\u0430\u0437.`);
|
|
6735
|
-
if (pressure.culprits.length) parts.push(`\u0423\u043F\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u0432: ${pressure.culprits.join("; ")}.`);
|
|
6736
|
-
parts.push(`\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u0438\u044F \u0445\u043E\u0441\u0442\u0430 \u2014 ${pressure.readings.join("; ")}.`);
|
|
6737
|
-
parts.push(`\u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C \u043F\u043E \u043F\u043E\u0440\u044F\u0434\u043A\u0443: ${advice.map((s, i) => `${i + 1}) ${s}`).join("; ")}.`);
|
|
6738
|
-
const body = parts.join(" ");
|
|
6739
|
-
return body.length <= MAX_ALERT_BODY_CHARS ? body : `${body.slice(0, MAX_ALERT_BODY_CHARS - 1)}\u2026`;
|
|
6740
|
-
}
|
|
6741
|
-
function reliefAlertTitle(cost, windowMs) {
|
|
6742
|
-
return `\u041C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u0442\u044F\u043D\u0435\u0442 \u0440\u043E\u0439: ${cost.pauses} \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043E\u043A \u0437\u0430 ${humanMinutes(windowMs)}, \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E ${humanMinutes(cost.lostMs)}`;
|
|
6743
|
-
}
|
|
6744
|
-
function buildReliefCostCandidates(cost, t, sustainMs, build) {
|
|
6745
|
-
if (!reliefAlertDue(cost, t)) return [];
|
|
6746
|
-
return [{ key: "relief_cost", minDurationMs: sustainMs, build }];
|
|
6747
|
-
}
|
|
6748
|
-
|
|
6749
|
-
// src/launcher.ts
|
|
6750
|
-
import { spawn } from "child_process";
|
|
6751
|
-
import { closeSync as closeSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, openSync as openSync2, readdirSync as readdirSync5, statSync as statSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
6752
|
-
import { createRequire } from "module";
|
|
6753
|
-
import { homedir as homedir3 } from "os";
|
|
6754
|
-
import { dirname as dirname2, join as join9 } from "path";
|
|
6755
|
-
import { fileURLToPath } from "url";
|
|
6756
|
-
|
|
6757
|
-
// src/claudeBin.ts
|
|
6758
|
-
import { existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
|
|
6759
|
-
import { win32 as winPath } from "path";
|
|
6760
|
-
var CLAUDE_FALLBACK = "claude";
|
|
6761
|
-
function compareVersionDirs(a, b) {
|
|
6762
|
-
const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
|
|
6763
|
-
const pa = parts(a);
|
|
6764
|
-
const pb = parts(b);
|
|
6765
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
6766
|
-
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
6767
|
-
if (d !== 0) return d;
|
|
6768
|
-
}
|
|
6769
|
-
return 0;
|
|
6770
|
-
}
|
|
6771
|
-
function realListDir(dir) {
|
|
6772
|
-
try {
|
|
6773
|
-
return readdirSync3(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
6774
|
-
} catch {
|
|
6775
|
-
return [];
|
|
6776
|
-
}
|
|
6777
|
-
}
|
|
6778
|
-
function winVersionRoots(env, listDir) {
|
|
6779
|
-
const roots = [];
|
|
6780
|
-
const appData = env.APPDATA;
|
|
6781
|
-
if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
|
|
6782
|
-
const localAppData = env.LOCALAPPDATA;
|
|
6783
|
-
if (localAppData) {
|
|
6784
|
-
const packages = winPath.join(localAppData, "Packages");
|
|
6785
|
-
for (const name of listDir(packages)) {
|
|
6786
|
-
if (!name.startsWith("Claude")) continue;
|
|
6787
|
-
roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
|
|
6788
|
-
}
|
|
6789
|
-
}
|
|
6790
|
-
return roots;
|
|
6791
|
-
}
|
|
6792
|
-
function findWindowsClaude(opts = {}) {
|
|
6793
|
-
const env = opts.env ?? process.env;
|
|
6794
|
-
const listDir = opts.listDir ?? realListDir;
|
|
6795
|
-
const fileExists = opts.fileExists ?? existsSync6;
|
|
6796
|
-
let best = null;
|
|
6797
|
-
for (const root of winVersionRoots(env, listDir)) {
|
|
6798
|
-
for (const version of listDir(root)) {
|
|
6799
|
-
const candidate = winPath.join(root, version, "claude.exe");
|
|
6800
|
-
if (!fileExists(candidate)) continue;
|
|
6801
|
-
if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
|
|
6802
|
-
}
|
|
6803
|
-
}
|
|
6804
|
-
return best?.path ?? null;
|
|
7781
|
+
if (!Number.isFinite(ctx.maxDrones)) steps.push("\u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0442\u044C \u0440\u043E\u0439 \u043E\u0434\u043D\u0438\u043C \u0434\u0440\u043E\u043D\u043E\u043C (maxDrones=1) \u2014 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u0440\u043E\u044F \u0432 /swarm");
|
|
7782
|
+
else if (ctx.maxDrones > 1) steps.push(`\u0441\u043D\u0438\u0437\u0438\u0442\u044C maxDrones \u0434\u043E 1 (\u0441\u0435\u0439\u0447\u0430\u0441 ${ctx.maxDrones}) \u2014 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u0440\u043E\u044F \u0432 /swarm`);
|
|
7783
|
+
if (ctx.exclusiveRepos.length) steps.push(`\u043F\u043E\u043C\u0435\u0442\u0438\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442 \u044D\u043A\u0441\u043A\u043B\u044E\u0437\u0438\u0432\u043D\u044B\u043C (\u043E\u0434\u0438\u043D \u0434\u0440\u043E\u043D \u043D\u0430 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439): ${capList(ctx.exclusiveRepos)}`);
|
|
7784
|
+
if (ctx.otherHosts.length) steps.push(`\u0443\u0432\u0435\u0437\u0442\u0438 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u043C\u0430\u0448\u0438\u043D\u0443 \u0440\u043E\u044F (spawn --host): ${capList(ctx.otherHosts)}`);
|
|
7785
|
+
steps.push(
|
|
7786
|
+
ctx.totalGb > 0 ? `\u043C\u0430\u0448\u0438\u043D\u0430 \u043C\u043E\u0449\u043D\u0435\u0435: ${ctx.totalGb.toFixed(0)} \u0413\u0411 \u043D\u0430 ${Math.max(ctx.activeDrones, 2)} \u0434\u0440\u043E\u043D\u0430 Claude Code \u043F\u043B\u044E\u0441 \u0441\u0431\u043E\u0440\u043A\u0438 \u2014 \u0432\u043F\u0440\u0438\u0442\u044B\u043A` : `\u043C\u0430\u0448\u0438\u043D\u0430 \u043C\u043E\u0449\u043D\u0435\u0435: ${Math.max(ctx.activeDrones, 2)} \u0434\u0440\u043E\u043D\u0430 Claude Code \u043F\u043B\u044E\u0441 \u0441\u0431\u043E\u0440\u043A\u0438 \u044D\u0442\u0430 \u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u0442\u044F\u043D\u0435\u0442`
|
|
7787
|
+
);
|
|
7788
|
+
return steps;
|
|
6805
7789
|
}
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
const
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
if (!warnedNoWindowsClaude) {
|
|
6821
|
-
warnedNoWindowsClaude = true;
|
|
6822
|
-
console.warn(
|
|
6823
|
-
"[overmind] claude.exe \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u043D\u0438 \u0432 %APPDATA%\\Claude\\claude-code, \u043D\u0438 \u0432 MSIX-\u043A\u043E\u043F\u0438\u0438 \u043F\u043E\u0434 %LOCALAPPDATA%\\Packages\\Claude* \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u0433\u043E\u043B\u043E\u0435 \xABclaude\xBB (\u043F\u043E\u0438\u0441\u043A \u043F\u043E PATH+PATHEXT). \u0423\u043F\u0430\u0434\u0451\u0442 ENOENT (bash-\u043E\u0431\u0451\u0440\u0442\u043A\u0430 \u0431\u0435\u0437 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0432 PATH \u043D\u0435 \u0432\u0438\u0434\u043D\u0430 spawn'\u0443) \u2014 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 SWARM_CLAUDE_BIN \u043F\u0443\u0442\u0451\u043C \u043A \u043D\u0430\u0441\u0442\u043E\u044F\u0449\u0435\u043C\u0443 claude.exe."
|
|
7790
|
+
function humanMinutes(ms) {
|
|
7791
|
+
const total = Math.round(ms / 6e4);
|
|
7792
|
+
if (total < 60) return `${total} \u043C\u0438\u043D`;
|
|
7793
|
+
return `${Math.floor(total / 60)} \u0447 ${String(total % 60).padStart(2, "0")} \u043C\u0438\u043D`;
|
|
7794
|
+
}
|
|
7795
|
+
function reliefAlertBody(cost, pressure, advice, windowMs, minSteps) {
|
|
7796
|
+
const window = humanMinutes(windowMs);
|
|
7797
|
+
const perPause = cost.pauses > 0 ? humanMinutes(cost.lostMs / cost.pauses) : null;
|
|
7798
|
+
const parts = [
|
|
7799
|
+
`\u0417\u0430 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${window} \u0434\u0440\u043E\u043D\u043E\u0432 \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u043B\u0438 ${cost.pauses} \u0440\u0430\u0437${cost.runs > 1 ? ` (${cost.runs} \u043F\u0440\u043E\u0433\u043E\u043D\u0430)` : ""}, \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E \u043E\u043A\u043E\u043B\u043E ${humanMinutes(cost.lostMs)}${perPause ? `, \u0432 \u0441\u0440\u0435\u0434\u043D\u0435\u043C ${perPause} \u043C\u0435\u0436\u0434\u0443 \u043F\u0430\u0443\u0437\u043E\u0439 \u0438 \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u043E\u043C` : ""}.`
|
|
7800
|
+
];
|
|
7801
|
+
if (cost.stepsBetween != null) {
|
|
7802
|
+
parts.push(
|
|
7803
|
+
cost.stepsBetween < minSteps ? `\u041C\u0435\u0436\u0434\u0443 \u043F\u0430\u0443\u0437\u0430\u043C\u0438 \u0434\u0440\u043E\u043D \u043D\u0435 \u0443\u0441\u043F\u0435\u0432\u0430\u0435\u0442 \u0434\u043E\u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C \u0448\u0430\u0433 (\u043C\u0435\u0434\u0438\u0430\u043D\u0430 ${cost.stepsBetween}) \u2014 \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u0438\u0434\u0451\u0442, \u0434\u0440\u043E\u043D \u043A\u0440\u0443\u0442\u0438\u0442\u0441\u044F \u043D\u0430 \u043C\u0435\u0441\u0442\u0435.` : `\u041C\u0435\u0436\u0434\u0443 \u043F\u0430\u0443\u0437\u0430\u043C\u0438 \u0434\u0440\u043E\u043D \u0443\u0441\u043F\u0435\u0432\u0430\u0435\u0442 ${cost.stepsBetween} \u0448\u0430\u0433(\u0430).`
|
|
6824
7804
|
);
|
|
6825
7805
|
}
|
|
6826
|
-
|
|
7806
|
+
if (cost.worst && cost.runs > 1) parts.push(`\u0411\u043E\u043B\u044C\u0448\u0435 \u0432\u0441\u0435\u0445 \u0434\u043E\u0441\u0442\u0430\u043B\u043E\u0441\u044C ${cost.worst.label} \u2014 ${cost.worst.pauses} \u0440\u0430\u0437.`);
|
|
7807
|
+
if (pressure.culprits.length) parts.push(`\u0423\u043F\u0438\u0440\u0430\u0435\u0442\u0441\u044F \u0432: ${pressure.culprits.join("; ")}.`);
|
|
7808
|
+
parts.push(`\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u0438\u044F \u0445\u043E\u0441\u0442\u0430 \u2014 ${pressure.readings.join("; ")}.`);
|
|
7809
|
+
parts.push(`\u0427\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C \u043F\u043E \u043F\u043E\u0440\u044F\u0434\u043A\u0443: ${advice.map((s, i) => `${i + 1}) ${s}`).join("; ")}.`);
|
|
7810
|
+
const body = parts.join(" ");
|
|
7811
|
+
return body.length <= MAX_ALERT_BODY_CHARS ? body : `${body.slice(0, MAX_ALERT_BODY_CHARS - 1)}\u2026`;
|
|
7812
|
+
}
|
|
7813
|
+
function reliefAlertTitle(cost, windowMs) {
|
|
7814
|
+
return `\u041C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u0442\u044F\u043D\u0435\u0442 \u0440\u043E\u0439: ${cost.pauses} \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043E\u043A \u0437\u0430 ${humanMinutes(windowMs)}, \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E ${humanMinutes(cost.lostMs)}`;
|
|
7815
|
+
}
|
|
7816
|
+
function buildReliefCostCandidates(cost, t, sustainMs, build) {
|
|
7817
|
+
if (!reliefAlertDue(cost, t)) return [];
|
|
7818
|
+
return [{ key: "relief_cost", minDurationMs: sustainMs, build }];
|
|
6827
7819
|
}
|
|
6828
7820
|
|
|
7821
|
+
// src/launcher.ts
|
|
7822
|
+
import { spawn } from "child_process";
|
|
7823
|
+
import { closeSync as closeSync2, existsSync as existsSync8, mkdirSync as mkdirSync6, openSync as openSync2, readdirSync as readdirSync7 } from "fs";
|
|
7824
|
+
import { createRequire } from "module";
|
|
7825
|
+
import { homedir as homedir4 } from "os";
|
|
7826
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
7827
|
+
import { fileURLToPath } from "url";
|
|
7828
|
+
|
|
6829
7829
|
// src/droneHome.ts
|
|
6830
|
-
import { lstatSync, mkdirSync as
|
|
6831
|
-
import { join as
|
|
7830
|
+
import { lstatSync, mkdirSync as mkdirSync5, readdirSync as readdirSync6, rmSync, statSync as statSync6, symlinkSync as symlinkSync2 } from "fs";
|
|
7831
|
+
import { join as join11, relative, sep } from "path";
|
|
6832
7832
|
var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
|
|
6833
7833
|
function droneHomeDeny(env = process.env) {
|
|
6834
7834
|
const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -6846,34 +7846,34 @@ function buildDroneHome(opts) {
|
|
|
6846
7846
|
const { realHome, droneHome } = opts;
|
|
6847
7847
|
const deny = opts.deny ?? droneHomeDeny();
|
|
6848
7848
|
const platform = opts.platform ?? process.platform;
|
|
6849
|
-
|
|
6850
|
-
const wanted = new Set(mirroredEntries(
|
|
7849
|
+
mkdirSync5(droneHome, { recursive: true, mode: 448 });
|
|
7850
|
+
const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
|
|
6851
7851
|
const rel = relative(realHome, droneHome);
|
|
6852
7852
|
if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
|
|
6853
|
-
for (const existing of
|
|
7853
|
+
for (const existing of readdirSync6(droneHome)) {
|
|
6854
7854
|
if (wanted.has(existing)) continue;
|
|
6855
|
-
const p =
|
|
7855
|
+
const p = join11(droneHome, existing);
|
|
6856
7856
|
try {
|
|
6857
7857
|
if (lstatSync(p).isSymbolicLink()) rmSync(p);
|
|
6858
7858
|
} catch {
|
|
6859
7859
|
}
|
|
6860
7860
|
}
|
|
6861
7861
|
for (const name of wanted) {
|
|
6862
|
-
const link =
|
|
7862
|
+
const link = join11(droneHome, name);
|
|
6863
7863
|
try {
|
|
6864
7864
|
lstatSync(link);
|
|
6865
7865
|
continue;
|
|
6866
7866
|
} catch {
|
|
6867
7867
|
}
|
|
6868
|
-
const target =
|
|
7868
|
+
const target = join11(realHome, name);
|
|
6869
7869
|
let isDir;
|
|
6870
7870
|
try {
|
|
6871
|
-
isDir =
|
|
7871
|
+
isDir = statSync6(target).isDirectory();
|
|
6872
7872
|
} catch {
|
|
6873
7873
|
continue;
|
|
6874
7874
|
}
|
|
6875
7875
|
try {
|
|
6876
|
-
|
|
7876
|
+
symlinkSync2(target, link, isDir ? "junction" : "file");
|
|
6877
7877
|
} catch (e) {
|
|
6878
7878
|
if (e.code !== "EEXIST") throw e;
|
|
6879
7879
|
}
|
|
@@ -6886,10 +7886,10 @@ var REAL_PATH_ENV_PINS = [
|
|
|
6886
7886
|
{ env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
|
|
6887
7887
|
];
|
|
6888
7888
|
function realPathEnvPins(realHome) {
|
|
6889
|
-
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env,
|
|
7889
|
+
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join11(realHome, ...segments)]));
|
|
6890
7890
|
}
|
|
6891
|
-
function
|
|
6892
|
-
|
|
7891
|
+
function dronePrivatePaths(realHome, swarmHome) {
|
|
7892
|
+
return [
|
|
6893
7893
|
// НЕ `${swarmHome}/**`. Worktree дрона живёт ВНУТРИ swarmHome
|
|
6894
7894
|
// (`<swarmHome>/worktrees.noindex/<repo>/<slug>`), и правило на весь каталог закрывало
|
|
6895
7895
|
// дрону его собственный рабочий каталог: Read/Write/Edit отвечали «File is in a directory
|
|
@@ -6910,11 +7910,6 @@ function droneDenyRules(realHome, swarmHome) {
|
|
|
6910
7910
|
`${realHome}/.claude/.credentials.json`,
|
|
6911
7911
|
`${realHome}/.npmrc`
|
|
6912
7912
|
];
|
|
6913
|
-
const abs = (p) => `//${p.replace(/^\/+/, "")}`;
|
|
6914
|
-
return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
|
|
6915
|
-
}
|
|
6916
|
-
function droneSettingsJson(realHome, swarmHome) {
|
|
6917
|
-
return JSON.stringify({ permissions: { deny: droneDenyRules(realHome, swarmHome) } });
|
|
6918
7913
|
}
|
|
6919
7914
|
|
|
6920
7915
|
// src/testForks.ts
|
|
@@ -6946,6 +7941,7 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
|
|
|
6946
7941
|
}
|
|
6947
7942
|
|
|
6948
7943
|
// src/launcher.ts
|
|
7944
|
+
init_executors();
|
|
6949
7945
|
var requireFromHere = createRequire(import.meta.url);
|
|
6950
7946
|
function resolveTsxCli() {
|
|
6951
7947
|
try {
|
|
@@ -6956,29 +7952,12 @@ function resolveTsxCli() {
|
|
|
6956
7952
|
);
|
|
6957
7953
|
}
|
|
6958
7954
|
}
|
|
6959
|
-
var
|
|
6960
|
-
function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
|
|
6961
|
-
const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
|
|
6962
|
-
const base3 = [
|
|
6963
|
-
"--dangerously-skip-permissions",
|
|
6964
|
-
...ISOLATION_FLAGS,
|
|
6965
|
-
"--mcp-config",
|
|
6966
|
-
cfgPath,
|
|
6967
|
-
...settings ? ["--settings", settings] : [],
|
|
6968
|
-
...pluginFlags,
|
|
6969
|
-
"--output-format",
|
|
6970
|
-
"json",
|
|
6971
|
-
"-p",
|
|
6972
|
-
prompt
|
|
6973
|
-
];
|
|
6974
|
-
const withModel = model ? ["--model", model, ...base3] : base3;
|
|
6975
|
-
return continueSession ? ["--continue", ...withModel] : withModel;
|
|
6976
|
-
}
|
|
7955
|
+
var MCP_SERVER_NAME = "overmind";
|
|
6977
7956
|
function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
|
|
6978
7957
|
const env = { ...base3 };
|
|
6979
7958
|
const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
|
|
6980
7959
|
if (isolate && !base3.TURBO_CACHE_DIR) {
|
|
6981
|
-
env.TURBO_CACHE_DIR =
|
|
7960
|
+
env.TURBO_CACHE_DIR = join12(worktree, ".turbo", "cache");
|
|
6982
7961
|
}
|
|
6983
7962
|
Object.assign(env, droneTestEnv(droneCap, base3));
|
|
6984
7963
|
for (const key of DAEMON_STATE_ENV) delete env[key];
|
|
@@ -7006,49 +7985,20 @@ var DAEMON_STATE_ENV = [
|
|
|
7006
7985
|
"TASKLESS_TOKEN"
|
|
7007
7986
|
];
|
|
7008
7987
|
function bundledRoleDir(kind) {
|
|
7009
|
-
return
|
|
7988
|
+
return join12(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
|
|
7010
7989
|
}
|
|
7011
7990
|
function roleDir(worktree, kind) {
|
|
7012
|
-
return
|
|
7991
|
+
return join12(worktree, ".claude", "roles", kind);
|
|
7013
7992
|
}
|
|
7014
7993
|
function roleDirs(worktree, kind) {
|
|
7015
|
-
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) =>
|
|
7016
|
-
}
|
|
7017
|
-
function transcriptDir(worktree, projectsDir) {
|
|
7018
|
-
return join9(projectsDir, worktree.replace(/[/.]/g, "-"));
|
|
7019
|
-
}
|
|
7020
|
-
function sessionTranscriptExists(worktree, projectsDir = join9(homedir3(), ".claude", "projects")) {
|
|
7021
|
-
try {
|
|
7022
|
-
return readdirSync5(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
|
|
7023
|
-
} catch {
|
|
7024
|
-
return false;
|
|
7025
|
-
}
|
|
7026
|
-
}
|
|
7027
|
-
function transcriptMtimeMs(worktree, projectsDir = join9(homedir3(), ".claude", "projects")) {
|
|
7028
|
-
const dir = transcriptDir(worktree, projectsDir);
|
|
7029
|
-
let files;
|
|
7030
|
-
try {
|
|
7031
|
-
files = readdirSync5(dir);
|
|
7032
|
-
} catch {
|
|
7033
|
-
return null;
|
|
7034
|
-
}
|
|
7035
|
-
let latest = null;
|
|
7036
|
-
for (const f of files) {
|
|
7037
|
-
if (!f.endsWith(".jsonl")) continue;
|
|
7038
|
-
try {
|
|
7039
|
-
const mtime2 = statSync5(join9(dir, f)).mtimeMs;
|
|
7040
|
-
if (latest === null || mtime2 > latest) latest = mtime2;
|
|
7041
|
-
} catch {
|
|
7042
|
-
}
|
|
7043
|
-
}
|
|
7044
|
-
return latest;
|
|
7994
|
+
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync8(dir));
|
|
7045
7995
|
}
|
|
7046
7996
|
function nextRunLogPath(runLogsDir, runId) {
|
|
7047
|
-
const dir =
|
|
7048
|
-
|
|
7997
|
+
const dir = join12(runLogsDir, String(runId));
|
|
7998
|
+
mkdirSync6(dir, { recursive: true });
|
|
7049
7999
|
let files = [];
|
|
7050
8000
|
try {
|
|
7051
|
-
files =
|
|
8001
|
+
files = readdirSync7(dir);
|
|
7052
8002
|
} catch {
|
|
7053
8003
|
}
|
|
7054
8004
|
let next = 0;
|
|
@@ -7056,7 +8006,7 @@ function nextRunLogPath(runLogsDir, runId) {
|
|
|
7056
8006
|
const m = /^(\d+)\.json/.exec(f);
|
|
7057
8007
|
if (m) next = Math.max(next, Number(m[1]) + 1);
|
|
7058
8008
|
}
|
|
7059
|
-
return
|
|
8009
|
+
return join12(dir, `${next}.json`);
|
|
7060
8010
|
}
|
|
7061
8011
|
function openNextRunLog(runLogsDir, runId) {
|
|
7062
8012
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
@@ -7091,57 +8041,63 @@ function makeLauncher(cfg) {
|
|
|
7091
8041
|
);
|
|
7092
8042
|
}
|
|
7093
8043
|
const channelEnv = cfg.channel && channelToken ? { SWARM_DRONE_CHANNEL_URL: cfg.channel.url, SWARM_DRONE_CHANNEL_TOKEN: channelToken } : { SWARM_DB: cfg.dbPath };
|
|
7094
|
-
const
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
|
|
7129
|
-
}
|
|
7130
|
-
}
|
|
8044
|
+
const mcp = {
|
|
8045
|
+
name: MCP_SERVER_NAME,
|
|
8046
|
+
command: process.execPath,
|
|
8047
|
+
args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["--conditions=development", resolveTsxCli(), cfg.overmindEntry, "--drone", "--run", String(run.id)],
|
|
8048
|
+
// SWARM_DRONE_CHANNEL_URL/TOKEN (TASWM-295) ИЛИ SWARM_DB (откат) — см. channelEnv выше.
|
|
8049
|
+
// Канал: дрон говорит только с демоном, файл базы ему не отдаётся вовсе. Токен подписан
|
|
8050
|
+
// на СТАБИЛЬНЫЙ ключ прогона, поэтому переживает рестарт демона.
|
|
8051
|
+
// SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — доверенный
|
|
8052
|
+
// источник «чья это сессия» (read_task и отчёт о сдаче доверяют ему, а не строке,
|
|
8053
|
+
// найденной по номеру прогона).
|
|
8054
|
+
// SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID (TASWM-263) и SWARM_RUN_NAME
|
|
8055
|
+
// (TASWM-265): роль, связи и имя прогона той же спавн-фиксацией. Нужны РЕЖИМУ ОТКАТА —
|
|
8056
|
+
// ensureRunHydrated заводит ими стаб-строку в sqlite, если демон в client-режиме её там
|
|
8057
|
+
// не заводил. На канале прогон отдаёт демон целиком, и стаб не нужен.
|
|
8058
|
+
// SWARM_WORKSPACE_ID (TASWM-288): ВОРКСПЕЙС прогона — той же спавн-фиксацией: задачные
|
|
8059
|
+
// вызовы (read_task, отчёт, тулы Героя) дрон делает НЕ через канал, а прямо в Taskless,
|
|
8060
|
+
// и воркспейс им нужен верно сразу. Без него read_task/отчёт дрона
|
|
8061
|
+
// ушли бы с глобальным пином демона — ровно то, что чинит эта задача. Пусто
|
|
8062
|
+
// (прогон до фичи / sqlite-бэкенд) → ключ не кладём, дрон берёт глобальный пин.
|
|
8063
|
+
// SWARM_HOME (TASWM-289): дрон едет с ТЕНЕВЫМ HOME, в котором `.taskless-swarm`
|
|
8064
|
+
// нет вовсе, — а дрон-MCP из этого же дерева процессов читает oauth.json
|
|
8065
|
+
// (connectTaskless в runDroneServer), пульс овермайнда и workspace.json. Наследовать
|
|
8066
|
+
// ему нечего, поэтому реальный SWARM_HOME кладём явно. Дрон этот конфиг прочитать
|
|
8067
|
+
// может, но нового ему это не даёт: его собственный worktree лежит ВНУТРИ
|
|
8068
|
+
// SWARM_HOME, путь он и так знает (см. «чего этот модуль не делает» в droneHome.ts).
|
|
8069
|
+
env: {
|
|
8070
|
+
...channelEnv,
|
|
8071
|
+
...cfg.droneIsolation ? { SWARM_HOME: cfg.droneIsolation.swarmHome } : {},
|
|
8072
|
+
SWARM_TASK_SLUG: run.task_slug,
|
|
8073
|
+
SWARM_RUN_KIND: run.kind,
|
|
8074
|
+
SWARM_RUN_NAME: run.name,
|
|
8075
|
+
...run.workspace_id ? { SWARM_WORKSPACE_ID: run.workspace_id } : {},
|
|
8076
|
+
...run.reviews_run_id != null ? { SWARM_REVIEWS_RUN_ID: String(run.reviews_run_id) } : {},
|
|
8077
|
+
...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
|
|
7131
8078
|
}
|
|
7132
8079
|
};
|
|
7133
|
-
const
|
|
7134
|
-
|
|
7135
|
-
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
|
|
7136
|
-
const pluginDirs = roleDirs(worktree, run.kind);
|
|
7137
|
-
const realHome = cfg.droneIsolation?.realHome ?? homedir3();
|
|
8080
|
+
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
|
|
8081
|
+
const realHome = cfg.droneIsolation?.realHome ?? homedir4();
|
|
7138
8082
|
const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
|
|
7139
|
-
|
|
7140
|
-
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync7(join9(cfg.droneIsolation.swarmHome, "token"))) {
|
|
8083
|
+
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join12(cfg.droneIsolation.swarmHome, "token"))) {
|
|
7141
8084
|
console.error(
|
|
7142
|
-
`[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${
|
|
8085
|
+
`[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${join12(cfg.droneIsolation.swarmHome, "token")} \u043D\u0435\u0442. read_task \u0434\u0440\u043E\u043D\u0430 \u0431\u0443\u0434\u0435\u0442 slug-only. \u041F\u043E\u0447\u0438\u043D\u0438: \`swarm login\` (oauth.json) \u0438\u043B\u0438 \u043F\u043E\u043B\u043E\u0436\u0438 \u0442\u043E\u043A\u0435\u043D \u0432 \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B.`
|
|
7143
8086
|
);
|
|
7144
8087
|
}
|
|
8088
|
+
const executor = cfg.executor ?? resolveExecutor();
|
|
8089
|
+
const plan = executor.plan({
|
|
8090
|
+
worktree,
|
|
8091
|
+
prompt,
|
|
8092
|
+
continueSession: !!continueSession,
|
|
8093
|
+
model,
|
|
8094
|
+
mcp,
|
|
8095
|
+
skillDirs: roleDirs(worktree, run.kind),
|
|
8096
|
+
// Закрытые дрону пути имеют смысл только при теневом HOME: без него дрон и так
|
|
8097
|
+
// сидит в HOME демона, и запирать половину дверей — самообман.
|
|
8098
|
+
denyPaths: cfg.droneIsolation ? dronePrivatePaths(realHome, cfg.droneIsolation.swarmHome) : [],
|
|
8099
|
+
realHome
|
|
8100
|
+
});
|
|
7145
8101
|
let stdio = "ignore";
|
|
7146
8102
|
let outFd;
|
|
7147
8103
|
let errFd;
|
|
@@ -7151,15 +8107,16 @@ function makeLauncher(cfg) {
|
|
|
7151
8107
|
errFd = fds.err ?? void 0;
|
|
7152
8108
|
stdio = ["ignore", outFd, errFd ?? "ignore"];
|
|
7153
8109
|
}
|
|
7154
|
-
const child = spawn(cfg.
|
|
8110
|
+
const child = spawn(cfg.bin ?? plan.bin, plan.args, {
|
|
7155
8111
|
cwd: worktree,
|
|
7156
8112
|
detached: true,
|
|
7157
8113
|
stdio,
|
|
7158
8114
|
// Пер-worktree TURBO_CACHE_DIR (TASWM-232): дрон не отыгрывает кеш соседнего
|
|
7159
8115
|
// worktree, проверки исполняются реально. Теневой HOME + чистка env от состояния
|
|
7160
8116
|
// демона (TASWM-289). Доля тестовых форков от числа дронов на хосте (TASWM-246).
|
|
7161
|
-
// Остальной env демона наследуется, как раньше.
|
|
7162
|
-
|
|
8117
|
+
// Остальной env демона наследуется, как раньше. Добавки адаптера — последними:
|
|
8118
|
+
// они про его собственное состояние, и перебивать их ройным нечем.
|
|
8119
|
+
env: { ...droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null), ...plan.env }
|
|
7163
8120
|
});
|
|
7164
8121
|
if (outFd !== void 0) closeSync2(outFd);
|
|
7165
8122
|
if (errFd !== void 0) closeSync2(errFd);
|
|
@@ -7173,89 +8130,56 @@ function makeLauncher(cfg) {
|
|
|
7173
8130
|
init_procCleanup();
|
|
7174
8131
|
|
|
7175
8132
|
// src/cost.ts
|
|
7176
|
-
|
|
7177
|
-
import {
|
|
7178
|
-
|
|
7179
|
-
// src/pricing.ts
|
|
7180
|
-
var MODEL_PRICING = {
|
|
7181
|
-
// Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
|
|
7182
|
-
// Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
|
|
7183
|
-
// ветвление по времени внутри расчёта стоимости.
|
|
7184
|
-
"claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
|
|
7185
|
-
"claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
7186
|
-
// Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
|
|
7187
|
-
"claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
7188
|
-
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
|
|
7189
|
-
};
|
|
7190
|
-
var CACHE_READ_FACTOR = 0.1;
|
|
7191
|
-
var CACHE_WRITE_5M_FACTOR = 1.25;
|
|
7192
|
-
function costFromUsage(usage, model) {
|
|
7193
|
-
const pricing = model ? MODEL_PRICING[model] : void 0;
|
|
7194
|
-
if (!pricing) {
|
|
7195
|
-
return { costUsd: 0, warning: `\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u0430\u0439\u0441\u0438\u043D\u0433\u0430: "${model ?? ""}" \u2014 cost \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D` };
|
|
7196
|
-
}
|
|
7197
|
-
const input = usage.input_tokens ?? 0;
|
|
7198
|
-
const output = usage.output_tokens ?? 0;
|
|
7199
|
-
const cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
7200
|
-
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
7201
|
-
const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
|
|
7202
|
-
return { costUsd };
|
|
7203
|
-
}
|
|
7204
|
-
|
|
7205
|
-
// src/claudeResult.ts
|
|
7206
|
-
function parseClaudeResultJson(raw) {
|
|
7207
|
-
const trimmed = raw.trim();
|
|
7208
|
-
if (!trimmed) return null;
|
|
7209
|
-
try {
|
|
7210
|
-
const parsed = JSON.parse(trimmed);
|
|
7211
|
-
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
7212
|
-
} catch {
|
|
7213
|
-
return null;
|
|
7214
|
-
}
|
|
7215
|
-
}
|
|
7216
|
-
function launchCostUsd(result, model) {
|
|
7217
|
-
if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
|
|
7218
|
-
return { costUsd: result.total_cost_usd };
|
|
7219
|
-
}
|
|
7220
|
-
if (!result.usage) return { costUsd: 0 };
|
|
7221
|
-
return costFromUsage(result.usage, model);
|
|
7222
|
-
}
|
|
7223
|
-
|
|
7224
|
-
// src/cost.ts
|
|
8133
|
+
init_executors();
|
|
8134
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync8, renameSync as renameSync7 } from "fs";
|
|
8135
|
+
import { join as join13 } from "path";
|
|
7225
8136
|
var UNPROCESSED_LOG_RE = /^\d+\.json$/;
|
|
7226
8137
|
function unprocessedLogFiles(dir) {
|
|
7227
8138
|
try {
|
|
7228
|
-
return
|
|
8139
|
+
return readdirSync8(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
|
|
7229
8140
|
} catch {
|
|
7230
8141
|
return [];
|
|
7231
8142
|
}
|
|
7232
8143
|
}
|
|
7233
8144
|
function accountRunCost(deps, run) {
|
|
7234
|
-
const dir =
|
|
8145
|
+
const dir = join13(deps.runLogsDir, String(run.id));
|
|
7235
8146
|
const files = unprocessedLogFiles(dir);
|
|
7236
8147
|
if (files.length === 0) return;
|
|
7237
8148
|
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
|
|
8149
|
+
const executor = deps.executor ?? resolveExecutor();
|
|
7238
8150
|
let added = 0;
|
|
8151
|
+
let tokensIn = 0;
|
|
8152
|
+
let tokensOut = 0;
|
|
8153
|
+
let durationMs = 0;
|
|
7239
8154
|
const processed = [];
|
|
7240
8155
|
for (const f of files) {
|
|
7241
|
-
const full =
|
|
8156
|
+
const full = join13(dir, f);
|
|
7242
8157
|
let raw = null;
|
|
7243
8158
|
try {
|
|
7244
8159
|
raw = readFileSync10(full, "utf8");
|
|
7245
8160
|
} catch {
|
|
7246
8161
|
continue;
|
|
7247
8162
|
}
|
|
7248
|
-
const result =
|
|
8163
|
+
const result = executor.parseResult(raw, model);
|
|
7249
8164
|
if (!result) {
|
|
7250
8165
|
console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: \u043D\u0435 \u0440\u0430\u0441\u043F\u0430\u0440\u0441\u0438\u043B\u0441\u044F ${full} (\u043F\u0440\u043E\u0433\u043E\u043D \u0443\u043C\u0435\u0440 \u0434\u043E \u0437\u0430\u043F\u0438\u0441\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430?)`);
|
|
7251
8166
|
} else {
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
8167
|
+
if (result.warning) console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${result.warning}`);
|
|
8168
|
+
added += result.costUsd;
|
|
8169
|
+
tokensIn += result.tokensIn ?? 0;
|
|
8170
|
+
tokensOut += result.tokensOut ?? 0;
|
|
8171
|
+
durationMs += result.durationMs ?? 0;
|
|
7255
8172
|
}
|
|
7256
8173
|
processed.push(full);
|
|
7257
8174
|
}
|
|
7258
|
-
if (added > 0
|
|
8175
|
+
if (added > 0 || tokensIn > 0 || tokensOut > 0 || durationMs > 0) {
|
|
8176
|
+
deps.db.updateRun(run.id, {
|
|
8177
|
+
cost_usd: run.cost_usd + added,
|
|
8178
|
+
tokens_in: run.tokens_in + tokensIn,
|
|
8179
|
+
tokens_out: run.tokens_out + tokensOut,
|
|
8180
|
+
duration_ms: run.duration_ms + durationMs
|
|
8181
|
+
});
|
|
8182
|
+
}
|
|
7259
8183
|
for (const full of processed) {
|
|
7260
8184
|
try {
|
|
7261
8185
|
renameSync7(full, `${full}.done`);
|
|
@@ -7268,14 +8192,15 @@ function makeCostAccountant(deps) {
|
|
|
7268
8192
|
}
|
|
7269
8193
|
|
|
7270
8194
|
// src/runOutcome.ts
|
|
7271
|
-
|
|
7272
|
-
import {
|
|
8195
|
+
init_executors();
|
|
8196
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync11 } from "fs";
|
|
8197
|
+
import { join as join14 } from "path";
|
|
7273
8198
|
var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
|
|
7274
|
-
function classifyExit(runLogsDir, runId) {
|
|
7275
|
-
const dir =
|
|
8199
|
+
function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
|
|
8200
|
+
const dir = join14(runLogsDir, String(runId));
|
|
7276
8201
|
let files;
|
|
7277
8202
|
try {
|
|
7278
|
-
files =
|
|
8203
|
+
files = readdirSync9(dir);
|
|
7279
8204
|
} catch {
|
|
7280
8205
|
return "unknown";
|
|
7281
8206
|
}
|
|
@@ -7289,14 +8214,15 @@ function classifyExit(runLogsDir, runId) {
|
|
|
7289
8214
|
if (!latest) return "unknown";
|
|
7290
8215
|
let raw;
|
|
7291
8216
|
try {
|
|
7292
|
-
raw = readFileSync11(
|
|
8217
|
+
raw = readFileSync11(join14(dir, latest.name), "utf8");
|
|
7293
8218
|
} catch {
|
|
7294
8219
|
return "unknown";
|
|
7295
8220
|
}
|
|
7296
|
-
return
|
|
8221
|
+
return executor.parseResult(raw, null) ? "graceful" : "crash";
|
|
7297
8222
|
}
|
|
7298
|
-
function makeExitClassifier(runLogsDir) {
|
|
7299
|
-
|
|
8223
|
+
function makeExitClassifier(runLogsDir, executor) {
|
|
8224
|
+
const exec = executor ?? resolveExecutor();
|
|
8225
|
+
return (run) => classifyExit(runLogsDir, run.id, exec);
|
|
7300
8226
|
}
|
|
7301
8227
|
|
|
7302
8228
|
// src/orchestrator.ts
|
|
@@ -7304,8 +8230,8 @@ init_src();
|
|
|
7304
8230
|
init_git();
|
|
7305
8231
|
|
|
7306
8232
|
// src/deps.ts
|
|
7307
|
-
import { existsSync as
|
|
7308
|
-
import { join as
|
|
8233
|
+
import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync12 } from "fs";
|
|
8234
|
+
import { join as join16 } from "path";
|
|
7309
8235
|
import { execa } from "execa";
|
|
7310
8236
|
var PREPARE_TIMEOUT_MS = 15 * 6e4;
|
|
7311
8237
|
var MAX_PLAN_COMMANDS = 12;
|
|
@@ -7328,14 +8254,14 @@ function safeReadText(p) {
|
|
|
7328
8254
|
}
|
|
7329
8255
|
function safeListDirs(p) {
|
|
7330
8256
|
try {
|
|
7331
|
-
return
|
|
8257
|
+
return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
7332
8258
|
} catch {
|
|
7333
8259
|
return [];
|
|
7334
8260
|
}
|
|
7335
8261
|
}
|
|
7336
8262
|
function safeListFiles(p) {
|
|
7337
8263
|
try {
|
|
7338
|
-
return
|
|
8264
|
+
return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
|
|
7339
8265
|
} catch {
|
|
7340
8266
|
return [];
|
|
7341
8267
|
}
|
|
@@ -7361,21 +8287,21 @@ function overridePlan(override) {
|
|
|
7361
8287
|
};
|
|
7362
8288
|
}
|
|
7363
8289
|
function detectPackageManager(worktree, io = {}) {
|
|
7364
|
-
const exists = io.exists ??
|
|
8290
|
+
const exists = io.exists ?? existsSync10;
|
|
7365
8291
|
const readText = io.readText ?? safeReadText;
|
|
7366
|
-
const hasPkgJson = exists(
|
|
8292
|
+
const hasPkgJson = exists(join16(worktree, "package.json"));
|
|
7367
8293
|
if (!hasPkgJson) {
|
|
7368
|
-
if (exists(
|
|
8294
|
+
if (exists(join16(worktree, "deno.json")) || exists(join16(worktree, "deno.jsonc")) || exists(join16(worktree, "deno.lock"))) return "deno";
|
|
7369
8295
|
return null;
|
|
7370
8296
|
}
|
|
7371
|
-
const pkg = readJson(readText,
|
|
8297
|
+
const pkg = readJson(readText, join16(worktree, "package.json"));
|
|
7372
8298
|
const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
|
|
7373
8299
|
if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
|
|
7374
|
-
if (exists(
|
|
7375
|
-
if (exists(
|
|
7376
|
-
if (exists(
|
|
7377
|
-
if (exists(
|
|
7378
|
-
if (exists(
|
|
8300
|
+
if (exists(join16(worktree, "bun.lock")) || exists(join16(worktree, "bun.lockb"))) return "bun";
|
|
8301
|
+
if (exists(join16(worktree, "pnpm-lock.yaml"))) return "pnpm";
|
|
8302
|
+
if (exists(join16(worktree, "yarn.lock"))) return "yarn";
|
|
8303
|
+
if (exists(join16(worktree, "package-lock.json"))) return "npm";
|
|
8304
|
+
if (exists(join16(worktree, "deno.lock"))) return "deno";
|
|
7379
8305
|
return "npm";
|
|
7380
8306
|
}
|
|
7381
8307
|
function jsInstallCommand(pm) {
|
|
@@ -7428,7 +8354,7 @@ var ECOSYSTEM_MARKERS = [
|
|
|
7428
8354
|
// конфигурация — дело сборки, не подготовки
|
|
7429
8355
|
];
|
|
7430
8356
|
function detectPlan(worktree, io) {
|
|
7431
|
-
const exists = io.exists ??
|
|
8357
|
+
const exists = io.exists ?? existsSync10;
|
|
7432
8358
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
7433
8359
|
const pm = detectPackageManager(worktree, io);
|
|
7434
8360
|
if (pm) {
|
|
@@ -7436,7 +8362,7 @@ function detectPlan(worktree, io) {
|
|
|
7436
8362
|
// node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
|
|
7437
8363
|
// У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
|
|
7438
8364
|
// заново, но он дешёвый: тянет из того же общего кэша.
|
|
7439
|
-
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists:
|
|
8365
|
+
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join16(worktree, "node_modules"), source: "detect" }
|
|
7440
8366
|
];
|
|
7441
8367
|
for (const target of workspaceBuildTargets(worktree, io)) {
|
|
7442
8368
|
commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
|
|
@@ -7444,12 +8370,12 @@ function detectPlan(worktree, io) {
|
|
|
7444
8370
|
return { source: "detect", ecosystem: `node/${pm}`, commands };
|
|
7445
8371
|
}
|
|
7446
8372
|
for (const marker of ECOSYSTEM_MARKERS) {
|
|
7447
|
-
const hit = marker.files.some((f) => exists(
|
|
8373
|
+
const hit = marker.files.some((f) => exists(join16(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
|
|
7448
8374
|
if (!hit) continue;
|
|
7449
8375
|
let ecosystem = marker.ecosystem;
|
|
7450
8376
|
let command = marker.command;
|
|
7451
8377
|
for (const refine of marker.refine ?? []) {
|
|
7452
|
-
if (!exists(
|
|
8378
|
+
if (!exists(join16(worktree, refine.file))) continue;
|
|
7453
8379
|
ecosystem = refine.ecosystem;
|
|
7454
8380
|
command = refine.command;
|
|
7455
8381
|
break;
|
|
@@ -7506,12 +8432,12 @@ var CI_INSTALL_PATTERNS = [
|
|
|
7506
8432
|
function ciWorkflowPlan(worktree, io) {
|
|
7507
8433
|
const readText = io.readText ?? safeReadText;
|
|
7508
8434
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
7509
|
-
const dir =
|
|
8435
|
+
const dir = join16(worktree, ".github", "workflows");
|
|
7510
8436
|
const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
|
|
7511
8437
|
const seen = /* @__PURE__ */ new Set();
|
|
7512
8438
|
const commands = [];
|
|
7513
8439
|
for (const name of files.sort()) {
|
|
7514
|
-
const yaml = readText(
|
|
8440
|
+
const yaml = readText(join16(dir, name));
|
|
7515
8441
|
if (yaml === null) continue;
|
|
7516
8442
|
for (const raw of workflowRunLines(yaml)) {
|
|
7517
8443
|
const line = raw.replace(/\s+/g, " ").trim();
|
|
@@ -7526,7 +8452,7 @@ function ciWorkflowPlan(worktree, io) {
|
|
|
7526
8452
|
return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
|
|
7527
8453
|
}
|
|
7528
8454
|
function workspaceGlobs(worktree, readText) {
|
|
7529
|
-
const yaml = readText(
|
|
8455
|
+
const yaml = readText(join16(worktree, "pnpm-workspace.yaml"));
|
|
7530
8456
|
if (yaml !== null) {
|
|
7531
8457
|
const globs = [];
|
|
7532
8458
|
let inPackages = false;
|
|
@@ -7546,7 +8472,7 @@ function workspaceGlobs(worktree, readText) {
|
|
|
7546
8472
|
}
|
|
7547
8473
|
if (globs.length) return globs;
|
|
7548
8474
|
}
|
|
7549
|
-
const pkg = readJson(readText,
|
|
8475
|
+
const pkg = readJson(readText, join16(worktree, "package.json"));
|
|
7550
8476
|
const ws = pkg?.workspaces;
|
|
7551
8477
|
const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
|
|
7552
8478
|
return list.filter((g) => typeof g === "string");
|
|
@@ -7557,14 +8483,14 @@ function workspaceDirs(worktree, globs, io) {
|
|
|
7557
8483
|
if (glob.startsWith("!")) continue;
|
|
7558
8484
|
const star = glob.indexOf("*");
|
|
7559
8485
|
if (star === -1) {
|
|
7560
|
-
const dir =
|
|
7561
|
-
if (io.exists(
|
|
8486
|
+
const dir = join16(worktree, glob);
|
|
8487
|
+
if (io.exists(join16(dir, "package.json"))) dirs.add(dir);
|
|
7562
8488
|
continue;
|
|
7563
8489
|
}
|
|
7564
|
-
const base3 =
|
|
8490
|
+
const base3 = join16(worktree, glob.slice(0, star).replace(/\/$/, ""));
|
|
7565
8491
|
for (const name of io.listDirs(base3)) {
|
|
7566
|
-
const dir =
|
|
7567
|
-
if (io.exists(
|
|
8492
|
+
const dir = join16(base3, name);
|
|
8493
|
+
if (io.exists(join16(dir, "package.json"))) dirs.add(dir);
|
|
7568
8494
|
}
|
|
7569
8495
|
}
|
|
7570
8496
|
return [...dirs];
|
|
@@ -7585,18 +8511,18 @@ function entryPaths(pkg) {
|
|
|
7585
8511
|
return out;
|
|
7586
8512
|
}
|
|
7587
8513
|
function workspaceBuildTargets(worktree, io = {}) {
|
|
7588
|
-
const exists = io.exists ??
|
|
8514
|
+
const exists = io.exists ?? existsSync10;
|
|
7589
8515
|
const readText = io.readText ?? safeReadText;
|
|
7590
8516
|
const listDirs = io.listDirs ?? safeListDirs;
|
|
7591
8517
|
const globs = workspaceGlobs(worktree, readText);
|
|
7592
8518
|
if (!globs.length) return [];
|
|
7593
8519
|
const pkgs = [];
|
|
7594
8520
|
for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
|
|
7595
|
-
const pkg = readJson(readText,
|
|
8521
|
+
const pkg = readJson(readText, join16(dir, "package.json"));
|
|
7596
8522
|
if (!pkg || typeof pkg.name !== "string") continue;
|
|
7597
8523
|
const scripts = pkg.scripts ?? {};
|
|
7598
8524
|
const entries = entryPaths(pkg);
|
|
7599
|
-
const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(
|
|
8525
|
+
const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join16(dir, e)));
|
|
7600
8526
|
const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
|
|
7601
8527
|
pkgs.push({ name: pkg.name, dir, deps, needsBuild });
|
|
7602
8528
|
}
|
|
@@ -7624,7 +8550,7 @@ function planPrepare(worktree, io = {}) {
|
|
|
7624
8550
|
return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
|
|
7625
8551
|
}
|
|
7626
8552
|
async function ensureWorktreeDeps(worktree, io = {}) {
|
|
7627
|
-
const exists = io.exists ??
|
|
8553
|
+
const exists = io.exists ?? existsSync10;
|
|
7628
8554
|
const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
|
|
7629
8555
|
const approve = io.approve ?? (() => true);
|
|
7630
8556
|
const plan = planPrepare(worktree, io);
|
|
@@ -7715,7 +8641,9 @@ var BaseBranchMissingError = class extends Error {
|
|
|
7715
8641
|
branch;
|
|
7716
8642
|
repo;
|
|
7717
8643
|
};
|
|
7718
|
-
var
|
|
8644
|
+
var SpawnDeferredError = class extends Error {
|
|
8645
|
+
};
|
|
8646
|
+
var AreaBusyError = class extends SpawnDeferredError {
|
|
7719
8647
|
constructor(area) {
|
|
7720
8648
|
super(`\u0437\u043E\u043D\u0430 ${area} \u0437\u0430\u043D\u044F\u0442\u0430 \u0434\u0440\u0443\u0433\u0438\u043C \u0434\u0440\u043E\u043D\u043E\u043C \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0441\u0442\u0430\u0451\u0442\u0441\u044F \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438`);
|
|
7721
8649
|
this.area = area;
|
|
@@ -7723,6 +8651,14 @@ var AreaBusyError = class extends Error {
|
|
|
7723
8651
|
}
|
|
7724
8652
|
area;
|
|
7725
8653
|
};
|
|
8654
|
+
var FileOverlapError = class extends SpawnDeferredError {
|
|
8655
|
+
constructor(message, blockedByRunId) {
|
|
8656
|
+
super(message);
|
|
8657
|
+
this.blockedByRunId = blockedByRunId;
|
|
8658
|
+
this.name = "FileOverlapError";
|
|
8659
|
+
}
|
|
8660
|
+
blockedByRunId;
|
|
8661
|
+
};
|
|
7726
8662
|
function healTaskSlug(db, run, task) {
|
|
7727
8663
|
const slug = task?.slug;
|
|
7728
8664
|
if (!slug || slug === run.task_slug || !isTaskSlug(slug) || isTaskSlug(run.task_slug)) return run;
|
|
@@ -7732,7 +8668,9 @@ function healTaskSlug(db, run, task) {
|
|
|
7732
8668
|
async function spawnDrone(deps, run) {
|
|
7733
8669
|
const repo = normalizeRepo(run.repo || deps.repo || "");
|
|
7734
8670
|
if (!repo) throw new Error(`\u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} (${run.task_slug}) \u043D\u0435\u0442 repo \u0438 \u043D\u0435\u0442 \u0434\u0435\u0444\u043E\u043B\u0442\u0430 SWARM_REPO`);
|
|
7735
|
-
|
|
8671
|
+
const conflict = deps.conflicts ? await deps.conflicts(run, repo) : { kind: "unknown" };
|
|
8672
|
+
if (conflict.kind === "wait") throw new FileOverlapError(conflict.message, conflict.blockedBy.id);
|
|
8673
|
+
if (conflict.kind === "unknown" && run.area && deps.db.runningAreas(repo).includes(run.area)) {
|
|
7736
8674
|
throw new AreaBusyError(run.area);
|
|
7737
8675
|
}
|
|
7738
8676
|
const branch = run.task_slug;
|
|
@@ -7783,13 +8721,13 @@ function isGithubPrUrl(url) {
|
|
|
7783
8721
|
return GH_PR_RE.test(url.trim());
|
|
7784
8722
|
}
|
|
7785
8723
|
function softStatus() {
|
|
7786
|
-
return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [] };
|
|
8724
|
+
return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [], headBranch: null };
|
|
7787
8725
|
}
|
|
7788
8726
|
async function prStatus(prUrl, run = realRunner) {
|
|
7789
8727
|
if (!isGithubPrUrl(prUrl)) return softStatus();
|
|
7790
8728
|
let out;
|
|
7791
8729
|
try {
|
|
7792
|
-
out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files"]);
|
|
8730
|
+
out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files,headRefName"]);
|
|
7793
8731
|
} catch {
|
|
7794
8732
|
return softStatus();
|
|
7795
8733
|
}
|
|
@@ -7810,9 +8748,20 @@ async function prStatus(prUrl, run = realRunner) {
|
|
|
7810
8748
|
// не смержен, открыт и GitHub уверенно говорит CONFLICTING
|
|
7811
8749
|
conflicting: !merged && state === "OPEN" && mergeable === "CONFLICTING",
|
|
7812
8750
|
isDraft: j.isDraft === true,
|
|
7813
|
-
files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : []
|
|
8751
|
+
files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : [],
|
|
8752
|
+
headBranch: typeof j.headRefName === "string" && j.headRefName ? j.headRefName : null
|
|
7814
8753
|
};
|
|
7815
8754
|
}
|
|
8755
|
+
async function prHeadBranch(prUrl, run = realRunner) {
|
|
8756
|
+
if (!isGithubPrUrl(prUrl)) return null;
|
|
8757
|
+
try {
|
|
8758
|
+
const out = await run(["pr", "view", prUrl, "--json", "headRefName"]);
|
|
8759
|
+
const j = JSON.parse(out);
|
|
8760
|
+
return typeof j.headRefName === "string" && j.headRefName ? j.headRefName : null;
|
|
8761
|
+
} catch {
|
|
8762
|
+
return null;
|
|
8763
|
+
}
|
|
8764
|
+
}
|
|
7816
8765
|
async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
|
|
7817
8766
|
if (!branch || !repoDir) return [];
|
|
7818
8767
|
try {
|
|
@@ -8020,6 +8969,13 @@ function reviewTransitionPatch(run, prUrl) {
|
|
|
8020
8969
|
}
|
|
8021
8970
|
return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
|
|
8022
8971
|
}
|
|
8972
|
+
function branchFromPrPatch(currentBranch, headBranch) {
|
|
8973
|
+
if (!headBranch || headBranch === currentBranch) return null;
|
|
8974
|
+
return {
|
|
8975
|
+
patch: { branch: headBranch },
|
|
8976
|
+
event: currentBranch ? `\u0432\u0435\u0442\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0432\u0437\u044F\u0442\u0430 \u0443 PR: ${currentBranch} \u2192 ${headBranch} (\u043F\u043E\u043B\u0435 \u0431\u044B\u043B\u043E \u0441\u043E\u0431\u0440\u0430\u043D\u043E \u0438\u0437 \u0441\u043B\u0430\u0433\u0430 \u2014 TASWM-356)` : `\u0432\u0435\u0442\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0432\u0437\u044F\u0442\u0430 \u0443 PR: ${headBranch} (TASWM-356)`
|
|
8977
|
+
};
|
|
8978
|
+
}
|
|
8023
8979
|
function applyReviewTransition(db, run, prUrl) {
|
|
8024
8980
|
const { patch, ...t } = reviewTransitionPatch(run, prUrl);
|
|
8025
8981
|
db.updateRun(run.id, patch);
|
|
@@ -8028,14 +8984,14 @@ function applyReviewTransition(db, run, prUrl) {
|
|
|
8028
8984
|
|
|
8029
8985
|
// src/droneServer.ts
|
|
8030
8986
|
init_src();
|
|
8031
|
-
function freezeInstruction(
|
|
8987
|
+
function freezeInstruction(branch, reason = "\u0435\u0433\u043E \u043F\u0443\u043B\u044C\u0441 (heartbeat) \u0443\u0441\u0442\u0430\u0440\u0435\u043B") {
|
|
8032
8988
|
return [
|
|
8033
8989
|
`\u26D4 OVERMIND \u041D\u0415\u0414\u041E\u0421\u0422\u0423\u041F\u0415\u041D \u2014 ${reason}. \u041D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0430\u0434 \u0442\u043E\u0431\u043E\u0439 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0442: \u043D\u0435\u043A\u043E\u043C\u0443`,
|
|
8034
8990
|
"\u043F\u0440\u0438\u043D\u044F\u0442\u044C \u0442\u0432\u043E\u0439 \u0432\u043E\u043F\u0440\u043E\u0441, \u0437\u0430\u043C\u0435\u0442\u0438\u0442\u044C \u0441\u043C\u0435\u0440\u0442\u044C, \u0441\u043C\u0435\u0440\u0436\u0438\u0442\u044C PR \u0438\u043B\u0438 \u0440\u0430\u0437\u0440\u0443\u043B\u0438\u0442\u044C \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442. \u0420\u0430\u0431\u043E\u0442\u0430\u0442\u044C \u0432\u0441\u043B\u0435\u043F\u0443\u044E \u043D\u0435\u043B\u044C\u0437\u044F.",
|
|
8035
8991
|
"\u0417\u0410\u041C\u0420\u0418 \u0438 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0441\u044C, \u0447\u0442\u043E\u0431\u044B \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u043F\u0440\u043E\u043F\u0430\u043B\u0430:",
|
|
8036
8992
|
"1. \u0417\u0430\u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0412\u0421\u042E \u0442\u0435\u043A\u0443\u0449\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043A\u0430\u043A \u0435\u0441\u0442\u044C: `git add -A && git commit` (\u0442\u0440\u0435\u0439\u043B\u0435\u0440 \u043F\u0440\u043E \u0434\u0440\u043E\u043D\u0430 \u2014 \u043A\u0430\u043A \u043E\u0431\u044B\u0447\u043D\u043E).",
|
|
8037
8993
|
" \u041D\u043E\u0432\u044B\u0439 \u043A\u043E\u0434 \u041D\u0415 \u043F\u0438\u0448\u0438, \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u0430\u043B\u044C\u0448\u0435 \u041D\u0415 \u0434\u0435\u043B\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u043E\u0445\u0440\u0430\u043D\u0438 \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043D\u043E\u0435, \u0431\u0435\u0437 \u043C\u0443\u0441\u043E\u0440\u043D\u044B\u0445 \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432.",
|
|
8038
|
-
`2. \u0417\u0430\u043F\u0443\u0448\u044C \u0432\u0435\u0442\u043A\u0443: \`git push -u origin ${
|
|
8994
|
+
`2. \u0417\u0430\u043F\u0443\u0448\u044C \u0432\u0435\u0442\u043A\u0443: \`git push -u origin ${branch}\` \u2014 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0440\u0451\u0442 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u043C.`,
|
|
8039
8995
|
"3. \u0417\u0430\u0432\u0435\u0440\u0448\u0438 \u0440\u0430\u0431\u043E\u0442\u0443 \u2014 \u0431\u043E\u043B\u044C\u0448\u0435 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u0432 \u041D\u0415 \u0437\u043E\u0432\u0438 (\u043D\u0438 report_step, \u043D\u0438 ask, \u043D\u0438 done). \u041F\u0440\u043E\u0441\u0442\u043E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438 \u0441\u0435\u0441\u0441\u0438\u044E.",
|
|
8040
8996
|
"\u041F\u0440\u043E\u0433\u043E\u043D \u043F\u043E\u043C\u0435\u0447\u0435\u043D \xABpaused\xBB. \u041A\u043E\u0433\u0434\u0430 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F \u043D\u043E\u0432\u044B\u0439 overmind, \u043E\u043D \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442 \u0442\u0435\u0431\u044F \u0441 \u044D\u0442\u043E\u0433\u043E \u043C\u0435\u0441\u0442\u0430 (--continue) \u2014",
|
|
8041
8997
|
"\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0448\u044C \u043E\u0442\u0442\u0443\u0434\u0430. \u041D\u0435 \u043F\u0435\u0440\u0435\u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0439 \u0438 \u043D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0437\u0430\u0434\u0430\u0447\u0443 \u0441\u0435\u0439\u0447\u0430\u0441."
|
|
@@ -8068,10 +9024,12 @@ async function buildDroneServer(store, opts = {}) {
|
|
|
8068
9024
|
const slugForInstruction = (fallback) => opts.expectedTaskSlug ?? fallback ?? "<slug>";
|
|
8069
9025
|
const freeze = async (withdraw = false) => {
|
|
8070
9026
|
let slug;
|
|
9027
|
+
let branch;
|
|
8071
9028
|
try {
|
|
8072
9029
|
if (withdraw) await store.withdrawMyQuestion("overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u0437\u0430\u043C\u0435\u0440, \u0432\u043E\u043F\u0440\u043E\u0441 \u0441\u043D\u044F\u0442");
|
|
8073
9030
|
const run = await store.myRun();
|
|
8074
9031
|
slug = run?.task_slug;
|
|
9032
|
+
branch = !opts.expectedTaskSlug || opts.expectedTaskSlug === run?.task_slug ? run?.branch : null;
|
|
8075
9033
|
if (run && run.status !== "paused") {
|
|
8076
9034
|
await store.updateRun(null, { status: "paused" });
|
|
8077
9035
|
await store.addEvent(null, "paused", "overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u0437\u0430\u043C\u0435\u0440, \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 origin, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430");
|
|
@@ -8079,7 +9037,7 @@ async function buildDroneServer(store, opts = {}) {
|
|
|
8079
9037
|
} catch (e) {
|
|
8080
9038
|
console.error("[overmind-drone] \u043F\u043E\u043C\u0435\u0442\u0438\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D paused \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C (\u043D\u0430\u0434\u0437\u043E\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D):", e);
|
|
8081
9039
|
}
|
|
8082
|
-
return { content: [{ type: "text", text: freezeInstruction(slugForInstruction(slug), opts.deadReason) }] };
|
|
9040
|
+
return { content: [{ type: "text", text: freezeInstruction(branch || slugForInstruction(slug), opts.deadReason) }] };
|
|
8083
9041
|
};
|
|
8084
9042
|
const guard = (fn) => async (a) => {
|
|
8085
9043
|
try {
|
|
@@ -8103,7 +9061,7 @@ async function buildDroneServer(store, opts = {}) {
|
|
|
8103
9061
|
);
|
|
8104
9062
|
server.tool(
|
|
8105
9063
|
"read_task",
|
|
8106
|
-
"\u0421\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0442\u0432\u043E\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u041F\u0420\u042F\u041C\u041E \u0421\u0415\u0419\u0427\u0410\u0421: \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 + \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (\u0430\u0432\u0442\u043E\u0440+\u0432\u0440\u0435\u043C\u044F+\u0442\u0435\u043A\u0441\u0442) + \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0445 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432 (\u0441\u043F\u0435\u043A\u0438 \u043E\u0431\u044B\u0447\u043D\u043E \u043B\u0435\u0436\u0430\u0442 \u0438\u043C\u0435\u043D\u043D\u043E \u0442\u0430\u043C). \u0411\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432. \u0417\u043E\u0432\u0438 \u043F\u0435\u0440\u0435\u0434 done() \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0433 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0443\u0436\u0435 \u043F\u043E\u0441\u043B\u0435 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430, \u0430 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0443\u0432\u0438\u0434\u0438\u0442 (\u043E\u043D \u0441\u043E\u0431\u0440\u0430\u043D \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435).",
|
|
9064
|
+
"\u0421\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0442\u0432\u043E\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u041F\u0420\u042F\u041C\u041E \u0421\u0415\u0419\u0427\u0410\u0421: \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 + \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 + \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (\u0430\u0432\u0442\u043E\u0440+\u0432\u0440\u0435\u043C\u044F+\u0442\u0435\u043A\u0441\u0442) + \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0445 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432 (\u0441\u043F\u0435\u043A\u0438 \u043E\u0431\u044B\u0447\u043D\u043E \u043B\u0435\u0436\u0430\u0442 \u0438\u043C\u0435\u043D\u043D\u043E \u0442\u0430\u043C) + \u0447\u0435\u043C \u043A\u043E\u043D\u0447\u0438\u043B\u0438\u0441\u044C \u0441\u043E\u0441\u0435\u0434\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0442\u043E\u0439 \u0436\u0435 \u0437\u043E\u043D\u0435 \u043A\u043E\u0434\u0430 \u0438 \u043A\u0442\u043E \u0438\u0437 \u0440\u043E\u044F \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0440\u044F\u0434\u043E\u043C. \u0411\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432. \u0417\u043E\u0432\u0438 \u043F\u0435\u0440\u0435\u0434 done() \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0433 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0443\u0436\u0435 \u043F\u043E\u0441\u043B\u0435 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430, \u0430 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0443\u0432\u0438\u0434\u0438\u0442 (\u043E\u043D \u0441\u043E\u0431\u0440\u0430\u043D \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435).",
|
|
8107
9065
|
{},
|
|
8108
9066
|
async () => {
|
|
8109
9067
|
if (!opts.taskless) {
|
|
@@ -8316,9 +9274,12 @@ ${report}${shots.warning}`,
|
|
|
8316
9274
|
await store.addEvent(null, "step", `\u043E\u0442\u0447\u0451\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 ${taskSlug} \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${e instanceof Error ? e.message : String(e)}`);
|
|
8317
9275
|
}
|
|
8318
9276
|
};
|
|
9277
|
+
const headBranch = opts.prHeadBranch ? await opts.prHeadBranch(pr_url).catch(() => null) : null;
|
|
9278
|
+
const branchFix = branchFromPrPatch(run?.branch, headBranch);
|
|
9279
|
+
if (branchFix) await store.addEvent(null, "step", branchFix.event);
|
|
8319
9280
|
if (run) {
|
|
8320
9281
|
const t = reviewTransitionPatch(run, pr_url);
|
|
8321
|
-
await store.updateRun(null, t.patch);
|
|
9282
|
+
await store.updateRun(null, { ...t.patch, ...branchFix?.patch ?? {} });
|
|
8322
9283
|
await store.addEvent(null, "pr_opened", pr_url);
|
|
8323
9284
|
await postReport(reportSlug(run.task_slug), t.cycle ?? run.review_cycle ?? 0);
|
|
8324
9285
|
if (t.isFixIteration) {
|
|
@@ -8331,7 +9292,7 @@ ${report}${shots.warning}`,
|
|
|
8331
9292
|
}
|
|
8332
9293
|
return { content: [{ type: "text", text: `\u043F\u0440\u0438\u043D\u044F\u0442\u043E, \u0443\u0448\u043B\u043E \u043D\u0430 \u0440\u0435\u0432\u044C\u044E${shotsNote(shots)}` }] };
|
|
8333
9294
|
}
|
|
8334
|
-
await store.updateRun(null, { status: "review", pr_url });
|
|
9295
|
+
await store.updateRun(null, { status: "review", pr_url, ...branchFix?.patch ?? {} });
|
|
8335
9296
|
await store.addEvent(null, "pr_opened", pr_url);
|
|
8336
9297
|
const fallbackSlug = reportSlug(void 0);
|
|
8337
9298
|
if (fallbackSlug) await postReport(fallbackSlug, void 0);
|
|
@@ -8496,7 +9457,7 @@ async function runDroneServer(runId) {
|
|
|
8496
9457
|
);
|
|
8497
9458
|
const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
|
|
8498
9459
|
const expectedKind = toRunKind(process.env.SWARM_RUN_KIND);
|
|
8499
|
-
const common = { taskless, runKind: expectedKind, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
|
|
9460
|
+
const common = { taskless, runKind: expectedKind, prHeadBranch, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
|
|
8500
9461
|
const channelUrl = process.env.SWARM_DRONE_CHANNEL_URL;
|
|
8501
9462
|
const channelToken = process.env.SWARM_DRONE_CHANNEL_TOKEN;
|
|
8502
9463
|
if (channelUrl && channelToken) {
|
|
@@ -8589,12 +9550,15 @@ var AnomalyTracker = class {
|
|
|
8589
9550
|
for (const [k, v] of Object.entries(restored.since)) this.since.set(k, v);
|
|
8590
9551
|
for (const [k, v] of Object.entries(restored.lastFiredAt)) this.lastFiredAt.set(k, v);
|
|
8591
9552
|
for (const [k, v] of Object.entries(restored.fires)) this.fires.set(k, v);
|
|
9553
|
+
for (const k of restored.resolved ?? []) this.resolved.add(k);
|
|
8592
9554
|
}
|
|
8593
9555
|
}
|
|
8594
9556
|
renotifyCooldownMs;
|
|
8595
9557
|
since = /* @__PURE__ */ new Map();
|
|
8596
9558
|
lastFiredAt = /* @__PURE__ */ new Map();
|
|
8597
9559
|
fires = /* @__PURE__ */ new Map();
|
|
9560
|
+
/** TAS-485: очередь «уже звонили, а условие ушло» — к гашению в инбоксе. */
|
|
9561
|
+
resolved = /* @__PURE__ */ new Set();
|
|
8598
9562
|
maxRepeats;
|
|
8599
9563
|
io;
|
|
8600
9564
|
/** Кандидаты, готовые к сигналу (отлежали minDurationMs, не в кулдауне) — НИЧЕГО не коммитит. */
|
|
@@ -8615,6 +9579,7 @@ var AnomalyTracker = class {
|
|
|
8615
9579
|
for (const map of [this.since, this.lastFiredAt, this.fires]) {
|
|
8616
9580
|
for (const k of [...map.keys()]) {
|
|
8617
9581
|
if (!seen.has(k)) {
|
|
9582
|
+
if (map === this.lastFiredAt) this.resolved.add(k);
|
|
8618
9583
|
map.delete(k);
|
|
8619
9584
|
pruned = true;
|
|
8620
9585
|
}
|
|
@@ -8627,8 +9592,25 @@ var AnomalyTracker = class {
|
|
|
8627
9592
|
commit(key, now) {
|
|
8628
9593
|
this.lastFiredAt.set(key, now);
|
|
8629
9594
|
this.fires.set(key, (this.fires.get(key) ?? 0) + 1);
|
|
9595
|
+
this.resolved.delete(key);
|
|
8630
9596
|
this.persist();
|
|
8631
9597
|
}
|
|
9598
|
+
/**
|
|
9599
|
+
* TAS-485: ключи, чьё условие ушло ПОСЛЕ того, как о нём звонили, — их записи
|
|
9600
|
+
* в инбоксе пора погасить. Список только ЧИТАЕТСЯ: вычёркивает доставленное
|
|
9601
|
+
* вызывающий (`forgetResolved`) и только после успеха — та же дисциплина, что
|
|
9602
|
+
* у eligible/commit. Проглоти мы сбой доставки как успех, тревога о том, чего
|
|
9603
|
+
* нет, осталась бы висеть навсегда и молча.
|
|
9604
|
+
*/
|
|
9605
|
+
pendingResolved() {
|
|
9606
|
+
return [...this.resolved];
|
|
9607
|
+
}
|
|
9608
|
+
/** Гашение по этим ключам доставлено — забываем их. */
|
|
9609
|
+
forgetResolved(keys) {
|
|
9610
|
+
let changed = false;
|
|
9611
|
+
for (const k of keys) changed = this.resolved.delete(k) || changed;
|
|
9612
|
+
if (changed) this.persist();
|
|
9613
|
+
}
|
|
8632
9614
|
/**
|
|
8633
9615
|
* Следующий сигнал по этому ключу — ПОСЛЕДНИЙ разрешённый в эпизоде: вызывающий меняет
|
|
8634
9616
|
* формулировку (LAST_REMINDER_SUFFIX), чтобы затихание не выглядело как «рой забыл».
|
|
@@ -8644,7 +9626,8 @@ var AnomalyTracker = class {
|
|
|
8644
9626
|
this.io.save({
|
|
8645
9627
|
since: Object.fromEntries(this.since),
|
|
8646
9628
|
lastFiredAt: Object.fromEntries(this.lastFiredAt),
|
|
8647
|
-
fires: Object.fromEntries(this.fires)
|
|
9629
|
+
fires: Object.fromEntries(this.fires),
|
|
9630
|
+
resolved: [...this.resolved]
|
|
8648
9631
|
});
|
|
8649
9632
|
} catch (e) {
|
|
8650
9633
|
console.error("[anomalies] \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u043D\u0430 \u0434\u0438\u0441\u043A:", e);
|
|
@@ -8698,8 +9681,9 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8698
9681
|
const changedAt = sqliteMs(run.updated_at);
|
|
8699
9682
|
return changedAt >= q.from && (q.to == null || changedAt <= q.to);
|
|
8700
9683
|
};
|
|
9684
|
+
const closedBeforeStart = (run) => opts.daemonStartedAt != null && sqliteMs(run.updated_at) < opts.daemonStartedAt;
|
|
8701
9685
|
const recentFailed = allRuns.filter(
|
|
8702
|
-
(r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r)
|
|
9686
|
+
(r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r)
|
|
8703
9687
|
);
|
|
8704
9688
|
if (recentFailed.length >= thresholds.failedSpikeCount) {
|
|
8705
9689
|
candidates.push({
|
|
@@ -8736,6 +9720,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8736
9720
|
if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
|
|
8737
9721
|
if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
|
|
8738
9722
|
if (killedByOperator(run)) continue;
|
|
9723
|
+
if (closedBeforeStart(run)) continue;
|
|
8739
9724
|
const repeat = isRepeatDown(run);
|
|
8740
9725
|
candidates.push({
|
|
8741
9726
|
key: `run_stalled:${run.id}`,
|
|
@@ -8756,6 +9741,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8756
9741
|
if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
|
|
8757
9742
|
if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
|
|
8758
9743
|
if (killedByOperator(run)) continue;
|
|
9744
|
+
if (closedBeforeStart(run)) continue;
|
|
8759
9745
|
const repeat = isRepeatDown(run);
|
|
8760
9746
|
candidates.push({
|
|
8761
9747
|
key: `run_failed:${run.id}`,
|
|
@@ -9175,22 +10161,22 @@ function makeExclusiveReader(opts) {
|
|
|
9175
10161
|
}
|
|
9176
10162
|
|
|
9177
10163
|
// src/envGate.ts
|
|
9178
|
-
import { existsSync as
|
|
9179
|
-
import { join as
|
|
10164
|
+
import { existsSync as existsSync13 } from "fs";
|
|
10165
|
+
import { join as join23 } from "path";
|
|
9180
10166
|
import { createConnection } from "net";
|
|
9181
10167
|
import { execa as execa2 } from "execa";
|
|
9182
10168
|
|
|
9183
10169
|
// src/platform.ts
|
|
9184
|
-
import { existsSync as
|
|
9185
|
-
import { delimiter, join as
|
|
10170
|
+
import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
|
|
10171
|
+
import { delimiter, join as join22 } from "path";
|
|
9186
10172
|
function resolveCommandPath(name, env = process.env, platform = process.platform) {
|
|
9187
|
-
if (name.includes("/") || name.includes("\\")) return
|
|
10173
|
+
if (name.includes("/") || name.includes("\\")) return existsSync12(name) ? name : null;
|
|
9188
10174
|
const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
|
|
9189
10175
|
const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
|
|
9190
10176
|
for (const dir of dirs) {
|
|
9191
10177
|
for (const ext of exts) {
|
|
9192
|
-
const candidate =
|
|
9193
|
-
if (
|
|
10178
|
+
const candidate = join22(dir, name + ext);
|
|
10179
|
+
if (existsSync12(candidate)) return candidate;
|
|
9194
10180
|
}
|
|
9195
10181
|
}
|
|
9196
10182
|
return null;
|
|
@@ -9248,8 +10234,8 @@ var COMPOSE_MARKERS = [
|
|
|
9248
10234
|
"compose.yaml"
|
|
9249
10235
|
];
|
|
9250
10236
|
function detectRequirements(repo, io = {}) {
|
|
9251
|
-
const exists = io.exists ??
|
|
9252
|
-
const marker = COMPOSE_MARKERS.find((f) => exists(
|
|
10237
|
+
const exists = io.exists ?? existsSync13;
|
|
10238
|
+
const marker = COMPOSE_MARKERS.find((f) => exists(join23(repo, f)));
|
|
9253
10239
|
return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
|
|
9254
10240
|
}
|
|
9255
10241
|
function resolveRequirements(repo, declared, io = {}) {
|
|
@@ -9385,8 +10371,8 @@ import { basename as basename3 } from "path";
|
|
|
9385
10371
|
import { serve as serve2 } from "@hono/node-server";
|
|
9386
10372
|
|
|
9387
10373
|
// src/diff.ts
|
|
9388
|
-
import { statSync as
|
|
9389
|
-
import { join as
|
|
10374
|
+
import { statSync as statSync8 } from "fs";
|
|
10375
|
+
import { join as join24 } from "path";
|
|
9390
10376
|
import { $ as $4 } from "execa";
|
|
9391
10377
|
var MAX_FILES = 200;
|
|
9392
10378
|
var MAX_LINES_PER_FILE = 1500;
|
|
@@ -9529,7 +10515,7 @@ async function resolveBase(wt, baseBranch) {
|
|
|
9529
10515
|
}
|
|
9530
10516
|
function mtime(path) {
|
|
9531
10517
|
try {
|
|
9532
|
-
return
|
|
10518
|
+
return statSync8(path).mtimeMs;
|
|
9533
10519
|
} catch {
|
|
9534
10520
|
return 0;
|
|
9535
10521
|
}
|
|
@@ -9554,7 +10540,7 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
9554
10540
|
files.push(pf);
|
|
9555
10541
|
}
|
|
9556
10542
|
}
|
|
9557
|
-
files.sort((a, b) => mtime(
|
|
10543
|
+
files.sort((a, b) => mtime(join24(worktree, b.path)) - mtime(join24(worktree, a.path)));
|
|
9558
10544
|
const truncatedFiles = files.length > MAX_FILES;
|
|
9559
10545
|
if (truncatedFiles) files = files.slice(0, MAX_FILES);
|
|
9560
10546
|
const added = files.reduce((s, f) => s + f.added, 0);
|
|
@@ -9569,11 +10555,25 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
9569
10555
|
...truncatedFiles ? { truncated: true } : {}
|
|
9570
10556
|
};
|
|
9571
10557
|
}
|
|
10558
|
+
async function changedPaths(worktree, opts = {}) {
|
|
10559
|
+
if (!worktree) return [];
|
|
10560
|
+
try {
|
|
10561
|
+
const isWt = (await $4({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
|
|
10562
|
+
if (isWt !== "true") return [];
|
|
10563
|
+
const from = await resolveBase(worktree, opts.baseBranch) ?? "HEAD";
|
|
10564
|
+
const tracked = (await $4({ reject: false })`git -C ${worktree} diff --name-only --find-renames ${from} --`).stdout;
|
|
10565
|
+
const untracked = (await $4({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
|
|
10566
|
+
const all = [...tracked.split("\n"), ...untracked.split("\0")].map((s) => s.trim()).filter(Boolean);
|
|
10567
|
+
return [...new Set(all)].sort();
|
|
10568
|
+
} catch {
|
|
10569
|
+
return [];
|
|
10570
|
+
}
|
|
10571
|
+
}
|
|
9572
10572
|
|
|
9573
10573
|
// package.json
|
|
9574
10574
|
var package_default = {
|
|
9575
10575
|
name: "@taskless-app/swarm",
|
|
9576
|
-
version: "0.
|
|
10576
|
+
version: "0.6.0",
|
|
9577
10577
|
type: "module",
|
|
9578
10578
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
9579
10579
|
keywords: [
|
|
@@ -9690,6 +10690,10 @@ function roySnapshot(db, now = Date.now(), overmindLog = [], paused = false, dae
|
|
|
9690
10690
|
stopReason: r.stop_reason,
|
|
9691
10691
|
reason,
|
|
9692
10692
|
updated: r.updated_at,
|
|
10693
|
+
startedAt: r.created_at,
|
|
10694
|
+
// TASWM-351: возраст прогона на карточке
|
|
10695
|
+
costUsd: r.cost_usd || 0,
|
|
10696
|
+
// TASWM-351: то же поле, что уходит в прод (swarm_run_upsert{costUsd})
|
|
9693
10697
|
step,
|
|
9694
10698
|
hasDecision: withQ.has(r.id),
|
|
9695
10699
|
rescued: evs.some((e) => e.kind === "rescued"),
|
|
@@ -11237,6 +12241,7 @@ async function checkDaemonVersion(start, now = () => /* @__PURE__ */ new Date())
|
|
|
11237
12241
|
}
|
|
11238
12242
|
|
|
11239
12243
|
// src/release.ts
|
|
12244
|
+
init_claudeBin();
|
|
11240
12245
|
var SWARM_PACKAGE = "@taskless-app/swarm";
|
|
11241
12246
|
var LATEST_TIMEOUT_MS = 4e3;
|
|
11242
12247
|
async function fetchLatestVersion(opts = {}) {
|
|
@@ -11271,9 +12276,9 @@ function outdatedLine(current, latest, updateCommand = "swarm update") {
|
|
|
11271
12276
|
// src/hostCommands.ts
|
|
11272
12277
|
init_src();
|
|
11273
12278
|
import { readFileSync as readFileSync18, renameSync as renameSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
11274
|
-
import { join as
|
|
12279
|
+
import { join as join25 } from "path";
|
|
11275
12280
|
function hostCommandStatePath(home) {
|
|
11276
|
-
return
|
|
12281
|
+
return join25(home, "host-command.json");
|
|
11277
12282
|
}
|
|
11278
12283
|
function readState(path) {
|
|
11279
12284
|
try {
|
|
@@ -11383,6 +12388,28 @@ async function executeOne(cmd, opts) {
|
|
|
11383
12388
|
await ack(cmd.id, "done", { result: `\u043F\u0440\u043E\u0433\u043E\u043D ${slug} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D` });
|
|
11384
12389
|
return false;
|
|
11385
12390
|
}
|
|
12391
|
+
// TASWM-351: «перезапустить с того же места». Отказ побудки (дрон уже работает,
|
|
12392
|
+
// продолжать нечем) уезжает как `rejected` с ЕЁ формулировкой — повторять такую
|
|
12393
|
+
// команду бессмысленно, и это ровно та разница между rejected и failed, ради
|
|
12394
|
+
// которой они разделены (см. HOST_COMMAND_STATUSES).
|
|
12395
|
+
case "wake_run": {
|
|
12396
|
+
const slug = cmd.runSlug?.trim();
|
|
12397
|
+
if (!slug) {
|
|
12398
|
+
await ack(cmd.id, "rejected", { result: "\u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043D\u043E, \u043A\u0430\u043A\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0442\u044C" });
|
|
12399
|
+
return false;
|
|
12400
|
+
}
|
|
12401
|
+
const woken = await effects.wakeRun(slug);
|
|
12402
|
+
if (!woken) {
|
|
12403
|
+
await ack(cmd.id, "rejected", { result: `\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043F\u043E ${slug} \u0443 \u043C\u0435\u043D\u044F \u043D\u0435\u0442` });
|
|
12404
|
+
return false;
|
|
12405
|
+
}
|
|
12406
|
+
if (!woken.ok) {
|
|
12407
|
+
await ack(cmd.id, "rejected", { result: woken.why });
|
|
12408
|
+
return false;
|
|
12409
|
+
}
|
|
12410
|
+
await ack(cmd.id, "done", { result: woken.why });
|
|
12411
|
+
return false;
|
|
12412
|
+
}
|
|
11386
12413
|
case "restart":
|
|
11387
12414
|
return await doRestart(cmd, opts, "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B\u0441\u044F \u043F\u043E \u043A\u043E\u043C\u0430\u043D\u0434\u0435 \u0438\u0437 Taskless");
|
|
11388
12415
|
case "update": {
|
|
@@ -11420,8 +12447,8 @@ async function doRestart(cmd, opts, note) {
|
|
|
11420
12447
|
|
|
11421
12448
|
// src/selfUpdate.ts
|
|
11422
12449
|
init_git();
|
|
11423
|
-
import { existsSync as
|
|
11424
|
-
import { join as
|
|
12450
|
+
import { existsSync as existsSync14 } from "fs";
|
|
12451
|
+
import { join as join26 } from "path";
|
|
11425
12452
|
import { $ as $6 } from "execa";
|
|
11426
12453
|
var DAEMON_PACKAGE = "@taskless-app/swarm";
|
|
11427
12454
|
var defaultRun = async (cmd, args, cwd) => {
|
|
@@ -11434,7 +12461,7 @@ function tail(text, max = 400) {
|
|
|
11434
12461
|
}
|
|
11435
12462
|
async function runSelfUpdate(io) {
|
|
11436
12463
|
const run = io.run ?? defaultRun;
|
|
11437
|
-
const exists = io.exists ??
|
|
12464
|
+
const exists = io.exists ?? existsSync14;
|
|
11438
12465
|
const repo = io.repo;
|
|
11439
12466
|
await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
|
|
11440
12467
|
const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
|
|
@@ -11463,7 +12490,7 @@ async function runSelfUpdate(io) {
|
|
|
11463
12490
|
};
|
|
11464
12491
|
}
|
|
11465
12492
|
await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
|
|
11466
|
-
if (!exists(
|
|
12493
|
+
if (!exists(join26(repo, "pnpm-lock.yaml"))) {
|
|
11467
12494
|
return { ok: false, step: "install", message: "\u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442 pnpm-lock.yaml \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u0435\u0433\u043E \u044D\u0442\u0438\u043C \u043F\u0443\u0442\u0451\u043C \u043D\u0435 \u0431\u0435\u0440\u0443\u0441\u044C" };
|
|
11468
12495
|
}
|
|
11469
12496
|
const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
|
|
@@ -11497,11 +12524,12 @@ function makeOvermindNarrator() {
|
|
|
11497
12524
|
}
|
|
11498
12525
|
|
|
11499
12526
|
// src/probe.ts
|
|
12527
|
+
init_executors();
|
|
11500
12528
|
import { spawn as spawn2 } from "child_process";
|
|
11501
|
-
var LIMIT_RE = /(weekly limit|usage limit|hit your .*limit)/i;
|
|
11502
12529
|
var RESET_RE = /resets[^\n]*/i;
|
|
11503
|
-
function
|
|
11504
|
-
const
|
|
12530
|
+
function executorHealthy(opts = {}) {
|
|
12531
|
+
const executor = opts.executor ?? resolveExecutor();
|
|
12532
|
+
const bin = opts.bin ?? executor.resolveBin();
|
|
11505
12533
|
const timeoutMs = opts.timeoutMs ?? 2e4;
|
|
11506
12534
|
return new Promise((resolve4) => {
|
|
11507
12535
|
let done = false;
|
|
@@ -11516,7 +12544,7 @@ function anthropicHealthy(opts = {}) {
|
|
|
11516
12544
|
}
|
|
11517
12545
|
resolve4(r);
|
|
11518
12546
|
};
|
|
11519
|
-
const child = spawn2(bin, [...
|
|
12547
|
+
const child = spawn2(bin, [...executor.probe.args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
11520
12548
|
child.stdout?.on("data", (d) => {
|
|
11521
12549
|
out += d.toString();
|
|
11522
12550
|
});
|
|
@@ -11527,7 +12555,9 @@ function anthropicHealthy(opts = {}) {
|
|
|
11527
12555
|
child.on("error", () => finish({ ok: false, reason: "exec", resetHint: null }));
|
|
11528
12556
|
child.on("exit", (code) => {
|
|
11529
12557
|
if (code === 0) return finish({ ok: true, reason: "ok", resetHint: null });
|
|
11530
|
-
if (
|
|
12558
|
+
if (executor.probe.limitPattern.test(out)) {
|
|
12559
|
+
return finish({ ok: false, reason: "limit", resetHint: out.match(RESET_RE)?.[0]?.trim() ?? null });
|
|
12560
|
+
}
|
|
11531
12561
|
finish({ ok: false, reason: "provider", resetHint: null });
|
|
11532
12562
|
});
|
|
11533
12563
|
});
|
|
@@ -11553,18 +12583,21 @@ async function fetchExternalStorm(url, opts = {}) {
|
|
|
11553
12583
|
}
|
|
11554
12584
|
|
|
11555
12585
|
// src/anomalyState.ts
|
|
11556
|
-
import { existsSync as
|
|
11557
|
-
import { join as
|
|
12586
|
+
import { existsSync as existsSync15, readFileSync as readFileSync19, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
|
|
12587
|
+
import { join as join27 } from "path";
|
|
11558
12588
|
var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
11559
|
-
var
|
|
12589
|
+
var MAX_RESOLVED = 200;
|
|
12590
|
+
function emptyState() {
|
|
12591
|
+
return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
|
|
12592
|
+
}
|
|
11560
12593
|
function anomalyStatePath(home) {
|
|
11561
|
-
return
|
|
12594
|
+
return join27(home, "anomaly-state.json");
|
|
11562
12595
|
}
|
|
11563
12596
|
function loadAnomalyState(home, now = Date.now(), io = {}) {
|
|
11564
|
-
const exists = io.exists ??
|
|
12597
|
+
const exists = io.exists ?? existsSync15;
|
|
11565
12598
|
const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
|
|
11566
12599
|
const path = anomalyStatePath(home);
|
|
11567
|
-
if (!exists(path)) return
|
|
12600
|
+
if (!exists(path)) return emptyState();
|
|
11568
12601
|
try {
|
|
11569
12602
|
const raw = JSON.parse(readFile2(path));
|
|
11570
12603
|
const since = numbers(raw.since);
|
|
@@ -11579,10 +12612,14 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
|
|
|
11579
12612
|
since: pick(since, fresh),
|
|
11580
12613
|
lastFiredAt: pick(lastFiredAt, fresh),
|
|
11581
12614
|
fires: pick(fires, fresh),
|
|
12615
|
+
// TAS-485: очередь гашения НЕ фильтруем тем же TTL, что ключи. Она про
|
|
12616
|
+
// уведомление, которое УЖЕ висит у человека в инбоксе: пролежав неделю
|
|
12617
|
+
// недоставленным, гашение не становится лишним — оно становится нужнее.
|
|
12618
|
+
resolved: strings(raw.resolved).slice(0, MAX_RESOLVED),
|
|
11582
12619
|
stopQuiet: parseQuiet(raw.stopQuiet)
|
|
11583
12620
|
};
|
|
11584
12621
|
} catch {
|
|
11585
|
-
return
|
|
12622
|
+
return emptyState();
|
|
11586
12623
|
}
|
|
11587
12624
|
}
|
|
11588
12625
|
function saveAnomalyState(home, state, io = {}) {
|
|
@@ -11605,9 +12642,14 @@ var AnomalyStateStore = class {
|
|
|
11605
12642
|
/** Инъекция в AnomalyTracker (см. AnomalyTrackerOpts.io). */
|
|
11606
12643
|
trackerIo() {
|
|
11607
12644
|
return {
|
|
11608
|
-
load: () => ({
|
|
12645
|
+
load: () => ({
|
|
12646
|
+
since: this.state.since,
|
|
12647
|
+
lastFiredAt: this.state.lastFiredAt,
|
|
12648
|
+
fires: this.state.fires,
|
|
12649
|
+
resolved: this.state.resolved
|
|
12650
|
+
}),
|
|
11609
12651
|
save: (snapshot) => {
|
|
11610
|
-
this.state = { ...snapshot, stopQuiet: this.state.stopQuiet };
|
|
12652
|
+
this.state = { ...snapshot, resolved: (snapshot.resolved ?? []).slice(0, MAX_RESOLVED), stopQuiet: this.state.stopQuiet };
|
|
11611
12653
|
this.flush();
|
|
11612
12654
|
}
|
|
11613
12655
|
};
|
|
@@ -11649,6 +12691,10 @@ function numbers(raw) {
|
|
|
11649
12691
|
}
|
|
11650
12692
|
return out;
|
|
11651
12693
|
}
|
|
12694
|
+
function strings(raw) {
|
|
12695
|
+
if (!Array.isArray(raw)) return [];
|
|
12696
|
+
return raw.filter((v) => typeof v === "string" && v.length > 0);
|
|
12697
|
+
}
|
|
11652
12698
|
function pick(src, keys) {
|
|
11653
12699
|
const out = {};
|
|
11654
12700
|
for (const k of keys) if (src[k] != null) out[k] = src[k];
|
|
@@ -11681,7 +12727,7 @@ function isRepeatEvent(prev, cur) {
|
|
|
11681
12727
|
}
|
|
11682
12728
|
function swarmLastActivityMs(db) {
|
|
11683
12729
|
let latest = null;
|
|
11684
|
-
const
|
|
12730
|
+
const bump2 = (iso) => {
|
|
11685
12731
|
const ms = sqliteMs(iso);
|
|
11686
12732
|
if (latest == null || ms > latest) latest = ms;
|
|
11687
12733
|
};
|
|
@@ -11689,12 +12735,12 @@ function swarmLastActivityMs(db) {
|
|
|
11689
12735
|
if (isTerminalStatus(run.status)) continue;
|
|
11690
12736
|
const evs = db.events(run.id);
|
|
11691
12737
|
if (!evs.length) {
|
|
11692
|
-
|
|
12738
|
+
bump2(run.updated_at);
|
|
11693
12739
|
continue;
|
|
11694
12740
|
}
|
|
11695
12741
|
let i = evs.length - 1;
|
|
11696
12742
|
while (i > 0 && isRepeatEvent(evs[i - 1], evs[i])) i--;
|
|
11697
|
-
|
|
12743
|
+
bump2(evs[i].at);
|
|
11698
12744
|
}
|
|
11699
12745
|
return latest;
|
|
11700
12746
|
}
|
|
@@ -11820,7 +12866,193 @@ function decideWatchdogAction(db, now, state, thresholds, droneCap, wt, exclusiv
|
|
|
11820
12866
|
// src/overmind.ts
|
|
11821
12867
|
init_storm();
|
|
11822
12868
|
init_src();
|
|
12869
|
+
|
|
12870
|
+
// src/zones.ts
|
|
12871
|
+
init_src();
|
|
12872
|
+
|
|
12873
|
+
// src/gitHistory.ts
|
|
12874
|
+
import { $ as $7 } from "execa";
|
|
12875
|
+
var REC = "";
|
|
12876
|
+
var FIELD = "";
|
|
12877
|
+
var LOG_FORMAT = `${REC}%H${FIELD}%s`;
|
|
12878
|
+
var SLUG_IN_SUBJECT = /\b([A-Z][A-Z0-9]{0,7}-\d+)\b/;
|
|
12879
|
+
function slugFromSubject(subject) {
|
|
12880
|
+
return SLUG_IN_SUBJECT.exec(subject)?.[1] ?? null;
|
|
12881
|
+
}
|
|
12882
|
+
function unquotePath(raw) {
|
|
12883
|
+
const p = raw.trim();
|
|
12884
|
+
return p.length >= 2 && p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
|
|
12885
|
+
}
|
|
12886
|
+
function parseGitLog(out) {
|
|
12887
|
+
const commits = [];
|
|
12888
|
+
for (const chunk of out.split(REC)) {
|
|
12889
|
+
if (!chunk.trim()) continue;
|
|
12890
|
+
const [head, ...rest] = chunk.split("\n");
|
|
12891
|
+
const [sha, subject] = (head ?? "").split(FIELD);
|
|
12892
|
+
if (!sha) continue;
|
|
12893
|
+
const files = rest.map(unquotePath).filter(Boolean);
|
|
12894
|
+
commits.push({ sha, subject: subject ?? "", files });
|
|
12895
|
+
}
|
|
12896
|
+
return commits;
|
|
12897
|
+
}
|
|
12898
|
+
var NOISE_RE = /^(journal\/|changelog\/|docs\/|design\/|scratch\/|\.claude\/|.*\.lock$|pnpm-lock\.yaml$)/;
|
|
12899
|
+
function isNoisePath(path) {
|
|
12900
|
+
return NOISE_RE.test(path);
|
|
12901
|
+
}
|
|
12902
|
+
var GROUP_DEFAULTS = { maxFilesPerCommit: 120, ignore: isNoisePath };
|
|
12903
|
+
function groupTaskHistory(commits, opts = {}) {
|
|
12904
|
+
const o = { ...GROUP_DEFAULTS, ...opts };
|
|
12905
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
12906
|
+
for (const c of commits) {
|
|
12907
|
+
const slug = slugFromSubject(c.subject);
|
|
12908
|
+
if (!slug) continue;
|
|
12909
|
+
if (c.files.length > o.maxFilesPerCommit) continue;
|
|
12910
|
+
const files = c.files.filter((f) => !o.ignore(f));
|
|
12911
|
+
if (!files.length) continue;
|
|
12912
|
+
let acc = bySlug.get(slug);
|
|
12913
|
+
if (!acc) bySlug.set(slug, acc = { files: /* @__PURE__ */ new Set(), texts: [] });
|
|
12914
|
+
for (const f of files) acc.files.add(f);
|
|
12915
|
+
acc.texts.push(c.subject);
|
|
12916
|
+
}
|
|
12917
|
+
return [...bySlug.entries()].map(([slug, acc]) => ({
|
|
12918
|
+
slug,
|
|
12919
|
+
files: [...acc.files],
|
|
12920
|
+
text: acc.texts.join(" \xB7 ")
|
|
12921
|
+
}));
|
|
12922
|
+
}
|
|
12923
|
+
async function readCommitLog(repo, opts = {}) {
|
|
12924
|
+
const max = opts.maxCommits ?? 1500;
|
|
12925
|
+
try {
|
|
12926
|
+
const { stdout } = await $7({ cwd: repo, reject: false })`git log --no-merges --name-only --pretty=format:${LOG_FORMAT} -n ${String(max)} ${opts.ref ?? "HEAD"}`;
|
|
12927
|
+
return parseGitLog(stdout ?? "");
|
|
12928
|
+
} catch {
|
|
12929
|
+
return [];
|
|
12930
|
+
}
|
|
12931
|
+
}
|
|
12932
|
+
async function readTaskHistory(repo, opts = {}) {
|
|
12933
|
+
return groupTaskHistory(await readCommitLog(repo, opts), opts);
|
|
12934
|
+
}
|
|
12935
|
+
async function headSha(repo) {
|
|
12936
|
+
try {
|
|
12937
|
+
const { stdout, exitCode } = await $7({ cwd: repo, reject: false })`git rev-parse HEAD`;
|
|
12938
|
+
return exitCode === 0 ? stdout.trim() : null;
|
|
12939
|
+
} catch {
|
|
12940
|
+
return null;
|
|
12941
|
+
}
|
|
12942
|
+
}
|
|
12943
|
+
|
|
12944
|
+
// src/zones.ts
|
|
12945
|
+
function runText(run) {
|
|
12946
|
+
return [run.task_title ?? "", run.task_desc ?? ""].join(" ").trim();
|
|
12947
|
+
}
|
|
12948
|
+
function predictForRun(model, run) {
|
|
12949
|
+
return predictFiles(model, { zone: run.area, text: runText(run) });
|
|
12950
|
+
}
|
|
12951
|
+
function short(files, n = 3) {
|
|
12952
|
+
return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
|
|
12953
|
+
}
|
|
12954
|
+
var SPINE_PREDICTION_WEIGHT = 0.7;
|
|
12955
|
+
function decideConflict({ model, candidate, active }) {
|
|
12956
|
+
if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
|
|
12957
|
+
if (!hasZoneModel(model)) return { kind: "unknown" };
|
|
12958
|
+
const predicted = predictForRun(model, candidate);
|
|
12959
|
+
if (!predicted.length) return { kind: "unknown" };
|
|
12960
|
+
if (!active.length) return { kind: "clear" };
|
|
12961
|
+
const spine = new Set(model.spine);
|
|
12962
|
+
const candidateSpine = predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
|
|
12963
|
+
if (candidateSpine.length) {
|
|
12964
|
+
const blocker = active[0];
|
|
12965
|
+
return {
|
|
12966
|
+
kind: "wait",
|
|
12967
|
+
blockedBy: blocker.run,
|
|
12968
|
+
message: `\u0437\u0430\u0434\u0430\u0447\u0430 \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${short(candidateSpine)}) \u2014 \u0442\u0430\u043A\u0438\u0435 \u0438\u0434\u0443\u0442 \u043F\u043E\u0441\u043B\u0435\u0434\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u0437\u043E\u043D\u044B; \u0436\u0434\u0443 ${blocker.run.task_slug} (#${blocker.run.id})`
|
|
12969
|
+
};
|
|
12970
|
+
}
|
|
12971
|
+
for (const w of active) {
|
|
12972
|
+
if (!w.actual) continue;
|
|
12973
|
+
const theirSpine = w.files.filter((f) => spine.has(f));
|
|
12974
|
+
if (theirSpine.length && zoneForFiles(model, w.files)?.zone === SPINE_ZONE) {
|
|
12975
|
+
return {
|
|
12976
|
+
kind: "wait",
|
|
12977
|
+
blockedBy: w.run,
|
|
12978
|
+
message: `${w.run.task_slug} (#${w.run.id}) \u043F\u0440\u044F\u043C\u043E \u0441\u0435\u0439\u0447\u0430\u0441 \u043F\u0440\u0430\u0432\u0438\u0442 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${short(theirSpine)}) \u2014 \u043E\u0442\u0432\u0435\u0442\u0432\u043B\u044F\u0442\u044C\u0441\u044F \u043E\u0442 \u043D\u0435\u0433\u043E \u0440\u0430\u043D\u043E, \u0436\u0434\u0443`
|
|
12979
|
+
};
|
|
12980
|
+
}
|
|
12981
|
+
}
|
|
12982
|
+
for (const w of active) {
|
|
12983
|
+
const risk = overlapRisk(predicted, w.files);
|
|
12984
|
+
if (!risk.overlap) continue;
|
|
12985
|
+
const what = risk.sharedFiles.length ? `\u043E\u0434\u043D\u0438 \u0444\u0430\u0439\u043B\u044B (${short(risk.sharedFiles)})` : `\u043E\u0434\u0438\u043D \u043A\u0430\u0442\u0430\u043B\u043E\u0433 (${short(risk.sharedDirs)})`;
|
|
12986
|
+
return {
|
|
12987
|
+
kind: "wait",
|
|
12988
|
+
blockedBy: w.run,
|
|
12989
|
+
message: `\u044D\u0442\u0438 \u0434\u0432\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0442\u0440\u043E\u0433\u0430\u044E\u0442 ${what} \u2014 \u0441\u0442\u0430\u0432\u0438\u043C \u043F\u043E\u0441\u043B\u0435\u0434\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u043D\u043E: \u0436\u0434\u0443 ${w.run.task_slug} (#${w.run.id})` + (w.actual ? "" : " (\u043F\u043E \u043F\u0440\u0435\u0434\u0441\u043A\u0430\u0437\u0430\u043D\u0438\u044E \u2014 \u0434\u0440\u043E\u043D \u0435\u0449\u0451 \u043D\u0435 \u043D\u0430\u0447\u0430\u043B \u043F\u0438\u0441\u0430\u0442\u044C \u043A\u043E\u0434)")
|
|
12990
|
+
};
|
|
12991
|
+
}
|
|
12992
|
+
return { kind: "clear" };
|
|
12993
|
+
}
|
|
12994
|
+
function suggestAreaForRun(model, run) {
|
|
12995
|
+
if (!hasZoneModel(model)) return null;
|
|
12996
|
+
const text = runText(run);
|
|
12997
|
+
if (!text) return null;
|
|
12998
|
+
const top = suggestZonesFromText(model, text)[0];
|
|
12999
|
+
return top && top.confidence >= 0.3 ? { zone: top.zone, confidence: top.confidence, why: top.why } : null;
|
|
13000
|
+
}
|
|
13001
|
+
var DEFAULT_TTL_MS = 10 * 6e4;
|
|
13002
|
+
var ZoneModels = class {
|
|
13003
|
+
constructor(opts = {}) {
|
|
13004
|
+
this.opts = opts;
|
|
13005
|
+
}
|
|
13006
|
+
opts;
|
|
13007
|
+
cache = /* @__PURE__ */ new Map();
|
|
13008
|
+
/** Модель репо. Истории нет / это не git — EMPTY_ZONE_MODEL (раздача на старом локе). */
|
|
13009
|
+
async forRepo(repo, now = Date.now()) {
|
|
13010
|
+
if (!repo) return EMPTY_ZONE_MODEL;
|
|
13011
|
+
const head = await headSha(repo);
|
|
13012
|
+
const hit = this.cache.get(repo);
|
|
13013
|
+
if (hit && hit.head === head && now - hit.builtAt < (this.opts.ttlMs ?? DEFAULT_TTL_MS)) return hit.model;
|
|
13014
|
+
const model = await this.build(repo);
|
|
13015
|
+
this.cache.set(repo, { head, builtAt: now, model });
|
|
13016
|
+
return model;
|
|
13017
|
+
}
|
|
13018
|
+
/** Сбросить кэш (тесты, `swarm zones --refresh`). */
|
|
13019
|
+
clear() {
|
|
13020
|
+
this.cache.clear();
|
|
13021
|
+
}
|
|
13022
|
+
async build(repo) {
|
|
13023
|
+
const history = await readTaskHistory(repo, { ...this.opts.maxCommits ? { maxCommits: this.opts.maxCommits } : {} });
|
|
13024
|
+
return buildZoneModel(withManualAreas(history, this.opts.manualAreas?.() ?? /* @__PURE__ */ new Map()));
|
|
13025
|
+
}
|
|
13026
|
+
};
|
|
13027
|
+
var ActiveFilesCache = class {
|
|
13028
|
+
constructor(read, ttlMs = 3e4) {
|
|
13029
|
+
this.read = read;
|
|
13030
|
+
this.ttlMs = ttlMs;
|
|
13031
|
+
}
|
|
13032
|
+
read;
|
|
13033
|
+
ttlMs;
|
|
13034
|
+
cache = /* @__PURE__ */ new Map();
|
|
13035
|
+
async filesOf(run, now = Date.now()) {
|
|
13036
|
+
const hit = this.cache.get(run.id);
|
|
13037
|
+
if (hit && now - hit.at < this.ttlMs) return hit.files;
|
|
13038
|
+
const files = await this.read(run);
|
|
13039
|
+
this.cache.set(run.id, { at: now, files });
|
|
13040
|
+
return files;
|
|
13041
|
+
}
|
|
13042
|
+
/** Забыть всех, кого нет среди живых (дрон закончил — его файлы больше не заняты). */
|
|
13043
|
+
prune(liveIds) {
|
|
13044
|
+
const live = new Set(liveIds);
|
|
13045
|
+
for (const id of [...this.cache.keys()]) if (!live.has(id)) this.cache.delete(id);
|
|
13046
|
+
}
|
|
13047
|
+
};
|
|
13048
|
+
function withManualAreas(history, areas) {
|
|
13049
|
+
return history.map((e) => ({ ...e, area: normalizeZoneName(areas.get(e.slug) ?? null) }));
|
|
13050
|
+
}
|
|
13051
|
+
|
|
13052
|
+
// src/overmind.ts
|
|
13053
|
+
var ANOMALY_RESOLVE_BATCH = 50;
|
|
11823
13054
|
var THIS_FILE = fileURLToPath2(import.meta.url);
|
|
13055
|
+
var executorHasSession = (p) => EXECUTOR.session.exists(p);
|
|
11824
13056
|
var OVERMIND_ENTRY = THIS_FILE;
|
|
11825
13057
|
var STALL_MS = Number(process.env.SWARM_STALL_MS) || 10 * 60 * 1e3;
|
|
11826
13058
|
var STALL_FLOOR_MS = Number(process.env.SWARM_STALL_FLOOR_MS) || 30 * 60 * 1e3;
|
|
@@ -11834,6 +13066,10 @@ var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
|
|
|
11834
13066
|
var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
|
|
11835
13067
|
var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
|
|
11836
13068
|
var REVIEW_ESCALATION_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F";
|
|
13069
|
+
var REVIEW_UNRECOVERABLE_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u044C";
|
|
13070
|
+
function reviewUnrecoverableMark(branch) {
|
|
13071
|
+
return `${REVIEW_UNRECOVERABLE_MARK} (\u0432\u0435\u0442\u043A\u0430 ${branch})`;
|
|
13072
|
+
}
|
|
11837
13073
|
var REVIEW_CYCLE_LIMIT = Number(process.env.SWARM_REVIEW_CYCLE_LIMIT) || 3;
|
|
11838
13074
|
var REVIEW_CYCLE_ESCALATION_MARK = "\u0440\u0435-\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0446\u0438\u043A\u043B\u0438\u043B\u043E\u0441\u044C";
|
|
11839
13075
|
var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
|
|
@@ -11860,6 +13096,7 @@ var CHANGES_WAKE_ESCALATION_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\
|
|
|
11860
13096
|
var NEEDS_HUMAN_MARKS = [
|
|
11861
13097
|
RESOLVE_ESCALATION_MARK,
|
|
11862
13098
|
REVIEW_ESCALATION_MARK,
|
|
13099
|
+
REVIEW_UNRECOVERABLE_MARK,
|
|
11863
13100
|
REVIEW_CYCLE_ESCALATION_MARK,
|
|
11864
13101
|
CI_WAKE_ESCALATION_MARK,
|
|
11865
13102
|
CHANGES_WAKE_ESCALATION_MARK
|
|
@@ -12188,8 +13425,12 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
12188
13425
|
// авторские подписи внутри него уже приведены к одной строке (renderAttachmentsBlock).
|
|
12189
13426
|
...attachmentsBlock ? ["", attachmentsBlock] : [],
|
|
12190
13427
|
"",
|
|
12191
|
-
|
|
12192
|
-
|
|
13428
|
+
// TASWM-356: ветка worktree — НЕ обязательно слаг. У продолжения чужого PR она своя
|
|
13429
|
+
// (её завёл прошлый дрон), и `git push -u origin <slug>` оттуда либо откажет, либо заведёт
|
|
13430
|
+
// вторую ветку мимо PR. Пишем ту, что реально записана на прогоне; пусто (первый спавн —
|
|
13431
|
+
// ветку заводит spawnDrone из слага) — прежняя формулировка.
|
|
13432
|
+
`\u0422\u044B \u2014 \u0434\u0440\u043E\u043D \u0440\u043E\u044F. \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C \u0432 \u044D\u0442\u043E\u043C git-worktree, \u0432\u0435\u0442\u043A\u0430 \`${run.branch || run.task_slug}\`${run.branch && run.branch !== run.task_slug ? " (\u043E\u043D\u0430 \u041D\u0415 \u0440\u0430\u0432\u043D\u0430 \u0441\u043B\u0430\u0433\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u044D\u0442\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u0435 \u0443\u0436\u0435 \u043E\u0442\u043A\u0440\u044B\u0442\u043E\u0433\u043E PR, \u043F\u0443\u0448 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u043D\u0435\u0451)" : " \u0441\u043E\u0437\u0434\u0430\u043D\u0430 \u043F\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438"}.`,
|
|
13433
|
+
"\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; read_task() \u2014 \u0441\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 (\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435+\u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438+\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u044B+\u0434\u043E\u043A\u0438+\u0438\u0441\u0445\u043E\u0434\u044B \u0441\u043E\u0441\u0435\u0434\u043D\u0438\u0445 \u0437\u0430\u0434\u0430\u0447 \u043F\u043E \u0437\u043E\u043D\u0435 \u043A\u043E\u0434\u0430) \u0431\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432, \u0435\u0441\u043B\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043C\u043E\u0433\u043B\u0438 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435 \u0441\u0442\u0430\u0440\u0442\u0430; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR; note(text) \u2014 \u0437\u0430\u043C\u0435\u0442\u043A\u0430 \xAB\u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438\xBB \u0411\u0415\u0417 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0438: \u043D\u0430\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u043D\u0430 \u043F\u0440\u043E\u0431\u0435\u043B \u0432 \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435, \u043F\u0440\u043E\u0442\u0443\u0445\u0448\u0443\u044E \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u044E, \u0431\u0430\u0433 \u0440\u044F\u0434\u043E\u043C \u0441\u043E \u0441\u0432\u043E\u0438\u043C \u0434\u0438\u0444\u0444\u043E\u043C \u2014 \u0441\u043A\u0430\u0436\u0438 \u0438 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u0434\u0430\u043B\u044C\u0448\u0435, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0443\u0432\u0438\u0434\u0438\u0442 \u0438 \u0441\u0430\u043C \u0440\u0435\u0448\u0438\u0442, \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u044C \u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043D\u0435 \u043F\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0439 \u0442\u0430\u043A\u043E\u0435 \u0432 \u0434\u043E\u0440\u043E\u0433\u043E\u0439 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 ask \u0438 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u0438 \u0432 \u043E\u0442\u0447\u0451\u0442\u0435 \u043E \u0441\u0434\u0430\u0447\u0435).",
|
|
12193
13434
|
"\u041F\u0440\u0430\u0432\u0438\u043B\u0430: \u0442\u0440\u043E\u0433\u0430\u0435\u0448\u044C UI \u2014 \u0421\u041D\u0410\u0427\u0410\u041B\u0410 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438, \u0435\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0441\u043A\u0438\u043B\u043B \u0441 UI-\u043F\u0440\u0430\u0432\u0438\u043B\u0430\u043C\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430, \u0438 \u0441\u043B\u0435\u0434\u0443\u0439 \u0435\u0433\u043E \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435 (\u043D\u0435\u0442 \u043D\u0443\u0436\u043D\u043E\u0433\u043E \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0430 \u2014 \u0437\u0430\u0432\u0435\u0434\u0438 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443, \u043D\u0435 \u0445\u0430\u0440\u0434\u043A\u043E\u0434\u044C \u043E\u0434\u043D\u043E\u0440\u0430\u0437\u043E\u0432\u043E\u0435); \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0441\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0438; \u043F\u043E\u0441\u043B\u0435 UI-\u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u0441\u043A\u0440\u0438\u043D\u044B mobile+desktop \u041A \u0417\u0410\u0414\u0410\u0427\u0415 (\u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 screenshots \u0443 done); \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0447\u0435\u0440\u0435\u0437 gh \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url).",
|
|
12194
13435
|
// TASWM-293: живой случай SCE-5 — ревьюер заблокировал PR за отсутствие CI (у проекта
|
|
12195
13436
|
// его нет и не предполагается), автора разбудили «закрывать замечания», замечаний по коду
|
|
@@ -12206,7 +13447,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
12206
13447
|
`\u0412\u0435\u0434\u0438 \u0434\u043D\u0435\u0432\u043D\u0438\u043A \u0437\u0430\u0434\u0430\u0447\u0438: \u0437\u0430\u0432\u0435\u0434\u0438 \`journal/${run.task_slug}.md\` \u0438 \u043F\u043E \u0445\u043E\u0434\u0443 \u0440\u0430\u0431\u043E\u0442\u044B (\u043D\u0435 \u043E\u0434\u043D\u0438\u043C \u043C\u0430\u0445\u043E\u043C \u0432 \u043A\u043E\u043D\u0446\u0435) \u0434\u043E\u043F\u0438\u0441\u044B\u0432\u0430\u0439 \u0442\u0443\u0434\u0430 \u0448\u0430\u0433\u0438 \u0438 \u043F\u0440\u0438\u043D\u044F\u0442\u044B\u0435 \u0440\u0435\u0448\u0435\u043D\u0438\u044F \u2014 \u044D\u0442\u043E \u0445\u0440\u043E\u043D\u0438\u043A\u0430 \u0434\u043B\u044F \u0440\u0435\u0442\u0440\u043E\u0441\u043F\u0435\u043A\u0442\u0438\u0432\u044B, \u0441\u0432\u043E\u0439 \u0444\u0430\u0439\u043B \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0434\u0440\u0430\u0442\u044C\u0441\u044F \u0437\u0430 \u043E\u0431\u0449\u0438\u0439 \u0441 \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u044B\u043C\u0438 \u0434\u0440\u043E\u043D\u0430\u043C\u0438. \u041A\u043E\u043C\u043C\u0438\u0442\u044C \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u043C\u0438 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F\u043C\u0438 PR, \u043E\u0431\u044B\u0447\u043D\u044B\u043C \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u043C. \u041D\u0435 \u0434\u0443\u0431\u043B\u0438\u0440\u0443\u0439 \u0441 summary \u0432 done(): \u0434\u043D\u0435\u0432\u043D\u0438\u043A \u2014 \u0441\u044B\u0440\u0430\u044F \u0445\u0440\u043E\u043D\u0438\u043A\u0430 (\u0447\u0442\u043E \u043F\u0440\u043E\u0431\u043E\u0432\u0430\u043B, \u0447\u0442\u043E \u043E\u0442\u0431\u0440\u043E\u0441\u0438\u043B \u0438 \u043F\u043E\u0447\u0435\u043C\u0443), summary \u2014 \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u0439 \u0438\u0442\u043E\u0433 \u0434\u043B\u044F \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430; \u0435\u0441\u043B\u0438 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0443\u0436\u0435 \u0440\u0430\u0441\u043F\u0438\u0441\u0430\u043D\u043E \u0432 \u0434\u043D\u0435\u0432\u043D\u0438\u043A\u0435, \u0432 summary \u043F\u0440\u043E\u0441\u0442\u043E \u0441\u043E\u0448\u043B\u0438\u0441\u044C \u043D\u0430 \u043D\u0435\u0433\u043E.`,
|
|
12207
13448
|
"",
|
|
12208
13449
|
"\u0421\u041E\u0425\u0420\u0410\u041D\u042F\u0419 \u0420\u0410\u0411\u041E\u0422\u0423 \u041F\u041E \u0425\u041E\u0414\u0423 \u2014 \u0438\u043D\u0430\u0447\u0435 \u043E\u043D\u0430 \u043F\u0440\u043E\u043F\u0430\u0434\u0451\u0442, \u0435\u0441\u043B\u0438 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0443\u0431\u044C\u044E\u0442:",
|
|
12209
|
-
`\u2022 \u041F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0437\u043D\u0430\u0447\u0438\u043C\u043E\u0433\u043E \u0448\u0430\u0433\u0430 \u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0438 \u0441\u0440\u0430\u0437\u0443 \u043F\u0443\u0448\u044C: git push -u origin ${run.
|
|
13450
|
+
`\u2022 \u041F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0437\u043D\u0430\u0447\u0438\u043C\u043E\u0433\u043E \u0448\u0430\u0433\u0430 \u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0438 \u0441\u0440\u0430\u0437\u0443 \u043F\u0443\u0448\u044C: git push -u origin ${run.branch || run.task_slug}. \u041D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0438\u0440\u0430\u0435\u0442 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0434\u0440\u043E\u043D\u043E\u043C \u2014 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0438 \u043F\u0440\u0430\u0432\u043A\u0438 \u0432 worktree \u043D\u0435 \u043F\u0435\u0440\u0435\u0436\u0438\u0432\u0443\u0442 \u0441\u043C\u0435\u0440\u0442\u044C \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430.`,
|
|
12210
13451
|
"\u2022 \u041D\u0435 \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439 \u0434\u043E\u043B\u0433\u0438\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0432 \u0444\u043E\u043D\u0435 \xAB\u0447\u0442\u043E\u0431\u044B \u043F\u043E\u0434\u043E\u0436\u0434\u0430\u0442\u044C\xBB: \u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u0442\u0441\u044F, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u0451\u0448\u044C \u0437\u0432\u0430\u0442\u044C \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B, \u0438 \u0444\u043E\u043D\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043D\u0435 \u0441\u043F\u0430\u0441\u0451\u0442. \u0414\u043E\u043B\u0433\u043E\u0435 \u0434\u0435\u0440\u0436\u0438 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u043C \u0432\u044B\u0437\u043E\u0432\u043E\u043C; \u0434\u043B\u0438\u043D\u043D\u0443\u044E \u0441\u0435\u0440\u0438\u044E \u0434\u0440\u043E\u0431\u0438 \u043D\u0430 \u0447\u0430\u0441\u0442\u0438 \u0438 \u043A\u043E\u043C\u043C\u0438\u0442\u044C+\u043F\u0443\u0448\u044C \u043C\u0435\u0436\u0434\u0443 \u043D\u0438\u043C\u0438.",
|
|
12211
13452
|
'\u2022 \u041F\u0435\u0440\u0435\u0434 \u0434\u043E\u043B\u0433\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 (\u0442\u0435\u0441\u0442\u044B, \u0441\u0431\u043E\u0440\u043A\u0430 \u2014 \u043C\u0438\u043D\u0443\u0442\u044B \u0431\u0435\u0437 \u0432\u044B\u0432\u043E\u0434\u0430) \u043A\u0438\u043D\u044C report_step("\u0433\u043E\u043D\u044E \u0442\u0435\u0441\u0442\u044B\u2026") \u2014 \u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E (\u0440\u043E\u0439 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E \u043E\u0442\u0441\u043B\u0435\u0436\u0438\u0432\u0430\u0435\u0442 \u0436\u0438\u0432\u043E\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043A\u043E\u043C\u0430\u043D\u0434\u044B), \u043D\u043E \u043F\u043E\u043B\u0435\u0437\u043D\u0435\u0435 \u0434\u043B\u044F \u0432\u0438\u0434\u0438\u043C\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430 \u0432 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u0435 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430.',
|
|
12212
13453
|
"",
|
|
@@ -12231,7 +13472,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
12231
13472
|
}
|
|
12232
13473
|
return parts.join("\n");
|
|
12233
13474
|
}
|
|
12234
|
-
var HERO_CHARTER_PATH =
|
|
13475
|
+
var HERO_CHARTER_PATH = join28(dirname5(THIS_FILE), "..", "docs", "HERO.md");
|
|
12235
13476
|
function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
|
|
12236
13477
|
const name = run.name || `#${run.id}`;
|
|
12237
13478
|
const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
|
|
@@ -12485,8 +13726,8 @@ async function wakeRun(deps, runId) {
|
|
|
12485
13726
|
if (!run) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0437\u0435\u0440\u043A\u0430\u043B\u0435 \u0440\u043E\u044F` };
|
|
12486
13727
|
if (run.pid != null && deps.isAlive(run.pid)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 (pid ${run.pid}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
|
|
12487
13728
|
if (isTerminalStatus(run.status)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435\u043D (${run.status}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
|
|
12488
|
-
const worktreeExists = deps.worktreeExists ??
|
|
12489
|
-
const hasTranscript = deps.hasTranscript ??
|
|
13729
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
13730
|
+
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
12490
13731
|
const wt = run.worktree_path;
|
|
12491
13732
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
12492
13733
|
const isService = run.reviews_run_id != null || run.resolves_run_id != null;
|
|
@@ -12570,9 +13811,9 @@ function repoHasCi(deps, run) {
|
|
|
12570
13811
|
function repoHasCiWorkflows(worktreePath, repo) {
|
|
12571
13812
|
for (const base3 of [worktreePath, repo]) {
|
|
12572
13813
|
if (!base3) continue;
|
|
12573
|
-
const dir =
|
|
13814
|
+
const dir = join28(base3, ".github", "workflows");
|
|
12574
13815
|
try {
|
|
12575
|
-
if (
|
|
13816
|
+
if (readdirSync12(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
|
|
12576
13817
|
} catch {
|
|
12577
13818
|
}
|
|
12578
13819
|
}
|
|
@@ -12719,8 +13960,8 @@ async function wakeReviewerOnCiReady(deps, run, over) {
|
|
|
12719
13960
|
return true;
|
|
12720
13961
|
}
|
|
12721
13962
|
function worktreeAndTranscriptAlive(deps, worktree) {
|
|
12722
|
-
const exists = deps.worktreeExists ??
|
|
12723
|
-
const transcript = deps.hasTranscript ??
|
|
13963
|
+
const exists = deps.worktreeExists ?? existsSync16;
|
|
13964
|
+
const transcript = deps.hasTranscript ?? executorHasSession;
|
|
12724
13965
|
return exists(worktree) && transcript(worktree);
|
|
12725
13966
|
}
|
|
12726
13967
|
async function spawnFreshReviewerAfterStall(deps, run, why) {
|
|
@@ -12823,6 +14064,22 @@ async function envGateAllows(deps, run) {
|
|
|
12823
14064
|
function effectiveRepo(deps, run) {
|
|
12824
14065
|
return normalizeRepo(run.repo || deps.repoFallback || "");
|
|
12825
14066
|
}
|
|
14067
|
+
async function zoneConflictFor(db, models, activeFiles, run, repo, repoFallback) {
|
|
14068
|
+
const model = await models.forRepo(repo);
|
|
14069
|
+
if (!hasZoneModel(model)) return { kind: "unknown" };
|
|
14070
|
+
const live = db.listRuns().filter((r) => (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status));
|
|
14071
|
+
activeFiles.prune(live.map((r) => r.id));
|
|
14072
|
+
const active = [];
|
|
14073
|
+
for (const r of live) {
|
|
14074
|
+
if (r.id === run.id) continue;
|
|
14075
|
+
if (normalizeRepo(r.repo || repoFallback || "") !== repo) continue;
|
|
14076
|
+
const files = await activeFiles.filesOf(r);
|
|
14077
|
+
active.push(
|
|
14078
|
+
files.length ? { run: r, files, actual: true } : { run: r, files: predictForRun(model, r).map((p) => p.path), actual: false }
|
|
14079
|
+
);
|
|
14080
|
+
}
|
|
14081
|
+
return decideConflict({ model, candidate: run, active });
|
|
14082
|
+
}
|
|
12826
14083
|
function activeDronesOnRepo(deps, repo, exceptRunId) {
|
|
12827
14084
|
return deps.db.listRuns().filter(
|
|
12828
14085
|
(r) => r.id !== exceptRunId && (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status) && effectiveRepo(deps, r) === repo
|
|
@@ -12863,10 +14120,10 @@ async function exclusiveGateAllows(deps, run) {
|
|
|
12863
14120
|
return true;
|
|
12864
14121
|
}
|
|
12865
14122
|
function pruneGateLogs(deps) {
|
|
12866
|
-
if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size) return;
|
|
14123
|
+
if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size && !deps.overlapLogged?.size) return;
|
|
12867
14124
|
const open = /* @__PURE__ */ new Set();
|
|
12868
14125
|
for (const run of deps.db.listRuns()) if (!isTerminalStatus(run.status)) open.add(run.id);
|
|
12869
|
-
for (const map of [deps.envGateLogged, deps.exclusiveLogged]) {
|
|
14126
|
+
for (const map of [deps.envGateLogged, deps.exclusiveLogged, deps.overlapLogged]) {
|
|
12870
14127
|
if (!map) continue;
|
|
12871
14128
|
for (const id of [...map.keys()]) if (!open.has(id)) map.delete(id);
|
|
12872
14129
|
}
|
|
@@ -12887,6 +14144,18 @@ async function exclusivityBlockedRunIds(deps) {
|
|
|
12887
14144
|
}
|
|
12888
14145
|
return blocked;
|
|
12889
14146
|
}
|
|
14147
|
+
async function fillSuggestedArea(deps, run) {
|
|
14148
|
+
if (run.area || !deps.suggestArea) return;
|
|
14149
|
+
const guess = await deps.suggestArea(run).catch(() => null);
|
|
14150
|
+
if (!guess) return;
|
|
14151
|
+
deps.db.updateRun(run.id, { area: guess.zone });
|
|
14152
|
+
run.area = guess.zone;
|
|
14153
|
+
deps.db.addEvent(
|
|
14154
|
+
run.id,
|
|
14155
|
+
"step",
|
|
14156
|
+
`\u0437\u043E\u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0440\u0435\u043F\u043E: ${guess.zone} (${Math.round(guess.confidence * 100)}% \u2014 ${guess.why}) \u2014 TASWM-348`
|
|
14157
|
+
);
|
|
14158
|
+
}
|
|
12890
14159
|
async function trySpawnQueuedRun(deps, run) {
|
|
12891
14160
|
if (deps.control?.paused) return "skipped";
|
|
12892
14161
|
const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
|
|
@@ -12902,11 +14171,21 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
12902
14171
|
if (!await exclusiveGateAllows(deps, run)) return "skipped";
|
|
12903
14172
|
if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
|
|
12904
14173
|
if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
|
|
14174
|
+
await fillSuggestedArea(deps, run);
|
|
12905
14175
|
try {
|
|
12906
14176
|
await deps.spawn(run);
|
|
12907
14177
|
narrate(deps, `\u0411\u0435\u0440\u0443 ${run.task_slug} \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0441\u043F\u0430\u0432\u043D\u044E ${droneLabel(run)}.`);
|
|
12908
14178
|
return "spawned";
|
|
12909
14179
|
} catch (e) {
|
|
14180
|
+
if (e instanceof FileOverlapError) {
|
|
14181
|
+
const logged = deps.overlapLogged ??= /* @__PURE__ */ new Map();
|
|
14182
|
+
if (logged.get(run.id) !== e.message) {
|
|
14183
|
+
logged.set(run.id, e.message);
|
|
14184
|
+
deps.db.addEvent(run.id, "step", `${e.message} \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0441\u0442\u0430\u0451\u0442\u0441\u044F \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 (TASWM-348)`);
|
|
14185
|
+
narrate(deps, `${droneLabel(run)} \u2014 ${e.message}. \u041D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u043E.`);
|
|
14186
|
+
}
|
|
14187
|
+
return "skipped";
|
|
14188
|
+
}
|
|
12910
14189
|
if (e instanceof AreaBusyError) return "skipped";
|
|
12911
14190
|
deps.db.updateRun(run.id, { status: "failed" });
|
|
12912
14191
|
deps.db.addEvent(run.id, "failed", String(e));
|
|
@@ -12933,8 +14212,8 @@ async function resumeWatchdogParked(deps, droneCap) {
|
|
|
12933
14212
|
}
|
|
12934
14213
|
if (!candidate) return;
|
|
12935
14214
|
if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
|
|
12936
|
-
const worktreeExists = deps.worktreeExists ??
|
|
12937
|
-
const hasTranscript = deps.hasTranscript ??
|
|
14215
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
14216
|
+
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
12938
14217
|
const wt = candidate.worktree_path;
|
|
12939
14218
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
12940
14219
|
const mode = canContinue ? "continue" : "fresh";
|
|
@@ -13102,8 +14381,8 @@ async function manageRelief(deps, now, droneCap, stormActive) {
|
|
|
13102
14381
|
if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
|
|
13103
14382
|
if (!deps.respawn) return;
|
|
13104
14383
|
const wt = resumeCandidate.worktree_path;
|
|
13105
|
-
const worktreeExists = deps.worktreeExists ??
|
|
13106
|
-
const hasTranscript = deps.hasTranscript ??
|
|
14384
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
14385
|
+
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
13107
14386
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
13108
14387
|
const mode = canContinue ? "continue" : "fresh";
|
|
13109
14388
|
if (mode === "continue" && wt && deps.reclaimWorktree) {
|
|
@@ -13226,15 +14505,28 @@ ${journal}`);
|
|
|
13226
14505
|
deps.db.addEvent(run.id, "step", `\u0434\u0435\u043C\u043E\u043D-\u043E\u0442\u0447\u0451\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 ${run.task_slug} \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${e instanceof Error ? e.message : String(e)}`);
|
|
13227
14506
|
});
|
|
13228
14507
|
}
|
|
14508
|
+
function syncBranchFromPr(db, run, headBranch) {
|
|
14509
|
+
const fix = branchFromPrPatch(run.branch, headBranch);
|
|
14510
|
+
if (!fix) return run;
|
|
14511
|
+
const updated = db.updateRun(run.id, fix.patch) ?? run;
|
|
14512
|
+
db.addEvent(run.id, "step", fix.event);
|
|
14513
|
+
return updated;
|
|
14514
|
+
}
|
|
14515
|
+
async function worktreeBranchFor(run, prHead) {
|
|
14516
|
+
if (run.branch) return run.branch;
|
|
14517
|
+
const fromPr = run.pr_url && prHead ? await prHead(run.pr_url).catch(() => null) : null;
|
|
14518
|
+
return fromPr || run.task_slug;
|
|
14519
|
+
}
|
|
13229
14520
|
async function adoptDroneToReview(deps, run, prUrl) {
|
|
13230
14521
|
const t = applyReviewTransition(deps.db, run, prUrl);
|
|
14522
|
+
const adopted = syncBranchFromPr(deps.db, run, deps.prHeadBranch ? await deps.prHeadBranch(prUrl).catch(() => null) : null);
|
|
13231
14523
|
deps.db.addEvent(run.id, "pr_opened", prUrl);
|
|
13232
14524
|
deps.db.addEvent(
|
|
13233
14525
|
run.id,
|
|
13234
14526
|
"step",
|
|
13235
14527
|
t.isFixIteration ? `\u0434\u0440\u043E\u043D \u043E\u0442\u043A\u0440\u044B\u043B PR \u043D\u0430 \u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0438 \u0438 \u0443\u043C\u0435\u0440 \u0434\u043E done() \u2014 \u0443\u0441\u044B\u043D\u043E\u0432\u043B\u0451\u043D \u043D\u0430 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E #${t.cycle} (\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB \u0441\u0431\u0440\u043E\u0448\u0435\u043D, pr_url \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0438\u0437 gh): ${prUrl}` : `\u0434\u0440\u043E\u043D \u043E\u0442\u043A\u0440\u044B\u043B PR \u043F\u0435\u0440\u0435\u0434 \u0441\u043C\u0435\u0440\u0442\u044C\u044E \u2014 \u0443\u0441\u044B\u043D\u043E\u0432\u043B\u0451\u043D \u043D\u0430 \u0440\u0435\u0432\u044C\u044E (pr_url \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0438\u0437 gh): ${prUrl}`
|
|
13236
14528
|
);
|
|
13237
|
-
await postAdoptionReport(deps,
|
|
14529
|
+
await postAdoptionReport(deps, adopted, prUrl, t.cycle ?? run.review_cycle ?? 0);
|
|
13238
14530
|
}
|
|
13239
14531
|
async function postScrapedVerdictReport(deps, runId, taskSlug, verdict, droneName, workspaceId, cycle) {
|
|
13240
14532
|
if (!deps.reportComment) return;
|
|
@@ -13264,8 +14556,8 @@ function buildEscalationCandidates(db) {
|
|
|
13264
14556
|
}
|
|
13265
14557
|
async function tickOnce(deps) {
|
|
13266
14558
|
const now = deps.now ? deps.now() : Date.now();
|
|
13267
|
-
const worktreeExists = deps.worktreeExists ??
|
|
13268
|
-
const hasTranscript = deps.hasTranscript ??
|
|
14559
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
14560
|
+
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
13269
14561
|
if (deps.rehydrate) {
|
|
13270
14562
|
const rh = deps.rehydrate;
|
|
13271
14563
|
const sinceLast = rh.lastAt == null ? Infinity : now - rh.lastAt;
|
|
@@ -13391,7 +14683,7 @@ async function tickOnce(deps) {
|
|
|
13391
14683
|
if (!stormActive) {
|
|
13392
14684
|
await resumeWatchdogParked(deps, droneCap);
|
|
13393
14685
|
}
|
|
13394
|
-
for (
|
|
14686
|
+
for (let run of deps.db.listRuns("review")) {
|
|
13395
14687
|
try {
|
|
13396
14688
|
if (run.resolves_run_id != null) {
|
|
13397
14689
|
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
@@ -13410,7 +14702,9 @@ async function tickOnce(deps) {
|
|
|
13410
14702
|
continue;
|
|
13411
14703
|
}
|
|
13412
14704
|
if (!run.pr_url) continue;
|
|
13413
|
-
const
|
|
14705
|
+
const prUrl = run.pr_url;
|
|
14706
|
+
const st = await deps.prStatus(prUrl);
|
|
14707
|
+
run = syncBranchFromPr(deps.db, run, st.headBranch);
|
|
13414
14708
|
if (st.merged) {
|
|
13415
14709
|
await finalizeMergedReview(deps, run);
|
|
13416
14710
|
narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${run.task_slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
|
|
@@ -13479,11 +14773,13 @@ async function tickOnce(deps) {
|
|
|
13479
14773
|
continue;
|
|
13480
14774
|
}
|
|
13481
14775
|
const attempts = deps.db.reviewerAttemptCountFor(run.id);
|
|
14776
|
+
const runEvents = deps.db.events(run.id);
|
|
14777
|
+
const unrecoverableMark = reviewUnrecoverableMark(run.branch || run.task_slug);
|
|
13482
14778
|
let verdictFromComment = null;
|
|
13483
14779
|
let lastAttempt = null;
|
|
13484
14780
|
if (attempts > 0 && deps.verdictComment) {
|
|
13485
14781
|
lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
|
|
13486
|
-
verdictFromComment = await deps.verdictComment(
|
|
14782
|
+
verdictFromComment = await deps.verdictComment(prUrl, lastAttempt?.created_at ?? run.created_at).catch(() => null);
|
|
13487
14783
|
}
|
|
13488
14784
|
if (verdictFromComment) {
|
|
13489
14785
|
const mark = verdictCommentMark(verdictFromComment.createdAt);
|
|
@@ -13498,15 +14794,16 @@ async function tickOnce(deps) {
|
|
|
13498
14794
|
await wakeAuthorOnChanges(deps, run, now, droneCap);
|
|
13499
14795
|
}
|
|
13500
14796
|
}
|
|
14797
|
+
} else if (runEvents.some((e) => (e.text ?? "").includes(unrecoverableMark))) {
|
|
13501
14798
|
} else if (attempts >= REVIEW_SPAWN_LIMIT) {
|
|
13502
|
-
if (!
|
|
14799
|
+
if (!runEvents.some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
|
|
13503
14800
|
deps.db.addEvent(run.id, "step", `${REVIEW_ESCALATION_MARK}: ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u0440\u0435\u0432\u044C\u044E \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
|
|
13504
14801
|
narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F \u0437\u0430 ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`);
|
|
13505
14802
|
}
|
|
13506
14803
|
} else {
|
|
13507
14804
|
const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
|
|
13508
14805
|
if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk) {
|
|
13509
|
-
const freshSt = await deps.prStatus(
|
|
14806
|
+
const freshSt = await deps.prStatus(prUrl);
|
|
13510
14807
|
if (freshSt.merged) {
|
|
13511
14808
|
await finalizeMergedReview(deps, run);
|
|
13512
14809
|
narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E, \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
|
|
@@ -13519,11 +14816,21 @@ async function tickOnce(deps) {
|
|
|
13519
14816
|
} else if (freshSt.conflicting || freshSt.isDraft) {
|
|
13520
14817
|
deps.db.addEvent(run.id, "step", "PR \u0441\u0442\u0430\u043B \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u044E\u0449\u0438\u043C/\u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u043E\u043C, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D \u0434\u043E \u0441\u043B\u0435\u0434. \u0442\u0438\u043A\u0430");
|
|
13521
14818
|
} else {
|
|
14819
|
+
run = syncBranchFromPr(deps.db, run, freshSt.headBranch);
|
|
13522
14820
|
try {
|
|
13523
14821
|
await deps.spawnReviewer(run, st.files ?? []);
|
|
13524
14822
|
narrate(deps, `PR ${droneLabel(run)} \u0433\u043E\u0442\u043E\u0432 \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430.`);
|
|
13525
14823
|
} catch (e) {
|
|
13526
|
-
|
|
14824
|
+
if (isUnrecoverableRefError(e)) {
|
|
14825
|
+
deps.db.addEvent(
|
|
14826
|
+
run.id,
|
|
14827
|
+
"step",
|
|
14828
|
+
`${unrecoverableMark}: ${String(e)} \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`
|
|
14829
|
+
);
|
|
14830
|
+
narrate(deps, `\u0420\u0435\u0432\u044C\u044E\u0435\u0440\u0430 ${droneLabel(run)} \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u044C: \u0432\u0435\u0442\u043A\u0438 PR \u043D\u0435\u0442 \u043D\u0430 origin \u2014 \u0437\u043E\u0432\u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0441\u0440\u0430\u0437\u0443, \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u043D\u0435 \u0436\u0433\u0443.`);
|
|
14831
|
+
} else {
|
|
14832
|
+
deps.db.addEvent(run.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u043E\u0441\u044C: ${String(e)}`);
|
|
14833
|
+
}
|
|
13527
14834
|
}
|
|
13528
14835
|
}
|
|
13529
14836
|
}
|
|
@@ -13790,7 +15097,9 @@ async function tickOnce(deps) {
|
|
|
13790
15097
|
// resource-gate-stall (TASWM-212): та же причина, что видит лог manageRelief — один
|
|
13791
15098
|
// deps.gateReason() на тик (кэш hostReading() внутри resources.ts, лишнего спавна нет).
|
|
13792
15099
|
...deps.gateReason ? { gateReason: deps.gateReason() } : {},
|
|
13793
|
-
...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {}
|
|
15100
|
+
...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {},
|
|
15101
|
+
// Рестарт не переигрывает падения (TAS-485) — см. closedBeforeStart в anomalies.ts.
|
|
15102
|
+
...az.startedAt != null ? { daemonStartedAt: az.startedAt } : {}
|
|
13794
15103
|
}),
|
|
13795
15104
|
...buildEscalationCandidates(deps.db),
|
|
13796
15105
|
// ЦЕНА РАЗГРУЗКИ (TASWM-316): «машина не тянет» с числами и рекомендацией — та же
|
|
@@ -13820,6 +15129,20 @@ async function tickOnce(deps) {
|
|
|
13820
15129
|
}
|
|
13821
15130
|
narrate(deps, `\u26A0\uFE0F ${signal.title}`);
|
|
13822
15131
|
}
|
|
15132
|
+
const stale = az.tracker.pendingResolved();
|
|
15133
|
+
if (stale.length > 0) {
|
|
15134
|
+
if (!az.resolve) {
|
|
15135
|
+
az.tracker.forgetResolved(stale);
|
|
15136
|
+
} else {
|
|
15137
|
+
const batch = stale.slice(0, ANOMALY_RESOLVE_BATCH);
|
|
15138
|
+
try {
|
|
15139
|
+
await az.resolve(batch);
|
|
15140
|
+
az.tracker.forgetResolved(batch);
|
|
15141
|
+
} catch (e) {
|
|
15142
|
+
console.error("[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0433\u0430\u0448\u0435\u043D\u0438\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0432\u0448\u0438\u0445\u0441\u044F \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u0439 \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u043E, \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:", e);
|
|
15143
|
+
}
|
|
15144
|
+
}
|
|
15145
|
+
}
|
|
13823
15146
|
}
|
|
13824
15147
|
if (deps.watchdog) {
|
|
13825
15148
|
try {
|
|
@@ -13885,7 +15208,7 @@ async function cancelRun(deps, slug, reason) {
|
|
|
13885
15208
|
}
|
|
13886
15209
|
async function resolveConflict(deps, reviewRun) {
|
|
13887
15210
|
const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
|
|
13888
|
-
const branch = reviewRun
|
|
15211
|
+
const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
|
|
13889
15212
|
const resolver = deps.db.createRun({
|
|
13890
15213
|
task_slug: reviewRun.task_slug,
|
|
13891
15214
|
repo,
|
|
@@ -13895,7 +15218,7 @@ async function resolveConflict(deps, reviewRun) {
|
|
|
13895
15218
|
workspace_id: reviewRun.workspace_id
|
|
13896
15219
|
// та же задача, тот же воркспейс (TASWM-288)
|
|
13897
15220
|
});
|
|
13898
|
-
const wt =
|
|
15221
|
+
const wt = join28(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
|
|
13899
15222
|
try {
|
|
13900
15223
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
13901
15224
|
await deps.git.setWorktreeIdentity(wt, resolver.name);
|
|
@@ -13925,14 +15248,14 @@ async function resolveConflict(deps, reviewRun) {
|
|
|
13925
15248
|
await deps.git.removeWorktree(repo, wt).catch(() => {
|
|
13926
15249
|
});
|
|
13927
15250
|
deps.db.updateRun(resolver.id, { status: "failed" });
|
|
13928
|
-
const marker = isTransientResourceError(e) ? "\u041E\u0421 \u043D\u0435 \u0434\u0430\u0451\u0442 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C git-\u043F\u0440\u043E\u0446\u0435\u0441\u0441\u044B (\u0440\u0435\u0441\u0443\u0440\u0441, \u043D\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442)" : "merge-main-in \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F";
|
|
15251
|
+
const marker = isUnrecoverableRefError(e) ? "\u0432\u0435\u0442\u043A\u0438 PR \u043D\u0435\u0442 \u043D\u0430 origin \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A" : isTransientResourceError(e) ? "\u041E\u0421 \u043D\u0435 \u0434\u0430\u0451\u0442 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C git-\u043F\u0440\u043E\u0446\u0435\u0441\u0441\u044B (\u0440\u0435\u0441\u0443\u0440\u0441, \u043D\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442)" : "merge-main-in \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F";
|
|
13929
15252
|
deps.db.addEvent(resolver.id, "failed", `${marker}: ${String(e)}`);
|
|
13930
15253
|
throw e;
|
|
13931
15254
|
}
|
|
13932
15255
|
}
|
|
13933
15256
|
async function spawnReviewer(deps, reviewRun, files) {
|
|
13934
15257
|
const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
|
|
13935
|
-
const branch = reviewRun
|
|
15258
|
+
const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
|
|
13936
15259
|
const priorReview = reviewRun.review_cycle > 0 ? lastChangesSummary(deps.db, reviewRun.id) : null;
|
|
13937
15260
|
const verifyMode = deps.verifyMode ? await deps.verifyMode(repo).catch(() => void 0) : void 0;
|
|
13938
15261
|
const checklist = buildReviewChecklist(reviewRun, files, priorReview, verifyMode);
|
|
@@ -13947,7 +15270,7 @@ async function spawnReviewer(deps, reviewRun, files) {
|
|
|
13947
15270
|
// (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
|
|
13948
15271
|
workspace_id: reviewRun.workspace_id
|
|
13949
15272
|
});
|
|
13950
|
-
const wt =
|
|
15273
|
+
const wt = join28(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
|
|
13951
15274
|
try {
|
|
13952
15275
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
13953
15276
|
await deps.git.setWorktreeIdentity(wt, reviewer.name);
|
|
@@ -13982,7 +15305,7 @@ async function respawnRun(deps, run, mode, nudge) {
|
|
|
13982
15305
|
]);
|
|
13983
15306
|
const base3 = effectiveBaseBranch(run.base_branch, projectBase);
|
|
13984
15307
|
const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
|
|
13985
|
-
if (!
|
|
15308
|
+
if (!existsSync16(wt)) {
|
|
13986
15309
|
const branch = runToSpawn.branch || runToSpawn.task_slug;
|
|
13987
15310
|
const source = await addWorktree(repo, wt, branch, {
|
|
13988
15311
|
...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
|
|
@@ -14011,7 +15334,7 @@ ${notice}` : freshPrompt;
|
|
|
14011
15334
|
deps.db.updateRun(run.id, { status: "running", pid });
|
|
14012
15335
|
}
|
|
14013
15336
|
async function syncCrashedOnStartup(deps) {
|
|
14014
|
-
const worktreeExists = deps.worktreeExists ??
|
|
15337
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
14015
15338
|
let orphaned = 0;
|
|
14016
15339
|
let merged = 0;
|
|
14017
15340
|
let requeued = 0;
|
|
@@ -14074,7 +15397,7 @@ async function makeStore(repo, worktreesDir, repoFlag) {
|
|
|
14074
15397
|
console.warn(`[overmind] \u043D\u0435 \u0441\u043C\u043E\u0433 \u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u044C \u043F\u0443\u0442\u044C \u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044E (${e instanceof Error ? e.message : e}) \u2014 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0437\u0430\u043F\u0443\u0441\u043A \u0441\u043D\u043E\u0432\u0430 \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u0442 --repo`);
|
|
14075
15398
|
}
|
|
14076
15399
|
}
|
|
14077
|
-
const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW,
|
|
15400
|
+
const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW, homedir5(), repoPaths);
|
|
14078
15401
|
const repoMap = emptyRepoMapState(scanRootsList);
|
|
14079
15402
|
if (SWARM_BACKEND !== "client") {
|
|
14080
15403
|
const { SwarmDb: SwarmDb2 } = await loadSqlite();
|
|
@@ -14148,10 +15471,10 @@ async function migrateWorktreesAtStartup() {
|
|
|
14148
15471
|
legacyDir: WORKTREES_LEGACY_DIR,
|
|
14149
15472
|
targetDir: WORKTREES_DIR,
|
|
14150
15473
|
envOverride: process.env.SWARM_WORKTREES || "",
|
|
14151
|
-
exists: (d) =>
|
|
15474
|
+
exists: (d) => existsSync16(d),
|
|
14152
15475
|
isNonEmptyDir: (d) => {
|
|
14153
15476
|
try {
|
|
14154
|
-
return
|
|
15477
|
+
return readdirSync12(d).length > 0;
|
|
14155
15478
|
} catch {
|
|
14156
15479
|
return false;
|
|
14157
15480
|
}
|
|
@@ -14163,7 +15486,7 @@ async function migrateWorktreesAtStartup() {
|
|
|
14163
15486
|
});
|
|
14164
15487
|
} catch (e) {
|
|
14165
15488
|
console.error("[migrate] \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u044F worktrees \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C \u2014 \u0444\u043E\u043B\u0431\u044D\u043A:", e);
|
|
14166
|
-
return
|
|
15489
|
+
return existsSync16(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
|
|
14167
15490
|
}
|
|
14168
15491
|
}
|
|
14169
15492
|
async function reportWorkspaceAccess(tl, db) {
|
|
@@ -14233,18 +15556,23 @@ async function runDaemon(opts) {
|
|
|
14233
15556
|
overmindEntry: THIS_FILE,
|
|
14234
15557
|
dbPath: DB_PATH,
|
|
14235
15558
|
runLogsDir: RUN_LOGS_DIR,
|
|
15559
|
+
// Кем исполнять (TASWM-349) — настройка SWARM_EXECUTOR, один резолв на весь демон.
|
|
15560
|
+
executor: EXECUTOR,
|
|
14236
15561
|
droneIsolation: DRONE_ISOLATION,
|
|
14237
15562
|
channel: droneChannelLaunch,
|
|
14238
15563
|
droneCap: () => lastDroneCap
|
|
14239
15564
|
});
|
|
14240
15565
|
let prepareReader;
|
|
15566
|
+
const zoneModels = new ZoneModels({ manualAreas: () => new Map(db.listRuns().map((r) => [r.task_slug, r.area])) });
|
|
15567
|
+
const activeFiles = new ActiveFilesCache((r) => changedPaths(r.worktree_path, { baseBranch: r.base_branch ?? null }));
|
|
14241
15568
|
const baseSpawn = {
|
|
14242
15569
|
db,
|
|
14243
15570
|
repo: opts.repo,
|
|
14244
15571
|
// фолбэк, если у прогона repo пуст
|
|
14245
15572
|
worktreesDir,
|
|
14246
15573
|
launch,
|
|
14247
|
-
prepare: (repo) => prepareReader ? prepareReader(repo) : Promise.resolve(null)
|
|
15574
|
+
prepare: (repo) => prepareReader ? prepareReader(repo) : Promise.resolve(null),
|
|
15575
|
+
conflicts: (run, repo) => zoneConflictFor(db, zoneModels, activeFiles, run, repo, opts.repo)
|
|
14248
15576
|
};
|
|
14249
15577
|
let intake;
|
|
14250
15578
|
if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_MAX_IN_FLIGHT > 0) {
|
|
@@ -14278,7 +15606,7 @@ async function runDaemon(opts) {
|
|
|
14278
15606
|
const client = db;
|
|
14279
15607
|
heartbeat = {
|
|
14280
15608
|
push: async () => {
|
|
14281
|
-
const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null);
|
|
15609
|
+
const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastDroneCap);
|
|
14282
15610
|
if (reply.commands.length) onHostCommands(reply.commands);
|
|
14283
15611
|
if (reply.logRequests.length) {
|
|
14284
15612
|
await serveLogRequests(
|
|
@@ -14348,7 +15676,14 @@ async function runDaemon(opts) {
|
|
|
14348
15676
|
autoReviewEnabled: SWARM_AUTO_REVIEW,
|
|
14349
15677
|
outboxPendingCount: () => client.pendingCount(),
|
|
14350
15678
|
notify: (signal) => client.notifyAnomaly(signal),
|
|
14351
|
-
|
|
15679
|
+
// TAS-485: условие ушло → гасим свою же тревогу в инбоксе.
|
|
15680
|
+
resolve: (keys) => client.resolveAnomalies(keys),
|
|
15681
|
+
stopQuiet: () => anomalyState.stopQuiet(),
|
|
15682
|
+
// TAS-485: этот демон отвечает за то, что случилось ПРИ НЁМ. Прогон, закрытый до
|
|
15683
|
+
// старта, он не переоткрывает — иначе каждый перезапуск начинает волну заново
|
|
15684
|
+
// (id прогонов в зеркале свои у каждой жизни процесса, durable-кулдаун по ним не
|
|
15685
|
+
// совпадает — см. closedBeforeStart в anomalies.ts).
|
|
15686
|
+
startedAt: Date.now()
|
|
14352
15687
|
};
|
|
14353
15688
|
}
|
|
14354
15689
|
const watchdog = {
|
|
@@ -14421,6 +15756,9 @@ async function runDaemon(opts) {
|
|
|
14421
15756
|
worktreesDir,
|
|
14422
15757
|
launch,
|
|
14423
15758
|
git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
|
|
15759
|
+
// TASWM-356: ветка ревьюера — фактическая ветка PR. Штатно она уже в поле прогона
|
|
15760
|
+
// (реконсайл синхронит её из headBranch), этот ридер — для прогонов без неё.
|
|
15761
|
+
prHeadBranch,
|
|
14424
15762
|
// TASWM-293: ТОТ ЖЕ живой ридер verifyMode, что у исполнителя (spawnDrone) —
|
|
14425
15763
|
// иначе ревьюер судит проект с verifyMode=local по правилам ci и блокирует PR
|
|
14426
15764
|
// за отсутствие CI, которого у проекта нет и не предполагается.
|
|
@@ -14513,7 +15851,7 @@ async function runDaemon(opts) {
|
|
|
14513
15851
|
if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
|
|
14514
15852
|
if (taskClient) await taskClient.startTask(runToSpawn.task_slug);
|
|
14515
15853
|
},
|
|
14516
|
-
prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state })),
|
|
15854
|
+
prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state, headBranch: s.headBranch })),
|
|
14517
15855
|
// Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
|
|
14518
15856
|
// (фолбэк — дефолт-репо роя). Реальный gh; логика усыновления — в tickOnce (тестируется).
|
|
14519
15857
|
openPrsForBranch: (branch, repoDir) => openPrsForBranch(branch, repoDir || opts.repo),
|
|
@@ -14537,6 +15875,8 @@ async function runDaemon(opts) {
|
|
|
14537
15875
|
reportComment: (slug, body, label, ws) => tl.forWorkspace(ws).addComment(slug, body, label)
|
|
14538
15876
|
} : {},
|
|
14539
15877
|
prCommitTitles: (prUrl) => prCommitTitles(prUrl),
|
|
15878
|
+
prHeadBranch: (prUrl) => prHeadBranch(prUrl),
|
|
15879
|
+
// TASWM-356: ветка PR на усыновлении, узким запросом
|
|
14540
15880
|
journalForBranch: (repo, branch, slug) => journalForBranch(repo || opts.repo, branch, slug),
|
|
14541
15881
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p),
|
|
14542
15882
|
// TASWM-303: тот же фолбэк repo, что у removeWorktree/journalForBranch — run.repo пуст
|
|
@@ -14546,15 +15886,17 @@ async function runDaemon(opts) {
|
|
|
14546
15886
|
repoHasCi: (run) => repoHasCiWorkflows(run.worktree_path, run.repo || opts.repo),
|
|
14547
15887
|
isAlive: pidAlive,
|
|
14548
15888
|
// Учёт cost/usage (TASWM-188): реальный аккаунтант поверх RUN_LOGS_DIR + db.
|
|
14549
|
-
accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db }),
|
|
15889
|
+
accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR }),
|
|
14550
15890
|
// Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
|
|
14551
15891
|
// (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
|
|
14552
|
-
exitOutcome: makeExitClassifier(RUN_LOGS_DIR),
|
|
15892
|
+
exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
|
|
14553
15893
|
// Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
|
|
14554
15894
|
livePpids,
|
|
14555
15895
|
// Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
|
|
14556
15896
|
// POST /swarm/kill → stopped в проде → overmind гасит живой pid на реконсайле).
|
|
14557
|
-
|
|
15897
|
+
// TASWM-349: свой способ остановки — за адаптером; не задан (оба сегодняшних) → общий
|
|
15898
|
+
// групповой SIGTERM.
|
|
15899
|
+
kill: (pid) => EXECUTOR.stop ? EXECUTOR.stop(pid, "SIGTERM") : killProcessGroup(pid, "SIGTERM"),
|
|
14558
15900
|
// Реклейм worktree паузнутого/возвращаемого дрона (TASWM-228): тот же cwd-скан, что и
|
|
14559
15901
|
// очистка перед сносом worktree, но БЕЗ сноса каталога — worktree остаётся под
|
|
14560
15902
|
// --continue. Гасит тяжёлую работу дрона, ушедшую из его process group (Bash-тул
|
|
@@ -14593,7 +15935,7 @@ async function runDaemon(opts) {
|
|
|
14593
15935
|
lastCommitMs: (p) => worktreeLastCommitMs(p),
|
|
14594
15936
|
// mtime транскрипта claude-сессии как liveness (TASWM-136): точнее коммитов — обновляется
|
|
14595
15937
|
// на каждое действие дрона (Edit/Read/tool-use), не только на коммит.
|
|
14596
|
-
transcriptMtimeMs: (p) =>
|
|
15938
|
+
transcriptMtimeMs: (p) => EXECUTOR.session.mtimeMs(p),
|
|
14597
15939
|
// Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
|
|
14598
15940
|
resourcesOk: hostResourcesOk,
|
|
14599
15941
|
// Ресурс-гейт v2 (TASWM-151): жёсткий порог активной разгрузки — те же сырые показания
|
|
@@ -14624,6 +15966,13 @@ async function runDaemon(opts) {
|
|
|
14624
15966
|
...exclusiveProject ? { exclusiveProject } : {},
|
|
14625
15967
|
repoFallback: opts.repo,
|
|
14626
15968
|
exclusiveLogged: /* @__PURE__ */ new Map(),
|
|
15969
|
+
// Зона задачи из истории репо (TASWM-348) — предлагаем сами, если постановщик
|
|
15970
|
+
// её не проставил. Дедуп событий отдельной мапой, как у гейтов выше.
|
|
15971
|
+
suggestArea: async (run) => {
|
|
15972
|
+
const repo = normalizeRepo(run.repo || opts.repo || "");
|
|
15973
|
+
return repo ? suggestAreaForRun(await zoneModels.forRepo(repo), run) : null;
|
|
15974
|
+
},
|
|
15975
|
+
overlapLogged: /* @__PURE__ */ new Map(),
|
|
14627
15976
|
// Ротация своего лога (TASWM-330): демон пишет в него неделями, а до этого ротация была
|
|
14628
15977
|
// только при старте. Итог ротации печатаем в сам лог — первой строкой нового поколения
|
|
14629
15978
|
// видно, что история не потерялась, а уехала в `.1`.
|
|
@@ -14648,7 +15997,9 @@ async function runDaemon(opts) {
|
|
|
14648
15997
|
repoFallback: opts.repo,
|
|
14649
15998
|
worktreesDir,
|
|
14650
15999
|
launch,
|
|
14651
|
-
git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree }
|
|
16000
|
+
git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree },
|
|
16001
|
+
prHeadBranch
|
|
16002
|
+
// TASWM-356: ветка PR для прогонов без записанного поля branch
|
|
14652
16003
|
},
|
|
14653
16004
|
reviewRun
|
|
14654
16005
|
).then(() => {
|
|
@@ -14679,8 +16030,8 @@ async function runDaemon(opts) {
|
|
|
14679
16030
|
mode,
|
|
14680
16031
|
nudge
|
|
14681
16032
|
),
|
|
14682
|
-
worktreeExists: (p) =>
|
|
14683
|
-
hasTranscript:
|
|
16033
|
+
worktreeExists: (p) => existsSync16(p),
|
|
16034
|
+
hasTranscript: executorHasSession,
|
|
14684
16035
|
// CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
|
|
14685
16036
|
// (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
|
|
14686
16037
|
// строится над failing/failingRunIds ИЗ checks — без повторного `gh pr checks`.
|
|
@@ -14691,7 +16042,7 @@ async function runDaemon(opts) {
|
|
|
14691
16042
|
// Штормовой режим (TASWM-23): health-probe на отбой (реальный claude) + внешний
|
|
14692
16043
|
// сигнал SWARM_STORM_URL (2-й слой, если задан). Логика/пороги — в manageStorm/storm.ts.
|
|
14693
16044
|
storm: {
|
|
14694
|
-
probe: () =>
|
|
16045
|
+
probe: () => executorHealthy({ executor: EXECUTOR, timeoutMs: resolveStormConfig().probeTimeoutMs }),
|
|
14695
16046
|
// externalDown опционален (exactOptionalPropertyTypes): ключ есть только при
|
|
14696
16047
|
// заданном SWARM_STORM_URL, иначе опускаем — не передаём undefined.
|
|
14697
16048
|
...SWARM_STORM_URL ? { externalDown: () => fetchExternalStorm(SWARM_STORM_URL) } : {}
|
|
@@ -14837,6 +16188,17 @@ async function runDaemon(opts) {
|
|
|
14837
16188
|
);
|
|
14838
16189
|
return run ? { id: run.id } : null;
|
|
14839
16190
|
},
|
|
16191
|
+
// TASWM-351: «перезапустить с того же места» из продукта. Тот же вход, что у
|
|
16192
|
+
// `swarm wake <id>` и у POST /wake локальной морды, — сознательно, чтобы у
|
|
16193
|
+
// побудки была ОДНА реализация: она умеет отличать «сессию можно продолжить»
|
|
16194
|
+
// от «продолжать нечем» и не будит служебные прогоны вслепую (см. wakeRun).
|
|
16195
|
+
// Слаг → прогон резолвим тем же activeRunForSlug, что и остановка: у команды
|
|
16196
|
+
// из веба нет и не будет локального id прогона (он живёт только в этом процессе).
|
|
16197
|
+
wakeRun: async (slug) => {
|
|
16198
|
+
const run = db.activeRunForSlug(slug);
|
|
16199
|
+
if (!run) return null;
|
|
16200
|
+
return wakeRun({ db, isAlive: pidAlive, respawn: deps.respawn, narrate: narrator.narrate }, run.id);
|
|
16201
|
+
},
|
|
14840
16202
|
update: async (onStep) => {
|
|
14841
16203
|
if (!daemonStart) {
|
|
14842
16204
|
return { ok: false, step: "fetch", message: "\u043A\u043E\u0434 \u0434\u0435\u043C\u043E\u043D\u0430 \u043B\u0435\u0436\u0438\u0442 \u043D\u0435 \u0432 git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E" };
|
|
@@ -15048,6 +16410,7 @@ export {
|
|
|
15048
16410
|
REVIEW_CYCLE_LIMIT,
|
|
15049
16411
|
REVIEW_ESCALATION_MARK,
|
|
15050
16412
|
REVIEW_SPAWN_LIMIT,
|
|
16413
|
+
REVIEW_UNRECOVERABLE_MARK,
|
|
15051
16414
|
SELF_REFERENCE_MARK,
|
|
15052
16415
|
STALL_FLOOR_MS,
|
|
15053
16416
|
STALL_MS,
|
|
@@ -15070,6 +16433,7 @@ export {
|
|
|
15070
16433
|
repoHasCiWorkflows,
|
|
15071
16434
|
resolveConflict,
|
|
15072
16435
|
respawnRun,
|
|
16436
|
+
reviewUnrecoverableMark,
|
|
15073
16437
|
reviveStuck,
|
|
15074
16438
|
runDaemon,
|
|
15075
16439
|
spawnReviewer,
|