@taskless-app/swarm 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/dist/cli.js +1551 -312
- package/dist/overmind.js +2249 -636
- package/package.json +2 -1
package/dist/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,22 @@ 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 SWARM_PACKAGE_NAME, MANUAL_UPDATE_COMMAND, HOST_COMMAND_KINDS, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, UPDATE_STEP_LABELS, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_ACK_GRACE_MS, HOST_COMMAND_MUTE_REASON, 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
|
-
|
|
753
|
+
SWARM_PACKAGE_NAME = "@taskless-app/swarm";
|
|
754
|
+
MANUAL_UPDATE_COMMAND = `npm i -g ${SWARM_PACKAGE_NAME}@latest`;
|
|
755
|
+
HOST_COMMAND_KINDS = ["pause", "resume", "restart", "update", "set_max_drones", "stop_run", "wake_run"];
|
|
743
756
|
HOST_COMMAND_STATUSES = ["pending", "delivered", "running", "done", "failed", "rejected", "expired"];
|
|
744
757
|
TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "rejected", "expired"]);
|
|
745
758
|
HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
|
|
@@ -752,46 +765,956 @@ var init_hostCommands = __esm({
|
|
|
752
765
|
};
|
|
753
766
|
HOST_COMMAND_TTL_MS = 30 * 6e4;
|
|
754
767
|
HOST_COMMAND_KEEPALIVE_MS = 10 * 6e4;
|
|
768
|
+
HOST_COMMAND_ACK_GRACE_MS = 5 * 6e4;
|
|
769
|
+
HOST_COMMAND_MUTE_REASON = `\u043C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u043B\u0430 \u0438\u0441\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u2014 \u0432\u0435\u0440\u043E\u044F\u0442\u043D\u043E, \u0434\u0435\u043C\u043E\u043D \u0441\u0442\u0430\u0440\u0435\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B. \u041E\u0431\u043D\u043E\u0432\u0438 \u0432\u0440\u0443\u0447\u043D\u0443\u044E: ${MANUAL_UPDATE_COMMAND}`;
|
|
755
770
|
HOST_COMMAND_LABELS = {
|
|
756
771
|
pause: "\u043F\u0430\u0443\u0437\u0430",
|
|
757
772
|
resume: "\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C",
|
|
758
773
|
restart: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A",
|
|
759
774
|
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
775
|
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"
|
|
776
|
+
stop_run: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
|
|
777
|
+
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
778
|
};
|
|
779
|
+
RUN_SCOPED_COMMAND_KINDS = ["stop_run", "wake_run"];
|
|
780
|
+
RUN_SCOPED = new Set(RUN_SCOPED_COMMAND_KINDS);
|
|
763
781
|
MAX_DRONES_LIMIT = 8;
|
|
764
782
|
}
|
|
765
783
|
});
|
|
766
784
|
|
|
767
|
-
// ../swarm-core/src/relief.ts
|
|
768
|
-
var RELIEF_PAUSE_MARK;
|
|
769
|
-
var init_relief = __esm({
|
|
770
|
-
"../swarm-core/src/relief.ts"() {
|
|
785
|
+
// ../swarm-core/src/relief.ts
|
|
786
|
+
var RELIEF_PAUSE_MARK;
|
|
787
|
+
var init_relief = __esm({
|
|
788
|
+
"../swarm-core/src/relief.ts"() {
|
|
789
|
+
"use strict";
|
|
790
|
+
init_time();
|
|
791
|
+
RELIEF_PAUSE_MARK = "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430";
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// ../swarm-core/src/cost.ts
|
|
796
|
+
var EMPTY_TASK_COST;
|
|
797
|
+
var init_cost = __esm({
|
|
798
|
+
"../swarm-core/src/cost.ts"() {
|
|
799
|
+
"use strict";
|
|
800
|
+
EMPTY_TASK_COST = Object.freeze({
|
|
801
|
+
costUsd: 0,
|
|
802
|
+
tokensIn: 0,
|
|
803
|
+
tokensOut: 0,
|
|
804
|
+
durationMs: 0,
|
|
805
|
+
runs: 0,
|
|
806
|
+
respawns: 0
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// ../swarm-core/src/runGuards.ts
|
|
812
|
+
var DEFAULT_RUN_LIMITS;
|
|
813
|
+
var init_runGuards = __esm({
|
|
814
|
+
"../swarm-core/src/runGuards.ts"() {
|
|
815
|
+
"use strict";
|
|
816
|
+
DEFAULT_RUN_LIMITS = {
|
|
817
|
+
maxCostUsd: 20,
|
|
818
|
+
runTimeoutMs: 8 * 60 * 6e4,
|
|
819
|
+
stepTimeoutMs: 90 * 6e4,
|
|
820
|
+
lostMs: 30 * 6e4
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// ../swarm-core/src/codeZones.ts
|
|
826
|
+
function hasZoneModel(m) {
|
|
827
|
+
return m.tasks > 0 && m.zones.length > 0;
|
|
828
|
+
}
|
|
829
|
+
function testFileFeature(basename5) {
|
|
830
|
+
const head = basename5.replace(/\.(test|spec)\.[a-z]+$/i, "").split(".")[0].toLowerCase();
|
|
831
|
+
return head && !GENERIC_DIRS.has(head) ? head : null;
|
|
832
|
+
}
|
|
833
|
+
function pathZoneCandidate(path) {
|
|
834
|
+
const parts = path.split("/").filter(Boolean);
|
|
835
|
+
if (parts.length <= 1) return "root";
|
|
836
|
+
if (WORKSPACE_DIRS.has(parts[0]) && parts.length >= 3) {
|
|
837
|
+
const pkg = parts[1];
|
|
838
|
+
if (parts[2] === "src" && parts.length >= 5) {
|
|
839
|
+
const feature = parts[3];
|
|
840
|
+
if (!GENERIC_DIRS.has(feature)) return feature;
|
|
841
|
+
}
|
|
842
|
+
if (GENERIC_TEST_DIRS.has(parts[2])) {
|
|
843
|
+
const feature = testFileFeature(parts[parts.length - 1]);
|
|
844
|
+
if (feature) return feature;
|
|
845
|
+
}
|
|
846
|
+
return pkg;
|
|
847
|
+
}
|
|
848
|
+
if (parts[0] === "src" && parts.length >= 3 && !GENERIC_DIRS.has(parts[1])) return parts[1];
|
|
849
|
+
return parts[0];
|
|
850
|
+
}
|
|
851
|
+
function dirOf(path) {
|
|
852
|
+
const i = path.lastIndexOf("/");
|
|
853
|
+
return i < 0 ? "" : path.slice(0, i);
|
|
854
|
+
}
|
|
855
|
+
function tokenize(text) {
|
|
856
|
+
const out = [];
|
|
857
|
+
for (const raw of text.split(/[^\p{L}\p{N}]+/u)) {
|
|
858
|
+
if (!raw) continue;
|
|
859
|
+
for (const piece of raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")) {
|
|
860
|
+
const t = piece.toLowerCase();
|
|
861
|
+
if (t.length < 3 || STOP_TERMS.has(t) || /^\d+$/.test(t)) continue;
|
|
862
|
+
out.push(t);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return out;
|
|
866
|
+
}
|
|
867
|
+
function normalizeZoneName(raw) {
|
|
868
|
+
if (!raw) return null;
|
|
869
|
+
const s = raw.trim().toLowerCase().replace(/^area:/, "");
|
|
870
|
+
return s || null;
|
|
871
|
+
}
|
|
872
|
+
function bump(m, k, by = 1) {
|
|
873
|
+
m.set(k, (m.get(k) ?? 0) + by);
|
|
874
|
+
}
|
|
875
|
+
function topKey(m) {
|
|
876
|
+
let best = null;
|
|
877
|
+
let bestN = 0;
|
|
878
|
+
let total = 0;
|
|
879
|
+
for (const [k, n] of m) {
|
|
880
|
+
total += n;
|
|
881
|
+
if (n > bestN) {
|
|
882
|
+
best = k;
|
|
883
|
+
bestN = n;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return best === null || total === 0 ? null : { key: best, count: bestN, share: bestN / total };
|
|
887
|
+
}
|
|
888
|
+
function buildZoneModel(entries, opts = {}) {
|
|
889
|
+
const o = { ...DEFAULTS, ...opts };
|
|
890
|
+
const tasks = entries.filter((e) => e.files.length > 0);
|
|
891
|
+
if (tasks.length < o.minTasks) return EMPTY_ZONE_MODEL;
|
|
892
|
+
const fileTasks = /* @__PURE__ */ new Map();
|
|
893
|
+
const candidate = /* @__PURE__ */ new Map();
|
|
894
|
+
for (const t of tasks) {
|
|
895
|
+
for (const f of new Set(t.files)) {
|
|
896
|
+
bump(fileTasks, f);
|
|
897
|
+
if (!candidate.has(f)) candidate.set(f, pathZoneCandidate(f));
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const zoneOf = new Map(candidate);
|
|
901
|
+
for (const [file, seen] of fileTasks) {
|
|
902
|
+
if (seen < 2) continue;
|
|
903
|
+
const votes = /* @__PURE__ */ new Map();
|
|
904
|
+
for (const t of tasks) {
|
|
905
|
+
const files = [...new Set(t.files)];
|
|
906
|
+
if (!files.includes(file)) continue;
|
|
907
|
+
const others = files.filter((f) => f !== file);
|
|
908
|
+
if (!others.length) continue;
|
|
909
|
+
for (const f of others) bump(votes, candidate.get(f), 1 / others.length);
|
|
910
|
+
}
|
|
911
|
+
const top = topKey(votes);
|
|
912
|
+
if (top && top.share >= 0.6 && top.key !== candidate.get(file)) zoneOf.set(file, top.key);
|
|
913
|
+
}
|
|
914
|
+
const voteWeight = (file) => 1 / Math.log2(2 + (fileTasks.get(file) ?? 1));
|
|
915
|
+
const taskZonePre = /* @__PURE__ */ new Map();
|
|
916
|
+
for (const t of tasks) {
|
|
917
|
+
const votes = /* @__PURE__ */ new Map();
|
|
918
|
+
for (const f of new Set(t.files)) bump(votes, zoneOf.get(f), voteWeight(f));
|
|
919
|
+
const top = topKey(votes);
|
|
920
|
+
if (top) taskZonePre.set(t.slug, top.key);
|
|
921
|
+
}
|
|
922
|
+
const fileZones = /* @__PURE__ */ new Map();
|
|
923
|
+
for (const t of tasks) {
|
|
924
|
+
const tz = taskZonePre.get(t.slug);
|
|
925
|
+
if (!tz) continue;
|
|
926
|
+
for (const f of new Set(t.files)) {
|
|
927
|
+
let m = fileZones.get(f);
|
|
928
|
+
if (!m) fileZones.set(f, m = /* @__PURE__ */ new Map());
|
|
929
|
+
bump(m, tz);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const spineSet = /* @__PURE__ */ new Set();
|
|
933
|
+
for (const [file, seen] of fileTasks) {
|
|
934
|
+
if (seen < o.spineMinTasks) continue;
|
|
935
|
+
const zonesOfFile = fileZones.get(file);
|
|
936
|
+
const dominant = zonesOfFile ? topKey(zonesOfFile) : null;
|
|
937
|
+
if ((zonesOfFile?.size ?? 0) < o.spineMinZones) continue;
|
|
938
|
+
if (dominant && dominant.share >= o.spineDominantShare) continue;
|
|
939
|
+
spineSet.add(file);
|
|
940
|
+
}
|
|
941
|
+
const fileZone = /* @__PURE__ */ new Map();
|
|
942
|
+
for (const [f, z2] of zoneOf) if (!spineSet.has(f)) fileZone.set(f, z2);
|
|
943
|
+
const taskZone = /* @__PURE__ */ new Map();
|
|
944
|
+
for (const t of tasks) {
|
|
945
|
+
const votes = /* @__PURE__ */ new Map();
|
|
946
|
+
for (const f of new Set(t.files)) {
|
|
947
|
+
const z2 = fileZone.get(f);
|
|
948
|
+
if (z2) bump(votes, z2, voteWeight(f));
|
|
949
|
+
}
|
|
950
|
+
const top = topKey(votes);
|
|
951
|
+
taskZone.set(t.slug, top ? top.key : SPINE_ZONE);
|
|
952
|
+
}
|
|
953
|
+
const labelVotes = /* @__PURE__ */ new Map();
|
|
954
|
+
for (const t of tasks) {
|
|
955
|
+
const manual = normalizeZoneName(t.area);
|
|
956
|
+
const z2 = taskZone.get(t.slug);
|
|
957
|
+
if (!manual || !z2 || z2 === SPINE_ZONE || manual === SPINE_ZONE) continue;
|
|
958
|
+
let m = labelVotes.get(z2);
|
|
959
|
+
if (!m) labelVotes.set(z2, m = /* @__PURE__ */ new Map());
|
|
960
|
+
bump(m, manual);
|
|
961
|
+
}
|
|
962
|
+
const rename = /* @__PURE__ */ new Map();
|
|
963
|
+
const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
|
|
964
|
+
const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
|
|
965
|
+
const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
|
|
966
|
+
return sum(b[1]) - sum(a[1]) || a[0].localeCompare(b[0]);
|
|
967
|
+
});
|
|
968
|
+
for (const [zone, votes] of clustersByEvidence) {
|
|
969
|
+
const top = topKey(votes);
|
|
970
|
+
if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
|
|
971
|
+
rename.set(zone, top.key);
|
|
972
|
+
takenNames.add(top.key);
|
|
973
|
+
}
|
|
974
|
+
const renamed = (z2) => rename.get(z2) ?? z2;
|
|
975
|
+
if (rename.size) {
|
|
976
|
+
for (const [f, z2] of fileZone) fileZone.set(f, renamed(z2));
|
|
977
|
+
for (const [s, z2] of taskZone) taskZone.set(s, renamed(z2));
|
|
978
|
+
}
|
|
979
|
+
const zoneTerms = /* @__PURE__ */ new Map();
|
|
980
|
+
const addTerm = (zone, term, w) => {
|
|
981
|
+
let m = zoneTerms.get(zone);
|
|
982
|
+
if (!m) zoneTerms.set(zone, m = /* @__PURE__ */ new Map());
|
|
983
|
+
bump(m, term, w);
|
|
984
|
+
};
|
|
985
|
+
for (const [f, z2] of fileZone) for (const term of tokenize(f)) addTerm(z2, term, 1);
|
|
986
|
+
for (const f of spineSet) for (const term of tokenize(f)) addTerm(SPINE_ZONE, term, 1);
|
|
987
|
+
for (const t of tasks) {
|
|
988
|
+
const z2 = taskZone.get(t.slug);
|
|
989
|
+
if (!z2 || !t.text) continue;
|
|
990
|
+
for (const term of tokenize(t.text)) addTerm(z2, term, 2);
|
|
991
|
+
}
|
|
992
|
+
const termZones = /* @__PURE__ */ new Map();
|
|
993
|
+
for (const terms of zoneTerms.values()) for (const term of terms.keys()) bump(termZones, term);
|
|
994
|
+
const zoneFiles = /* @__PURE__ */ new Map();
|
|
995
|
+
for (const [f, z2] of fileZone) {
|
|
996
|
+
const arr = zoneFiles.get(z2) ?? [];
|
|
997
|
+
arr.push(f);
|
|
998
|
+
zoneFiles.set(z2, arr);
|
|
999
|
+
}
|
|
1000
|
+
const zoneTaskCount = /* @__PURE__ */ new Map();
|
|
1001
|
+
for (const z2 of taskZone.values()) bump(zoneTaskCount, z2);
|
|
1002
|
+
const byFreq = (a, b) => (fileTasks.get(b) ?? 0) - (fileTasks.get(a) ?? 0) || a.localeCompare(b);
|
|
1003
|
+
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));
|
|
1004
|
+
const spine = [...spineSet].sort(byFreq);
|
|
1005
|
+
if (spineSet.size) zones.push({ zone: SPINE_ZONE, tasks: zoneTaskCount.get(SPINE_ZONE) ?? 0, files: spine });
|
|
1006
|
+
const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
|
|
1007
|
+
return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
|
|
1008
|
+
}
|
|
1009
|
+
function zoneForFiles(model, files) {
|
|
1010
|
+
if (!hasZoneModel(model) || !files.length) return null;
|
|
1011
|
+
const spineSet = new Set(model.spine);
|
|
1012
|
+
const touchedSpine = files.filter((f) => spineSet.has(f));
|
|
1013
|
+
const votes = /* @__PURE__ */ new Map();
|
|
1014
|
+
for (const f of new Set(files)) {
|
|
1015
|
+
const z2 = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
|
|
1016
|
+
if (z2) bump(votes, z2);
|
|
1017
|
+
}
|
|
1018
|
+
const top = topKey(votes);
|
|
1019
|
+
if (!top) {
|
|
1020
|
+
return {
|
|
1021
|
+
zone: SPINE_ZONE,
|
|
1022
|
+
confidence: 1,
|
|
1023
|
+
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(", ")})`,
|
|
1024
|
+
spine: true
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
return {
|
|
1028
|
+
zone: top.key,
|
|
1029
|
+
confidence: top.share,
|
|
1030
|
+
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}`,
|
|
1031
|
+
spine: touchedSpine.length > 0
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
function suggestZonesFromText(model, text, limit = 3) {
|
|
1035
|
+
if (!hasZoneModel(model)) return [];
|
|
1036
|
+
const terms = tokenize(text);
|
|
1037
|
+
if (!terms.length) return [];
|
|
1038
|
+
const zoneCount = model.zoneTerms.size || 1;
|
|
1039
|
+
const idf = (term) => Math.log(1 + zoneCount / (model.termZones.get(term) ?? 1));
|
|
1040
|
+
const scores = /* @__PURE__ */ new Map();
|
|
1041
|
+
const hits = /* @__PURE__ */ new Map();
|
|
1042
|
+
const tasksOfZone = new Map(model.zones.map((z2) => [z2.zone, z2.tasks]));
|
|
1043
|
+
for (const [zone, dict] of model.zoneTerms) {
|
|
1044
|
+
if ((tasksOfZone.get(zone) ?? 0) < 2) continue;
|
|
1045
|
+
let dot = 0;
|
|
1046
|
+
let norm = 0;
|
|
1047
|
+
const matched = [];
|
|
1048
|
+
for (const [term, w] of dict) {
|
|
1049
|
+
const v = Math.log(1 + w) * idf(term);
|
|
1050
|
+
norm += v * v;
|
|
1051
|
+
}
|
|
1052
|
+
if (!norm) continue;
|
|
1053
|
+
for (const term of new Set(terms)) {
|
|
1054
|
+
const w = dict.get(term);
|
|
1055
|
+
if (!w) continue;
|
|
1056
|
+
dot += Math.log(1 + w) * idf(term) * idf(term);
|
|
1057
|
+
matched.push(term);
|
|
1058
|
+
}
|
|
1059
|
+
const score = dot / Math.sqrt(norm) * Math.log(1 + (tasksOfZone.get(zone) ?? 1));
|
|
1060
|
+
if (score > 0) {
|
|
1061
|
+
scores.set(zone, score);
|
|
1062
|
+
hits.set(zone, matched);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
const total = [...scores.values()].reduce((a, b) => a + b, 0);
|
|
1066
|
+
if (!total) return [];
|
|
1067
|
+
return [...scores.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([zone, s]) => ({
|
|
1068
|
+
zone,
|
|
1069
|
+
confidence: s / total,
|
|
1070
|
+
why: `\u0441\u043E\u0432\u043F\u0430\u043B\u043E \u043F\u043E \u0441\u043B\u043E\u0432\u0430\u043C: ${(hits.get(zone) ?? []).slice(0, 5).join(", ")}`,
|
|
1071
|
+
spine: zone === SPINE_ZONE
|
|
1072
|
+
}));
|
|
1073
|
+
}
|
|
1074
|
+
function similarTasks(model, text, limit = 5) {
|
|
1075
|
+
const terms = new Set(tokenize(text));
|
|
1076
|
+
if (!terms.size) return [];
|
|
1077
|
+
const scored = [];
|
|
1078
|
+
for (const [i, e] of model.entries.entries()) {
|
|
1079
|
+
const other = model.entryTerms[i];
|
|
1080
|
+
if (!other?.size) continue;
|
|
1081
|
+
let inter = 0;
|
|
1082
|
+
for (const t of terms) if (other.has(t)) inter += Math.log(1 + (model.zoneTerms.size || 1) / (model.termZones.get(t) ?? 1));
|
|
1083
|
+
if (inter <= 0) continue;
|
|
1084
|
+
scored.push({ entry: e, score: inter / Math.sqrt(terms.size * other.size) });
|
|
1085
|
+
}
|
|
1086
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1087
|
+
}
|
|
1088
|
+
function predictFiles(model, opts) {
|
|
1089
|
+
if (!hasZoneModel(model)) return [];
|
|
1090
|
+
const limit = opts.limit ?? 40;
|
|
1091
|
+
const best = /* @__PURE__ */ new Map();
|
|
1092
|
+
const put = (path, weight, why) => {
|
|
1093
|
+
const cur = best.get(path);
|
|
1094
|
+
if (!cur || cur.weight < weight) best.set(path, { path, weight, why });
|
|
1095
|
+
};
|
|
1096
|
+
if (opts.text) {
|
|
1097
|
+
const sim = similarTasks(model, opts.text, opts.similar ?? 5);
|
|
1098
|
+
const topScore = sim[0]?.score ?? 0;
|
|
1099
|
+
for (const { entry, score } of sim) {
|
|
1100
|
+
const w = topScore > 0 ? Math.min(1, score / topScore) : 0;
|
|
1101
|
+
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}`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const zone = normalizeZoneName(opts.zone) ?? (opts.text ? suggestZonesFromText(model, opts.text)[0]?.zone ?? null : null);
|
|
1105
|
+
if (zone) {
|
|
1106
|
+
const stat = model.zones.find((z2) => z2.zone === zone);
|
|
1107
|
+
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}`));
|
|
1108
|
+
}
|
|
1109
|
+
return [...best.values()].sort((a, b) => b.weight - a.weight || a.path.localeCompare(b.path)).slice(0, limit);
|
|
1110
|
+
}
|
|
1111
|
+
function overlapRisk(predicted, actual, opts = {}) {
|
|
1112
|
+
const o = { ...OVERLAP_DEFAULTS, ...opts };
|
|
1113
|
+
const actualSet = new Set(actual);
|
|
1114
|
+
const actualDirs = new Set([...actualSet].map(dirOf).filter(Boolean));
|
|
1115
|
+
const totalWeight = predicted.reduce((a, p) => a + p.weight, 0);
|
|
1116
|
+
if (!totalWeight) return { overlap: false, score: 0, sharedFiles: [], sharedDirs: [] };
|
|
1117
|
+
let hit = 0;
|
|
1118
|
+
let strong = false;
|
|
1119
|
+
const sharedFiles = [];
|
|
1120
|
+
const sharedDirs = /* @__PURE__ */ new Set();
|
|
1121
|
+
for (const p of predicted) {
|
|
1122
|
+
if (actualSet.has(p.path)) {
|
|
1123
|
+
hit += p.weight;
|
|
1124
|
+
sharedFiles.push(p.path);
|
|
1125
|
+
if (p.weight >= o.strongFile) strong = true;
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const d = dirOf(p.path);
|
|
1129
|
+
if (d && actualDirs.has(d)) {
|
|
1130
|
+
hit += p.weight * 0.25;
|
|
1131
|
+
sharedDirs.add(d);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
const score = Math.min(1, hit / totalWeight);
|
|
1135
|
+
return {
|
|
1136
|
+
overlap: strong || score >= o.threshold,
|
|
1137
|
+
score,
|
|
1138
|
+
sharedFiles: sharedFiles.slice(0, 10),
|
|
1139
|
+
sharedDirs: [...sharedDirs].slice(0, 5)
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS, OVERLAP_DEFAULTS;
|
|
1143
|
+
var init_codeZones = __esm({
|
|
1144
|
+
"../swarm-core/src/codeZones.ts"() {
|
|
1145
|
+
"use strict";
|
|
1146
|
+
SPINE_ZONE = "core";
|
|
1147
|
+
DEFAULTS = {
|
|
1148
|
+
minTasks: 5,
|
|
1149
|
+
spineMinTasks: 3,
|
|
1150
|
+
spineMinZones: 3,
|
|
1151
|
+
spineDominantShare: 0.6
|
|
1152
|
+
};
|
|
1153
|
+
EMPTY_ZONE_MODEL = {
|
|
1154
|
+
tasks: 0,
|
|
1155
|
+
zones: [],
|
|
1156
|
+
fileZone: /* @__PURE__ */ new Map(),
|
|
1157
|
+
spine: [],
|
|
1158
|
+
taskZone: /* @__PURE__ */ new Map(),
|
|
1159
|
+
zoneTerms: /* @__PURE__ */ new Map(),
|
|
1160
|
+
termZones: /* @__PURE__ */ new Map(),
|
|
1161
|
+
entries: [],
|
|
1162
|
+
entryTerms: []
|
|
1163
|
+
};
|
|
1164
|
+
GENERIC_DIRS = /* @__PURE__ */ new Set([
|
|
1165
|
+
"src",
|
|
1166
|
+
"source",
|
|
1167
|
+
"lib",
|
|
1168
|
+
"libs",
|
|
1169
|
+
"util",
|
|
1170
|
+
"utils",
|
|
1171
|
+
"common",
|
|
1172
|
+
"internal",
|
|
1173
|
+
"app",
|
|
1174
|
+
"test",
|
|
1175
|
+
"tests",
|
|
1176
|
+
"__tests__",
|
|
1177
|
+
"spec",
|
|
1178
|
+
"specs",
|
|
1179
|
+
"index"
|
|
1180
|
+
]);
|
|
1181
|
+
WORKSPACE_DIRS = /* @__PURE__ */ new Set(["packages", "apps", "services", "libs", "modules"]);
|
|
1182
|
+
GENERIC_TEST_DIRS = /* @__PURE__ */ new Set(["tests", "test", "__tests__", "spec"]);
|
|
1183
|
+
STOP_TERMS = /* @__PURE__ */ new Set([
|
|
1184
|
+
"the",
|
|
1185
|
+
"and",
|
|
1186
|
+
"for",
|
|
1187
|
+
"with",
|
|
1188
|
+
"from",
|
|
1189
|
+
"into",
|
|
1190
|
+
"src",
|
|
1191
|
+
"test",
|
|
1192
|
+
"tests",
|
|
1193
|
+
"index",
|
|
1194
|
+
"packages",
|
|
1195
|
+
"package",
|
|
1196
|
+
"json",
|
|
1197
|
+
"yml",
|
|
1198
|
+
"yaml",
|
|
1199
|
+
"tsx",
|
|
1200
|
+
"mjs",
|
|
1201
|
+
"cjs",
|
|
1202
|
+
"\u0447\u0442\u043E",
|
|
1203
|
+
"\u043A\u0430\u043A",
|
|
1204
|
+
"\u0434\u043B\u044F",
|
|
1205
|
+
"\u043F\u0440\u0438",
|
|
1206
|
+
"\u044D\u0442\u043E",
|
|
1207
|
+
"\u0438\u043B\u0438",
|
|
1208
|
+
"\u0435\u0441\u043B\u0438",
|
|
1209
|
+
"\u0447\u0442\u043E\u0431\u044B",
|
|
1210
|
+
"\u0442\u043E\u0436\u0435",
|
|
1211
|
+
"\u0435\u0433\u043E",
|
|
1212
|
+
"\u0432\u0441\u0435",
|
|
1213
|
+
"feat",
|
|
1214
|
+
"fix",
|
|
1215
|
+
"chore",
|
|
1216
|
+
"docs",
|
|
1217
|
+
"refactor",
|
|
1218
|
+
"\u043F\u0435\u0440\u0444",
|
|
1219
|
+
"wip"
|
|
1220
|
+
]);
|
|
1221
|
+
OVERLAP_DEFAULTS = { threshold: 0.2, strongFile: 0.35 };
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// ../swarm-core/src/index.ts
|
|
1226
|
+
var init_src = __esm({
|
|
1227
|
+
"../swarm-core/src/index.ts"() {
|
|
1228
|
+
"use strict";
|
|
1229
|
+
init_types();
|
|
1230
|
+
init_timeline();
|
|
1231
|
+
init_reviewPhase();
|
|
1232
|
+
init_status();
|
|
1233
|
+
init_time();
|
|
1234
|
+
init_windows();
|
|
1235
|
+
init_slug();
|
|
1236
|
+
init_text();
|
|
1237
|
+
init_comment();
|
|
1238
|
+
init_names();
|
|
1239
|
+
init_baseBranch();
|
|
1240
|
+
init_hosts();
|
|
1241
|
+
init_hostPool();
|
|
1242
|
+
init_hostCommands();
|
|
1243
|
+
init_relief();
|
|
1244
|
+
init_cost();
|
|
1245
|
+
init_runGuards();
|
|
1246
|
+
init_codeZones();
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
// src/claudeBin.ts
|
|
1251
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
1252
|
+
import { win32 as winPath } from "path";
|
|
1253
|
+
function compareVersionDirs(a, b) {
|
|
1254
|
+
const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
|
|
1255
|
+
const pa = parts(a);
|
|
1256
|
+
const pb = parts(b);
|
|
1257
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1258
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1259
|
+
if (d !== 0) return d;
|
|
1260
|
+
}
|
|
1261
|
+
return 0;
|
|
1262
|
+
}
|
|
1263
|
+
function realListDir(dir) {
|
|
1264
|
+
try {
|
|
1265
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1266
|
+
} catch {
|
|
1267
|
+
return [];
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
function winVersionRoots(env, listDir) {
|
|
1271
|
+
const roots = [];
|
|
1272
|
+
const appData = env.APPDATA;
|
|
1273
|
+
if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
|
|
1274
|
+
const localAppData = env.LOCALAPPDATA;
|
|
1275
|
+
if (localAppData) {
|
|
1276
|
+
const packages = winPath.join(localAppData, "Packages");
|
|
1277
|
+
for (const name of listDir(packages)) {
|
|
1278
|
+
if (!name.startsWith("Claude")) continue;
|
|
1279
|
+
roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return roots;
|
|
1283
|
+
}
|
|
1284
|
+
function findWindowsClaude(opts = {}) {
|
|
1285
|
+
const env = opts.env ?? process.env;
|
|
1286
|
+
const listDir = opts.listDir ?? realListDir;
|
|
1287
|
+
const fileExists = opts.fileExists ?? existsSync2;
|
|
1288
|
+
let best = null;
|
|
1289
|
+
for (const root of winVersionRoots(env, listDir)) {
|
|
1290
|
+
for (const version of listDir(root)) {
|
|
1291
|
+
const candidate = winPath.join(root, version, "claude.exe");
|
|
1292
|
+
if (!fileExists(candidate)) continue;
|
|
1293
|
+
if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
return best?.path ?? null;
|
|
1297
|
+
}
|
|
1298
|
+
function resolveClaudeBin(opts = {}) {
|
|
1299
|
+
const env = opts.env ?? process.env;
|
|
1300
|
+
const platform = opts.platform ?? process.platform;
|
|
1301
|
+
const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
|
|
1302
|
+
if (explicit) return explicit;
|
|
1303
|
+
if (platform !== "win32") return CLAUDE_FALLBACK;
|
|
1304
|
+
if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
|
|
1305
|
+
windowsScanDone = true;
|
|
1306
|
+
const found = findWindowsClaude({ ...opts, env });
|
|
1307
|
+
cachedWindowsBin = found;
|
|
1308
|
+
if (found) return found;
|
|
1309
|
+
if (!warnedNoWindowsClaude) {
|
|
1310
|
+
warnedNoWindowsClaude = true;
|
|
1311
|
+
console.warn(
|
|
1312
|
+
"[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."
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
return CLAUDE_FALLBACK;
|
|
1316
|
+
}
|
|
1317
|
+
var CLAUDE_FALLBACK, warnedNoWindowsClaude, cachedWindowsBin, windowsScanDone;
|
|
1318
|
+
var init_claudeBin = __esm({
|
|
1319
|
+
"src/claudeBin.ts"() {
|
|
1320
|
+
"use strict";
|
|
1321
|
+
CLAUDE_FALLBACK = "claude";
|
|
1322
|
+
warnedNoWindowsClaude = false;
|
|
1323
|
+
cachedWindowsBin = null;
|
|
1324
|
+
windowsScanDone = false;
|
|
1325
|
+
}
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
// src/pricing.ts
|
|
1329
|
+
function costFromUsage(usage, model, table = MODEL_PRICING) {
|
|
1330
|
+
const pricing = model ? table[model] : void 0;
|
|
1331
|
+
if (!pricing) {
|
|
1332
|
+
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` };
|
|
1333
|
+
}
|
|
1334
|
+
const input = usage.input_tokens ?? 0;
|
|
1335
|
+
const output = usage.output_tokens ?? 0;
|
|
1336
|
+
const cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
1337
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
1338
|
+
const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
|
|
1339
|
+
return { costUsd };
|
|
1340
|
+
}
|
|
1341
|
+
var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
|
|
1342
|
+
var init_pricing = __esm({
|
|
1343
|
+
"src/pricing.ts"() {
|
|
1344
|
+
"use strict";
|
|
1345
|
+
MODEL_PRICING = {
|
|
1346
|
+
// Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
|
|
1347
|
+
// Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
|
|
1348
|
+
// ветвление по времени внутри расчёта стоимости.
|
|
1349
|
+
"claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
|
|
1350
|
+
"claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1351
|
+
// Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
|
|
1352
|
+
"claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1353
|
+
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
|
|
1354
|
+
};
|
|
1355
|
+
CACHE_READ_FACTOR = 0.1;
|
|
1356
|
+
CACHE_WRITE_5M_FACTOR = 1.25;
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
// src/claudeResult.ts
|
|
1361
|
+
function parseClaudeResultJson(raw) {
|
|
1362
|
+
const trimmed = raw.trim();
|
|
1363
|
+
if (!trimmed) return null;
|
|
1364
|
+
try {
|
|
1365
|
+
const parsed = JSON.parse(trimmed);
|
|
1366
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
1367
|
+
} catch {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
function launchCostUsd(result, model) {
|
|
1372
|
+
if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
|
|
1373
|
+
return { costUsd: result.total_cost_usd };
|
|
1374
|
+
}
|
|
1375
|
+
if (!result.usage) return { costUsd: 0 };
|
|
1376
|
+
return costFromUsage(result.usage, model);
|
|
1377
|
+
}
|
|
1378
|
+
function launchSpend(result) {
|
|
1379
|
+
const u = result.usage ?? {};
|
|
1380
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
1381
|
+
return {
|
|
1382
|
+
tokensIn: num(u.input_tokens) + num(u.cache_creation_input_tokens) + num(u.cache_read_input_tokens),
|
|
1383
|
+
tokensOut: num(u.output_tokens),
|
|
1384
|
+
durationMs: num(result.duration_ms)
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
var init_claudeResult = __esm({
|
|
1388
|
+
"src/claudeResult.ts"() {
|
|
1389
|
+
"use strict";
|
|
1390
|
+
init_pricing();
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
// src/executors/claudeCode.ts
|
|
1395
|
+
import { readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1396
|
+
import { homedir } from "os";
|
|
1397
|
+
import { join as join2 } from "path";
|
|
1398
|
+
function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
|
|
1399
|
+
const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
|
|
1400
|
+
const base3 = [
|
|
1401
|
+
"--dangerously-skip-permissions",
|
|
1402
|
+
...ISOLATION_FLAGS,
|
|
1403
|
+
"--mcp-config",
|
|
1404
|
+
cfgPath,
|
|
1405
|
+
...settings ? ["--settings", settings] : [],
|
|
1406
|
+
...pluginFlags,
|
|
1407
|
+
"--output-format",
|
|
1408
|
+
"json",
|
|
1409
|
+
"-p",
|
|
1410
|
+
prompt
|
|
1411
|
+
];
|
|
1412
|
+
const withModel = model ? ["--model", model, ...base3] : base3;
|
|
1413
|
+
return continueSession ? ["--continue", ...withModel] : withModel;
|
|
1414
|
+
}
|
|
1415
|
+
function droneDenyRules(paths) {
|
|
1416
|
+
const abs = (p) => `//${p.replace(/^\/+/, "")}`;
|
|
1417
|
+
return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
|
|
1418
|
+
}
|
|
1419
|
+
function droneSettingsJson(paths) {
|
|
1420
|
+
return JSON.stringify({ permissions: { deny: droneDenyRules(paths) } });
|
|
1421
|
+
}
|
|
1422
|
+
function transcriptDir(worktree, projectsDir) {
|
|
1423
|
+
return join2(projectsDir, worktree.replace(/[/.]/g, "-"));
|
|
1424
|
+
}
|
|
1425
|
+
function sessionTranscriptExists(worktree, projectsDir = join2(homedir(), ".claude", "projects")) {
|
|
1426
|
+
try {
|
|
1427
|
+
return readdirSync2(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
|
|
1428
|
+
} catch {
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
function transcriptMtimeMs(worktree, projectsDir = join2(homedir(), ".claude", "projects")) {
|
|
1433
|
+
const dir = transcriptDir(worktree, projectsDir);
|
|
1434
|
+
let files;
|
|
1435
|
+
try {
|
|
1436
|
+
files = readdirSync2(dir);
|
|
1437
|
+
} catch {
|
|
1438
|
+
return null;
|
|
1439
|
+
}
|
|
1440
|
+
let latest = null;
|
|
1441
|
+
for (const f of files) {
|
|
1442
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
1443
|
+
try {
|
|
1444
|
+
const mtime2 = statSync(join2(dir, f)).mtimeMs;
|
|
1445
|
+
if (latest === null || mtime2 > latest) latest = mtime2;
|
|
1446
|
+
} catch {
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return latest;
|
|
1450
|
+
}
|
|
1451
|
+
function writeMcpConfig(spec) {
|
|
1452
|
+
const cfg = {
|
|
1453
|
+
mcpServers: {
|
|
1454
|
+
[spec.mcp.name]: { command: spec.mcp.command, args: spec.mcp.args, env: spec.mcp.env }
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
const path = join2(spec.worktree, MCP_CONFIG_FILE);
|
|
1458
|
+
writeFileSync2(path, JSON.stringify(cfg, null, 2));
|
|
1459
|
+
return path;
|
|
1460
|
+
}
|
|
1461
|
+
var ISOLATION_FLAGS, MCP_CONFIG_FILE, claudeCodeExecutor;
|
|
1462
|
+
var init_claudeCode = __esm({
|
|
1463
|
+
"src/executors/claudeCode.ts"() {
|
|
1464
|
+
"use strict";
|
|
1465
|
+
init_claudeBin();
|
|
1466
|
+
init_claudeResult();
|
|
1467
|
+
ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
|
|
1468
|
+
MCP_CONFIG_FILE = ".swarm-mcp.json";
|
|
1469
|
+
claudeCodeExecutor = {
|
|
1470
|
+
id: "claude-code",
|
|
1471
|
+
title: "Claude Code (claude)",
|
|
1472
|
+
binEnv: "SWARM_CLAUDE_BIN",
|
|
1473
|
+
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" },
|
|
1474
|
+
resolveBin: () => resolveClaudeBin(),
|
|
1475
|
+
// Стрелка, а не метод: `plan` могут вызвать в отрыве от объекта (деструктуризация в тестах,
|
|
1476
|
+
// проброс колбэком), и `this` тогда уедет в undefined.
|
|
1477
|
+
plan: (spec) => {
|
|
1478
|
+
const cfgPath = writeMcpConfig(spec);
|
|
1479
|
+
const settings = spec.denyPaths.length > 0 ? droneSettingsJson(spec.denyPaths) : void 0;
|
|
1480
|
+
return {
|
|
1481
|
+
bin: resolveClaudeBin(),
|
|
1482
|
+
args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings)
|
|
1483
|
+
};
|
|
1484
|
+
},
|
|
1485
|
+
parseResult(raw, model) {
|
|
1486
|
+
const result = parseClaudeResultJson(raw);
|
|
1487
|
+
if (!result) return null;
|
|
1488
|
+
const est = launchCostUsd(result, model);
|
|
1489
|
+
const spend = launchSpend(result);
|
|
1490
|
+
return {
|
|
1491
|
+
costUsd: est.costUsd,
|
|
1492
|
+
...est.warning ? { warning: est.warning } : {},
|
|
1493
|
+
tokensIn: spend.tokensIn,
|
|
1494
|
+
tokensOut: spend.tokensOut,
|
|
1495
|
+
durationMs: spend.durationMs
|
|
1496
|
+
};
|
|
1497
|
+
},
|
|
1498
|
+
session: {
|
|
1499
|
+
exists: (worktree) => sessionTranscriptExists(worktree),
|
|
1500
|
+
mtimeMs: (worktree) => transcriptMtimeMs(worktree)
|
|
1501
|
+
},
|
|
1502
|
+
// `claude -p "ok" --max-turns 1` (TASWM-23) — прямее, чем HEAD на статус-хост: дёргает ТОТ
|
|
1503
|
+
// ЖЕ путь (auth + реальный ответ модели), что и дрон. ISOLATION_FLAGS те же, что у дрона
|
|
1504
|
+
// (TASWM-90): SessionStart-хуки юзера могли бы подвесить пробу и дать ложный шторм.
|
|
1505
|
+
probe: {
|
|
1506
|
+
args: [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"],
|
|
1507
|
+
// TASWM-214: исчерпанный лимит подписки — НЕ шторм провайдера, и путать их нельзя.
|
|
1508
|
+
limitPattern: /(weekly limit|usage limit|hit your .*limit)/i
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
// src/swarmHome.ts
|
|
1515
|
+
import { homedir as homedir2 } from "os";
|
|
1516
|
+
import { join as join3 } from "path";
|
|
1517
|
+
import { mkdirSync } from "fs";
|
|
1518
|
+
var SWARM_HOME;
|
|
1519
|
+
var init_swarmHome = __esm({
|
|
1520
|
+
"src/swarmHome.ts"() {
|
|
1521
|
+
"use strict";
|
|
1522
|
+
SWARM_HOME = process.env.SWARM_HOME || join3(homedir2(), ".taskless-swarm");
|
|
1523
|
+
mkdirSync(SWARM_HOME, { recursive: true });
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
// src/executors/codex.ts
|
|
1528
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync3, statSync as statSync2, symlinkSync } from "fs";
|
|
1529
|
+
import { join as join4 } from "path";
|
|
1530
|
+
function resolveCodexBin(env = process.env) {
|
|
1531
|
+
return (env.SWARM_CODEX_BIN ?? "").trim() || "codex";
|
|
1532
|
+
}
|
|
1533
|
+
function codexHomePath(worktree, swarmHome = SWARM_HOME) {
|
|
1534
|
+
return join4(swarmHome, "executor-state", "codex", worktree.replace(/[/.]/g, "-"));
|
|
1535
|
+
}
|
|
1536
|
+
function ensureCodexHome(worktree, realHome, env = process.env, swarmHome = SWARM_HOME) {
|
|
1537
|
+
const home = codexHomePath(worktree, swarmHome);
|
|
1538
|
+
mkdirSync2(home, { recursive: true });
|
|
1539
|
+
const real = (env.CODEX_HOME ?? "").trim() || join4(realHome, ".codex");
|
|
1540
|
+
for (const file of ["auth.json", "config.toml"]) {
|
|
1541
|
+
const target = join4(real, file);
|
|
1542
|
+
const link = join4(home, file);
|
|
1543
|
+
if (!existsSync3(target) || existsSync3(link)) continue;
|
|
1544
|
+
try {
|
|
1545
|
+
symlinkSync(target, link);
|
|
1546
|
+
} catch {
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return home;
|
|
1550
|
+
}
|
|
1551
|
+
function tomlValue(v) {
|
|
1552
|
+
if (typeof v === "string") {
|
|
1553
|
+
const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
1554
|
+
return `"${escaped}"`;
|
|
1555
|
+
}
|
|
1556
|
+
if (Array.isArray(v)) return `[${v.map((x) => tomlValue(x)).join(", ")}]`;
|
|
1557
|
+
return `{${Object.entries(v).map(([k, val]) => `${k} = ${tomlValue(val)}`).join(", ")}}`;
|
|
1558
|
+
}
|
|
1559
|
+
function mcpConfigArgs(spec) {
|
|
1560
|
+
const inline = `{command = ${tomlValue(spec.mcp.command)}, args = ${tomlValue(spec.mcp.args)}, env = ${tomlValue(spec.mcp.env)}}`;
|
|
1561
|
+
return ["-c", `mcp_servers.${spec.mcp.name}=${inline}`];
|
|
1562
|
+
}
|
|
1563
|
+
function skillsNotice(skillDirs) {
|
|
1564
|
+
if (skillDirs.length === 0) return "";
|
|
1565
|
+
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.";
|
|
1566
|
+
}
|
|
1567
|
+
function readUsage(node) {
|
|
1568
|
+
if (typeof node !== "object" || node === null) return null;
|
|
1569
|
+
const rec = node;
|
|
1570
|
+
const direct = rec.total_token_usage ?? rec.usage ?? rec.token_usage;
|
|
1571
|
+
if (direct && typeof direct === "object") return direct;
|
|
1572
|
+
const info = rec.info;
|
|
1573
|
+
if (info && typeof info === "object") return readUsage(info);
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
function parseCodexStream(raw) {
|
|
1577
|
+
let usage = null;
|
|
1578
|
+
let completed = false;
|
|
1579
|
+
for (const line of raw.split("\n")) {
|
|
1580
|
+
const trimmed = line.trim();
|
|
1581
|
+
if (!trimmed) continue;
|
|
1582
|
+
let parsed;
|
|
1583
|
+
try {
|
|
1584
|
+
parsed = JSON.parse(trimmed);
|
|
1585
|
+
} catch {
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
if (typeof parsed !== "object" || parsed === null) continue;
|
|
1589
|
+
const rec = parsed;
|
|
1590
|
+
const msg = typeof rec.msg === "object" && rec.msg !== null ? rec.msg : rec;
|
|
1591
|
+
const type = typeof msg.type === "string" ? msg.type : null;
|
|
1592
|
+
const found = readUsage(msg);
|
|
1593
|
+
if (found) usage = found;
|
|
1594
|
+
if (type && TERMINAL_EVENTS.has(type)) completed = true;
|
|
1595
|
+
}
|
|
1596
|
+
return { usage, completed };
|
|
1597
|
+
}
|
|
1598
|
+
function newestRolloutMtime(dir, depth = 4) {
|
|
1599
|
+
let entries;
|
|
1600
|
+
try {
|
|
1601
|
+
entries = readdirSync3(dir);
|
|
1602
|
+
} catch {
|
|
1603
|
+
return null;
|
|
1604
|
+
}
|
|
1605
|
+
let latest = null;
|
|
1606
|
+
for (const name of entries) {
|
|
1607
|
+
const full = join4(dir, name);
|
|
1608
|
+
let st;
|
|
1609
|
+
try {
|
|
1610
|
+
st = statSync2(full);
|
|
1611
|
+
} catch {
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
if (st.isDirectory()) {
|
|
1615
|
+
if (depth <= 0) continue;
|
|
1616
|
+
const nested = newestRolloutMtime(full, depth - 1);
|
|
1617
|
+
if (nested !== null && (latest === null || nested > latest)) latest = nested;
|
|
1618
|
+
} else if (name.endsWith(".jsonl")) {
|
|
1619
|
+
if (latest === null || st.mtimeMs > latest) latest = st.mtimeMs;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return latest;
|
|
1623
|
+
}
|
|
1624
|
+
var BYPASS_FLAG, SKIP_GIT_CHECK_FLAG, CODEX_MODEL_PRICING, TERMINAL_EVENTS, codexExecutor;
|
|
1625
|
+
var init_codex = __esm({
|
|
1626
|
+
"src/executors/codex.ts"() {
|
|
1627
|
+
"use strict";
|
|
1628
|
+
init_swarmHome();
|
|
1629
|
+
init_pricing();
|
|
1630
|
+
BYPASS_FLAG = "--dangerously-bypass-approvals-and-sandbox";
|
|
1631
|
+
SKIP_GIT_CHECK_FLAG = "--skip-git-repo-check";
|
|
1632
|
+
CODEX_MODEL_PRICING = {};
|
|
1633
|
+
TERMINAL_EVENTS = /* @__PURE__ */ new Set(["task_complete", "turn.completed", "thread.completed", "session.completed"]);
|
|
1634
|
+
codexExecutor = {
|
|
1635
|
+
id: "codex",
|
|
1636
|
+
title: "Codex CLI (codex)",
|
|
1637
|
+
binEnv: "SWARM_CODEX_BIN",
|
|
1638
|
+
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" },
|
|
1639
|
+
resolveBin: () => resolveCodexBin(),
|
|
1640
|
+
// Стрелка, а не метод — см. тот же комментарий в claudeCode.ts.
|
|
1641
|
+
plan: (spec) => {
|
|
1642
|
+
const notice = skillsNotice(spec.skillDirs);
|
|
1643
|
+
const prompt = notice ? `${spec.prompt}
|
|
1644
|
+
|
|
1645
|
+
${notice}` : spec.prompt;
|
|
1646
|
+
const args = [
|
|
1647
|
+
"exec",
|
|
1648
|
+
// Продолжение оборванной сессии — ПОДКОМАНДА, а не флаг: `exec resume --last` берёт
|
|
1649
|
+
// последнюю сессию этого CODEX_HOME (то есть этого worktree, см. codexHome).
|
|
1650
|
+
...spec.continueSession ? ["resume", "--last"] : [],
|
|
1651
|
+
"--json",
|
|
1652
|
+
SKIP_GIT_CHECK_FLAG,
|
|
1653
|
+
BYPASS_FLAG,
|
|
1654
|
+
...spec.model ? ["-m", spec.model] : [],
|
|
1655
|
+
...mcpConfigArgs(spec),
|
|
1656
|
+
prompt
|
|
1657
|
+
];
|
|
1658
|
+
return { bin: resolveCodexBin(), args, env: { CODEX_HOME: ensureCodexHome(spec.worktree, spec.realHome) } };
|
|
1659
|
+
},
|
|
1660
|
+
parseResult(raw, model) {
|
|
1661
|
+
const { usage, completed } = parseCodexStream(raw);
|
|
1662
|
+
if (!completed) return null;
|
|
1663
|
+
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" };
|
|
1664
|
+
const cached = usage.cached_input_tokens ?? 0;
|
|
1665
|
+
const common = {
|
|
1666
|
+
input_tokens: Math.max(0, (usage.input_tokens ?? 0) - cached),
|
|
1667
|
+
output_tokens: usage.output_tokens ?? 0,
|
|
1668
|
+
cache_read_input_tokens: cached
|
|
1669
|
+
};
|
|
1670
|
+
const est = costFromUsage(common, model, CODEX_MODEL_PRICING);
|
|
1671
|
+
return est.warning ? { costUsd: est.costUsd, warning: est.warning } : { costUsd: est.costUsd };
|
|
1672
|
+
},
|
|
1673
|
+
session: {
|
|
1674
|
+
exists: (worktree) => newestRolloutMtime(join4(codexHomePath(worktree), "sessions")) !== null,
|
|
1675
|
+
mtimeMs: (worktree) => newestRolloutMtime(join4(codexHomePath(worktree), "sessions"))
|
|
1676
|
+
},
|
|
1677
|
+
probe: {
|
|
1678
|
+
// Тот же принцип, что у claude: дешёвый реальный прогон — он проверяет и auth, и живость
|
|
1679
|
+
// провайдера разом. Один короткий ответ, без тулов и без сети сверх модели.
|
|
1680
|
+
args: ["exec", "--json", SKIP_GIT_CHECK_FLAG, BYPASS_FLAG, "ok"],
|
|
1681
|
+
limitPattern: /(usage limit|rate limit|quota exceeded|insufficient_quota)/i
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
// src/executors/types.ts
|
|
1688
|
+
var init_types2 = __esm({
|
|
1689
|
+
"src/executors/types.ts"() {
|
|
771
1690
|
"use strict";
|
|
772
|
-
init_time();
|
|
773
|
-
RELIEF_PAUSE_MARK = "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430";
|
|
774
1691
|
}
|
|
775
1692
|
});
|
|
776
1693
|
|
|
777
|
-
//
|
|
778
|
-
|
|
779
|
-
"
|
|
1694
|
+
// src/executors/index.ts
|
|
1695
|
+
function resolveExecutor(id) {
|
|
1696
|
+
const key = (id ?? "").trim();
|
|
1697
|
+
if (!key) return EXECUTORS[DEFAULT_EXECUTOR_ID];
|
|
1698
|
+
const found = EXECUTORS[key];
|
|
1699
|
+
if (!found) {
|
|
1700
|
+
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(", ")}`);
|
|
1701
|
+
}
|
|
1702
|
+
return found;
|
|
1703
|
+
}
|
|
1704
|
+
var DEFAULT_EXECUTOR_ID, EXECUTORS;
|
|
1705
|
+
var init_executors = __esm({
|
|
1706
|
+
"src/executors/index.ts"() {
|
|
780
1707
|
"use strict";
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
init_baseBranch();
|
|
792
|
-
init_hosts();
|
|
793
|
-
init_hostCommands();
|
|
794
|
-
init_relief();
|
|
1708
|
+
init_claudeCode();
|
|
1709
|
+
init_codex();
|
|
1710
|
+
init_types2();
|
|
1711
|
+
init_claudeCode();
|
|
1712
|
+
init_codex();
|
|
1713
|
+
DEFAULT_EXECUTOR_ID = "claude-code";
|
|
1714
|
+
EXECUTORS = {
|
|
1715
|
+
[claudeCodeExecutor.id]: claudeCodeExecutor,
|
|
1716
|
+
[codexExecutor.id]: codexExecutor
|
|
1717
|
+
};
|
|
795
1718
|
}
|
|
796
1719
|
});
|
|
797
1720
|
|
|
@@ -811,6 +1734,7 @@ __export(config_exports, {
|
|
|
811
1734
|
DRONE_HOME: () => DRONE_HOME,
|
|
812
1735
|
DRONE_HOME_ISOLATION: () => DRONE_HOME_ISOLATION,
|
|
813
1736
|
DRONE_ISOLATION: () => DRONE_ISOLATION,
|
|
1737
|
+
EXECUTOR: () => EXECUTOR,
|
|
814
1738
|
HEARTBEAT_PATH: () => HEARTBEAT_PATH,
|
|
815
1739
|
HEARTBEAT_STALE_MS: () => HEARTBEAT_STALE_MS,
|
|
816
1740
|
REPO_MAP_PATH: () => REPO_MAP_PATH,
|
|
@@ -880,9 +1804,8 @@ __export(config_exports, {
|
|
|
880
1804
|
resolveEffectiveWorktreesDir: () => resolveEffectiveWorktreesDir,
|
|
881
1805
|
reviewCutoffAt: () => reviewCutoffAt
|
|
882
1806
|
});
|
|
883
|
-
import {
|
|
884
|
-
import {
|
|
885
|
-
import { mkdirSync, readFileSync as readFileSync2, realpathSync, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1807
|
+
import { basename, join as join5, resolve } from "path";
|
|
1808
|
+
import { readFileSync as readFileSync2, realpathSync, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
886
1809
|
function normalizeRepo(repo) {
|
|
887
1810
|
const r = (repo ?? "").trim();
|
|
888
1811
|
if (!r) return "";
|
|
@@ -893,7 +1816,7 @@ function normalizeRepo(repo) {
|
|
|
893
1816
|
}
|
|
894
1817
|
}
|
|
895
1818
|
function droneWorktreePath(worktreesDir, repo, slug) {
|
|
896
|
-
return
|
|
1819
|
+
return join5(worktreesDir, basename(normalizeRepo(repo)), slug);
|
|
897
1820
|
}
|
|
898
1821
|
function resolveEffectiveWorktreesDir(deps) {
|
|
899
1822
|
if (deps.envOverride) return deps.envOverride;
|
|
@@ -935,7 +1858,7 @@ function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
|
|
|
935
1858
|
}
|
|
936
1859
|
function tokenFromFile() {
|
|
937
1860
|
try {
|
|
938
|
-
return readFileSync2(
|
|
1861
|
+
return readFileSync2(join5(SWARM_HOME, "token"), "utf8").trim();
|
|
939
1862
|
} catch {
|
|
940
1863
|
return "";
|
|
941
1864
|
}
|
|
@@ -949,34 +1872,36 @@ function reviewCutoffAt() {
|
|
|
949
1872
|
const at = stamp();
|
|
950
1873
|
try {
|
|
951
1874
|
const tmp = `${REVIEW_CUTOFF_PATH}.tmp`;
|
|
952
|
-
|
|
1875
|
+
writeFileSync3(tmp, JSON.stringify({ at }));
|
|
953
1876
|
renameSync2(tmp, REVIEW_CUTOFF_PATH);
|
|
954
1877
|
} catch {
|
|
955
1878
|
}
|
|
956
1879
|
return at;
|
|
957
1880
|
}
|
|
958
|
-
var
|
|
1881
|
+
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
1882
|
var init_config = __esm({
|
|
960
1883
|
"src/config.ts"() {
|
|
961
1884
|
"use strict";
|
|
962
1885
|
init_src();
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1886
|
+
init_executors();
|
|
1887
|
+
init_swarmHome();
|
|
1888
|
+
init_swarmHome();
|
|
1889
|
+
DB_PATH = process.env.SWARM_DB || join5(SWARM_HOME, "swarm.db");
|
|
1890
|
+
EXECUTOR = resolveExecutor(process.env.SWARM_EXECUTOR);
|
|
1891
|
+
DRONE_HOME = process.env.SWARM_DRONE_HOME || join5(SWARM_HOME, "drone-home");
|
|
967
1892
|
DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
|
|
968
1893
|
DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
|
|
969
|
-
WORKTREES_NOINDEX_DIR =
|
|
970
|
-
WORKTREES_LEGACY_DIR =
|
|
1894
|
+
WORKTREES_NOINDEX_DIR = join5(SWARM_HOME, "worktrees.noindex");
|
|
1895
|
+
WORKTREES_LEGACY_DIR = join5(SWARM_HOME, "worktrees");
|
|
971
1896
|
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 ||
|
|
1897
|
+
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join5(SWARM_HOME, "run-logs");
|
|
1898
|
+
DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join5(SWARM_HOME, "overmind.log");
|
|
974
1899
|
DAEMON_LOG_MAX_BYTES = envNum("SWARM_DAEMON_LOG_MAX_BYTES", 16 * 1024 * 1024);
|
|
975
1900
|
DAEMON_LOG_KEEP = envNum("SWARM_DAEMON_LOG_KEEP", 2);
|
|
976
1901
|
DAEMON_LOG_CHECK_MS = envNum("SWARM_DAEMON_LOG_CHECK_MS", 6e4);
|
|
977
1902
|
DIST_BUILD = true;
|
|
978
1903
|
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 ||
|
|
1904
|
+
SWARM_OUTBOX = process.env.SWARM_OUTBOX || join5(SWARM_HOME, "outbox.jsonl");
|
|
980
1905
|
SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
|
|
981
1906
|
SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
|
|
982
1907
|
SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
|
|
@@ -1001,15 +1926,15 @@ var init_config = __esm({
|
|
|
1001
1926
|
DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
|
|
1002
1927
|
DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
|
|
1003
1928
|
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 ||
|
|
1929
|
+
DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join5(SWARM_HOME, "drone-channel.key");
|
|
1005
1930
|
DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
|
|
1006
|
-
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT ||
|
|
1931
|
+
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join5(SWARM_HOME, "overmind.heartbeat");
|
|
1007
1932
|
HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
1008
1933
|
SWARM_REPO = process.env.SWARM_REPO || "";
|
|
1009
1934
|
SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
|
|
1010
1935
|
SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
|
|
1011
1936
|
SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
|
|
1012
|
-
REPO_MAP_PATH = process.env.SWARM_REPO_MAP ||
|
|
1937
|
+
REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join5(SWARM_HOME, "repo-map.json");
|
|
1013
1938
|
SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
|
|
1014
1939
|
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
1015
1940
|
SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
@@ -1020,7 +1945,7 @@ var init_config = __esm({
|
|
|
1020
1945
|
TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
|
|
1021
1946
|
SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
|
|
1022
1947
|
SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
|
|
1023
|
-
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF ||
|
|
1948
|
+
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join5(SWARM_HOME, "review-cutoff.json");
|
|
1024
1949
|
SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
|
|
1025
1950
|
SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
|
|
1026
1951
|
WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
|
|
@@ -1377,10 +2302,10 @@ var init_storm = __esm({
|
|
|
1377
2302
|
});
|
|
1378
2303
|
|
|
1379
2304
|
// src/heartbeat.ts
|
|
1380
|
-
import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as
|
|
2305
|
+
import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
1381
2306
|
function writeHeartbeat(path, now) {
|
|
1382
2307
|
const tmp = `${path}.tmp`;
|
|
1383
|
-
|
|
2308
|
+
writeFileSync5(tmp, String(now));
|
|
1384
2309
|
renameSync3(tmp, path);
|
|
1385
2310
|
}
|
|
1386
2311
|
function readHeartbeat(path) {
|
|
@@ -1721,16 +2646,21 @@ var init_procCleanup = __esm({
|
|
|
1721
2646
|
});
|
|
1722
2647
|
|
|
1723
2648
|
// src/git.ts
|
|
1724
|
-
import { existsSync as
|
|
2649
|
+
import { existsSync as existsSync9, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
|
|
1725
2650
|
import { appendFile, mkdir, readFile, rm } from "fs/promises";
|
|
1726
|
-
import { isAbsolute as isAbsolute2, join as
|
|
2651
|
+
import { isAbsolute as isAbsolute2, join as join15 } from "path";
|
|
1727
2652
|
import { $ as $2 } from "execa";
|
|
2653
|
+
function isUnrecoverableRefError(e) {
|
|
2654
|
+
if (e instanceof MissingRefError) return true;
|
|
2655
|
+
const msg = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
|
2656
|
+
return MISSING_REF_RE.test(msg);
|
|
2657
|
+
}
|
|
1728
2658
|
async function reclaimStaleWorktree(repo, worktree) {
|
|
1729
2659
|
await detachWorktree(repo, worktree);
|
|
1730
2660
|
}
|
|
1731
2661
|
function isLinkedWorktree(dir) {
|
|
1732
2662
|
try {
|
|
1733
|
-
return
|
|
2663
|
+
return statSync7(join15(dir, ".git")).isFile();
|
|
1734
2664
|
} catch {
|
|
1735
2665
|
return false;
|
|
1736
2666
|
}
|
|
@@ -1742,7 +2672,7 @@ async function detachWorktree(repo, worktree) {
|
|
|
1742
2672
|
}
|
|
1743
2673
|
}
|
|
1744
2674
|
async function ensureWorktreeDirGone(worktree) {
|
|
1745
|
-
if (!
|
|
2675
|
+
if (!existsSync9(worktree)) return false;
|
|
1746
2676
|
if (!isLinkedWorktree(worktree)) {
|
|
1747
2677
|
console.error(
|
|
1748
2678
|
`[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 +2687,7 @@ async function ensureWorktreeDirGone(worktree) {
|
|
|
1757
2687
|
);
|
|
1758
2688
|
return true;
|
|
1759
2689
|
}
|
|
1760
|
-
if (
|
|
2690
|
+
if (existsSync9(worktree)) {
|
|
1761
2691
|
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
2692
|
}
|
|
1763
2693
|
return true;
|
|
@@ -1800,7 +2730,7 @@ async function addWorktree(repo, path, branch, opts = {}) {
|
|
|
1800
2730
|
}
|
|
1801
2731
|
const busyAt = await worktreeForBranch(repo, branch);
|
|
1802
2732
|
if (busyAt) {
|
|
1803
|
-
const exists =
|
|
2733
|
+
const exists = existsSync9(busyAt);
|
|
1804
2734
|
const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
|
|
1805
2735
|
if (live) throw new WorktreeBusyError(branch, busyAt);
|
|
1806
2736
|
if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
|
|
@@ -1836,9 +2766,9 @@ async function ensureGitExcluded(worktree, pattern) {
|
|
|
1836
2766
|
const out = await $2({ reject: false })`git -C ${worktree} rev-parse --git-common-dir`;
|
|
1837
2767
|
const raw = out.stdout.trim();
|
|
1838
2768
|
if (out.exitCode !== 0 || !raw) return false;
|
|
1839
|
-
const commonDir = isAbsolute2(raw) ? raw :
|
|
1840
|
-
const infoDir =
|
|
1841
|
-
const excludeFile =
|
|
2769
|
+
const commonDir = isAbsolute2(raw) ? raw : join15(worktree, raw);
|
|
2770
|
+
const infoDir = join15(commonDir, "info");
|
|
2771
|
+
const excludeFile = join15(infoDir, "exclude");
|
|
1842
2772
|
let current = "";
|
|
1843
2773
|
try {
|
|
1844
2774
|
current = await readFile(excludeFile, "utf8");
|
|
@@ -1871,7 +2801,7 @@ async function worktreeDirty(path) {
|
|
|
1871
2801
|
async function repairMovedWorktrees(targetDir) {
|
|
1872
2802
|
const subdirs = (dir) => {
|
|
1873
2803
|
try {
|
|
1874
|
-
return
|
|
2804
|
+
return readdirSync10(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join15(dir, d.name));
|
|
1875
2805
|
} catch {
|
|
1876
2806
|
return [];
|
|
1877
2807
|
}
|
|
@@ -1915,7 +2845,9 @@ async function journalForBranch(repo, branch, slug) {
|
|
|
1915
2845
|
}
|
|
1916
2846
|
async function prepareDetachedWorktree(repo, branch, worktree) {
|
|
1917
2847
|
await $2`git -C ${repo} fetch --quiet origin`;
|
|
1918
|
-
|
|
2848
|
+
const ref = `origin/${branch}`;
|
|
2849
|
+
if (!await revParse(repo, `${ref}^{commit}`)) throw new MissingRefError(ref, repo);
|
|
2850
|
+
await $2`git -C ${repo} worktree add --detach --force ${worktree} ${ref}`;
|
|
1919
2851
|
}
|
|
1920
2852
|
async function mergeRef(worktree, ref) {
|
|
1921
2853
|
const before = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
|
|
@@ -1948,7 +2880,7 @@ async function worktreeLastCommitMs(worktree) {
|
|
|
1948
2880
|
const sec = Number(out);
|
|
1949
2881
|
return Number.isInteger(sec) && sec > 0 ? sec * 1e3 : null;
|
|
1950
2882
|
}
|
|
1951
|
-
var WorktreeBusyError, GITHUB_REMOTE_RE;
|
|
2883
|
+
var WorktreeBusyError, MissingRefError, MISSING_REF_RE, GITHUB_REMOTE_RE;
|
|
1952
2884
|
var init_git = __esm({
|
|
1953
2885
|
"src/git.ts"() {
|
|
1954
2886
|
"use strict";
|
|
@@ -1963,6 +2895,17 @@ var init_git = __esm({
|
|
|
1963
2895
|
branch;
|
|
1964
2896
|
worktreePath;
|
|
1965
2897
|
};
|
|
2898
|
+
MissingRefError = class extends Error {
|
|
2899
|
+
constructor(ref, repo) {
|
|
2900
|
+
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)`);
|
|
2901
|
+
this.ref = ref;
|
|
2902
|
+
this.repo = repo;
|
|
2903
|
+
this.name = "MissingRefError";
|
|
2904
|
+
}
|
|
2905
|
+
ref;
|
|
2906
|
+
repo;
|
|
2907
|
+
};
|
|
2908
|
+
MISSING_REF_RE = /invalid reference|unknown revision|not a valid object name|couldn't find remote ref/i;
|
|
1966
2909
|
GITHUB_REMOTE_RE = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/i;
|
|
1967
2910
|
}
|
|
1968
2911
|
});
|
|
@@ -1983,7 +2926,7 @@ __export(droneChannel_exports, {
|
|
|
1983
2926
|
verifyLocalCliToken: () => verifyLocalCliToken
|
|
1984
2927
|
});
|
|
1985
2928
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
1986
|
-
import { chmodSync, mkdirSync as
|
|
2929
|
+
import { chmodSync, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
|
|
1987
2930
|
import { dirname as dirname3 } from "path";
|
|
1988
2931
|
import { serve } from "@hono/node-server";
|
|
1989
2932
|
function loadChannelSecret(path) {
|
|
@@ -1993,7 +2936,7 @@ function loadChannelSecret(path) {
|
|
|
1993
2936
|
} catch {
|
|
1994
2937
|
}
|
|
1995
2938
|
const secret = randomBytes(32).toString("hex");
|
|
1996
|
-
|
|
2939
|
+
mkdirSync7(dirname3(path), { recursive: true });
|
|
1997
2940
|
writeFileSync9(path, secret, { mode: 384 });
|
|
1998
2941
|
try {
|
|
1999
2942
|
chmodSync(path, 384);
|
|
@@ -2324,7 +3267,7 @@ var init_droneChannel = __esm({
|
|
|
2324
3267
|
|
|
2325
3268
|
// src/attachments.ts
|
|
2326
3269
|
import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
|
|
2327
|
-
import { join as
|
|
3270
|
+
import { join as join18 } from "path";
|
|
2328
3271
|
function attachmentsFromDoc(doc, origin) {
|
|
2329
3272
|
const out = [];
|
|
2330
3273
|
const walk2 = (node) => {
|
|
@@ -2415,7 +3358,7 @@ async function materializeAttachments(opts) {
|
|
|
2415
3358
|
};
|
|
2416
3359
|
const result = { files: [], skipped: [] };
|
|
2417
3360
|
if (opts.refs.length === 0) return result;
|
|
2418
|
-
const dir =
|
|
3361
|
+
const dir = join18(opts.worktree, ATTACHMENTS_DIR);
|
|
2419
3362
|
await io.removeDir(dir);
|
|
2420
3363
|
let dirReady = false;
|
|
2421
3364
|
let total = 0;
|
|
@@ -2461,7 +3404,7 @@ async function materializeAttachments(opts) {
|
|
|
2461
3404
|
await io.mkdir(dir);
|
|
2462
3405
|
dirReady = true;
|
|
2463
3406
|
}
|
|
2464
|
-
await io.writeFile(
|
|
3407
|
+
await io.writeFile(join18(dir, name), file.bytes);
|
|
2465
3408
|
} catch (e) {
|
|
2466
3409
|
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
3410
|
continue;
|
|
@@ -2614,7 +3557,7 @@ var init_attachments = __esm({
|
|
|
2614
3557
|
|
|
2615
3558
|
// src/shotUpload.ts
|
|
2616
3559
|
import { readFile as fsReadFile } from "fs/promises";
|
|
2617
|
-
import { basename as basename2, isAbsolute as isAbsolute3, join as
|
|
3560
|
+
import { basename as basename2, isAbsolute as isAbsolute3, join as join19 } from "path";
|
|
2618
3561
|
function shotMimetype(path) {
|
|
2619
3562
|
const dot = path.lastIndexOf(".");
|
|
2620
3563
|
if (dot < 0) return null;
|
|
@@ -2640,7 +3583,7 @@ async function readShots(paths, opts) {
|
|
|
2640
3583
|
}
|
|
2641
3584
|
let bytes;
|
|
2642
3585
|
try {
|
|
2643
|
-
bytes = await read(isAbsolute3(path) ? path :
|
|
3586
|
+
bytes = await read(isAbsolute3(path) ? path : join19(opts.worktree, path));
|
|
2644
3587
|
} catch (e) {
|
|
2645
3588
|
skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
|
|
2646
3589
|
continue;
|
|
@@ -2717,12 +3660,12 @@ var init_shotUpload = __esm({
|
|
|
2717
3660
|
});
|
|
2718
3661
|
|
|
2719
3662
|
// src/oauth.ts
|
|
2720
|
-
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
3663
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
2721
3664
|
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
|
|
2722
|
-
import { join as
|
|
3665
|
+
import { join as join20 } from "path";
|
|
2723
3666
|
function makePkce() {
|
|
2724
3667
|
const verifier = randomBytes2(48).toString("base64url");
|
|
2725
|
-
const challenge =
|
|
3668
|
+
const challenge = createHash2("sha256").update(verifier).digest("base64url");
|
|
2726
3669
|
return { verifier, challenge };
|
|
2727
3670
|
}
|
|
2728
3671
|
async function registerClient(args, fetchFn = fetch) {
|
|
@@ -2854,15 +3797,15 @@ var init_oauth = __esm({
|
|
|
2854
3797
|
AUTHORIZE = "/api/oauth/authorize";
|
|
2855
3798
|
TOKEN = "/api/oauth/token";
|
|
2856
3799
|
SCOPE = "openid profile email taskless:full";
|
|
2857
|
-
STORE_PATH =
|
|
2858
|
-
CLIENT_PATH =
|
|
3800
|
+
STORE_PATH = join20(SWARM_HOME, "oauth.json");
|
|
3801
|
+
CLIENT_PATH = join20(SWARM_HOME, "oauth-client.json");
|
|
2859
3802
|
base = (api) => api.replace(/\/$/, "");
|
|
2860
3803
|
}
|
|
2861
3804
|
});
|
|
2862
3805
|
|
|
2863
3806
|
// src/agentToken.ts
|
|
2864
3807
|
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
|
|
2865
|
-
import { join as
|
|
3808
|
+
import { join as join21 } from "path";
|
|
2866
3809
|
function loadAgentToken() {
|
|
2867
3810
|
try {
|
|
2868
3811
|
const s = JSON.parse(readFileSync15(STORE_PATH2, "utf8"));
|
|
@@ -2882,6 +3825,14 @@ async function probeAgentToken(args, fetchFn = fetch) {
|
|
|
2882
3825
|
return "unreachable";
|
|
2883
3826
|
}
|
|
2884
3827
|
}
|
|
3828
|
+
function isRevokedTokenError(e) {
|
|
3829
|
+
if (e && typeof e === "object" && e.code === 401) return true;
|
|
3830
|
+
const text = e instanceof Error ? e.message : typeof e === "string" ? e : "";
|
|
3831
|
+
return /invalid_token|(?:http|status|code)[ =:]*401\b|\(401\)|401 unauthorized/i.test(text);
|
|
3832
|
+
}
|
|
3833
|
+
function revokedTokenLine() {
|
|
3834
|
+
return "[taskless] \u0442\u043E\u043A\u0435\u043D \u0440\u043E\u044F \u043E\u0442\u043E\u0437\u0432\u0430\u043D (401 invalid_token) \u2014 \u0440\u043E\u0439 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442 \u0440\u0430\u0431\u043E\u0442\u0443. \u041F\u043E\u0447\u0438\u043D\u0438 \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: swarm login";
|
|
3835
|
+
}
|
|
2885
3836
|
function revokedTokenComplaint() {
|
|
2886
3837
|
return [
|
|
2887
3838
|
"",
|
|
@@ -2902,14 +3853,14 @@ var init_agentToken = __esm({
|
|
|
2902
3853
|
"src/agentToken.ts"() {
|
|
2903
3854
|
"use strict";
|
|
2904
3855
|
init_config();
|
|
2905
|
-
STORE_PATH2 =
|
|
3856
|
+
STORE_PATH2 = join21(SWARM_HOME, "agent.json");
|
|
2906
3857
|
base2 = (api) => api.replace(/\/$/, "");
|
|
2907
3858
|
}
|
|
2908
3859
|
});
|
|
2909
3860
|
|
|
2910
3861
|
// src/workspace.ts
|
|
2911
3862
|
import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
|
|
2912
|
-
import { join as
|
|
3863
|
+
import { join as join22 } from "path";
|
|
2913
3864
|
function loadWorkspace() {
|
|
2914
3865
|
try {
|
|
2915
3866
|
const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
|
|
@@ -2926,7 +3877,7 @@ var init_workspace = __esm({
|
|
|
2926
3877
|
"src/workspace.ts"() {
|
|
2927
3878
|
"use strict";
|
|
2928
3879
|
init_config();
|
|
2929
|
-
WORKSPACE_PATH =
|
|
3880
|
+
WORKSPACE_PATH = join22(SWARM_HOME, "workspace.json");
|
|
2930
3881
|
}
|
|
2931
3882
|
});
|
|
2932
3883
|
|
|
@@ -3009,6 +3960,11 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
|
|
|
3009
3960
|
if (!doc) return null;
|
|
3010
3961
|
return { title: doc.title ?? "", body: extractText(doc.body) };
|
|
3011
3962
|
}
|
|
3963
|
+
async function getTaskContext(idOrSlug) {
|
|
3964
|
+
const res = await safeCall("get_task_context", { id: idOrSlug });
|
|
3965
|
+
const text = res?.content?.find((c) => c.type === "text")?.text;
|
|
3966
|
+
return typeof text === "string" && text.trim() ? text : null;
|
|
3967
|
+
}
|
|
3012
3968
|
async function resolveId(idOrSlug) {
|
|
3013
3969
|
const cached = idCache.get(idOrSlug);
|
|
3014
3970
|
if (cached) return cached;
|
|
@@ -3076,6 +4032,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
|
|
|
3076
4032
|
},
|
|
3077
4033
|
backlogStatusId,
|
|
3078
4034
|
getDocument,
|
|
4035
|
+
getTaskContext,
|
|
3079
4036
|
createTask: async (input) => {
|
|
3080
4037
|
if (!input.projectId) {
|
|
3081
4038
|
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 +4072,19 @@ function requireTaskBody(task, slug, workspaceId) {
|
|
|
3115
4072
|
if (!task) throw new TaskBodyUnavailableError(slug, workspaceId);
|
|
3116
4073
|
return task;
|
|
3117
4074
|
}
|
|
4075
|
+
function wrapUntrusted(parts) {
|
|
4076
|
+
if (parts.length === 0) return "";
|
|
4077
|
+
return [
|
|
4078
|
+
UNTRUSTED_TASK_INPUT_NOTICE,
|
|
4079
|
+
"",
|
|
4080
|
+
"\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",
|
|
4081
|
+
parts.join("\n\n"),
|
|
4082
|
+
"\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"
|
|
4083
|
+
].join("\n");
|
|
4084
|
+
}
|
|
3118
4085
|
async function fetchTaskContext(client, task) {
|
|
4086
|
+
const brief = await client.getTaskContext(task.id);
|
|
4087
|
+
if (brief) return wrapUntrusted([brief]);
|
|
3119
4088
|
const parts = [];
|
|
3120
4089
|
const comments = task.comments ?? [];
|
|
3121
4090
|
if (comments.length > 0) {
|
|
@@ -3151,14 +4120,7 @@ ${text || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
|
|
3151
4120
|
)
|
|
3152
4121
|
);
|
|
3153
4122
|
}
|
|
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");
|
|
4123
|
+
return wrapUntrusted(parts);
|
|
3162
4124
|
}
|
|
3163
4125
|
function commentIdOf(res) {
|
|
3164
4126
|
const comment = res?.structuredContent?.comment;
|
|
@@ -3270,7 +4232,9 @@ async function connectTaskless(cfg) {
|
|
|
3270
4232
|
try {
|
|
3271
4233
|
await client.connect(transport, { timeout: TASKLESS_CONNECT_TIMEOUT_MS });
|
|
3272
4234
|
} catch (e) {
|
|
3273
|
-
console.warn(
|
|
4235
|
+
console.warn(
|
|
4236
|
+
isRevokedTokenError(e) ? `${revokedTokenLine()} (\u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442 \u2014 slug-only \u0440\u0435\u0436\u0438\u043C)` : `Taskless: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 slug-only. \u0412\u043E\u0439\u0434\u0438 \u0437\u0430\u043D\u043E\u0432\u043E.`
|
|
4237
|
+
);
|
|
3274
4238
|
return null;
|
|
3275
4239
|
}
|
|
3276
4240
|
const call = async (name, args) => await client.callTool({ name, arguments: args });
|
|
@@ -3294,7 +4258,9 @@ async function connectSwarmMcp(cfg) {
|
|
|
3294
4258
|
try {
|
|
3295
4259
|
await client.connect(transport);
|
|
3296
4260
|
} catch (e) {
|
|
3297
|
-
console.warn(
|
|
4261
|
+
console.warn(
|
|
4262
|
+
isRevokedTokenError(e) ? `${revokedTokenLine()} (\u043F\u043E\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u0430 \u043D\u0435\u0442 \u2014 \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u043A\u043E\u043F\u044F\u0442\u0441\u044F \u0432 outbox)` : `Swarm MCP: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 \u0431\u0443\u0444\u0435\u0440 \u0432 outbox.`
|
|
4263
|
+
);
|
|
3298
4264
|
return null;
|
|
3299
4265
|
}
|
|
3300
4266
|
return async (name, args) => await client.callTool({ name, arguments: swarmToolArgs(name, args, workspace) });
|
|
@@ -3353,7 +4319,7 @@ __export(db_exports, {
|
|
|
3353
4319
|
SwarmDb: () => SwarmDb
|
|
3354
4320
|
});
|
|
3355
4321
|
import { createRequire as createRequire2 } from "module";
|
|
3356
|
-
import { existsSync as
|
|
4322
|
+
import { existsSync as existsSync11 } from "fs";
|
|
3357
4323
|
import { resolve as resolve3 } from "path";
|
|
3358
4324
|
function rowToQuestion(row) {
|
|
3359
4325
|
return {
|
|
@@ -3387,6 +4353,10 @@ function rowToRun(row) {
|
|
|
3387
4353
|
depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
|
|
3388
4354
|
model: row.model ?? null,
|
|
3389
4355
|
cost_usd: row.cost_usd ?? 0,
|
|
4356
|
+
// Токены/время работы (TASWM-353) — старые прогоны без колонок → 0.
|
|
4357
|
+
tokens_in: row.tokens_in ?? 0,
|
|
4358
|
+
tokens_out: row.tokens_out ?? 0,
|
|
4359
|
+
duration_ms: row.duration_ms ?? 0,
|
|
3390
4360
|
pr_url: row.pr_url ?? null,
|
|
3391
4361
|
superseded_by: row.superseded_by ?? null,
|
|
3392
4362
|
decision: row.decision ? JSON.parse(row.decision) : null,
|
|
@@ -3452,6 +4422,9 @@ CREATE TABLE IF NOT EXISTS runs (
|
|
|
3452
4422
|
depends_on TEXT,
|
|
3453
4423
|
model TEXT,
|
|
3454
4424
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
4425
|
+
tokens_in INTEGER NOT NULL DEFAULT 0,
|
|
4426
|
+
tokens_out INTEGER NOT NULL DEFAULT 0,
|
|
4427
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
3455
4428
|
pr_url TEXT,
|
|
3456
4429
|
superseded_by INTEGER,
|
|
3457
4430
|
decision TEXT,
|
|
@@ -3545,7 +4518,11 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
3545
4518
|
// Базовая ветка PR (TASWM-285): куда дрон открывает PR и от чего ответвляется.
|
|
3546
4519
|
"ALTER TABLE runs ADD COLUMN base_branch TEXT",
|
|
3547
4520
|
// Воркспейс задачи прогона (TASWM-288): им пинятся задачные MCP-вызовы прогона.
|
|
3548
|
-
"ALTER TABLE runs ADD COLUMN workspace_id TEXT"
|
|
4521
|
+
"ALTER TABLE runs ADD COLUMN workspace_id TEXT",
|
|
4522
|
+
// Чем обошёлся прогон, кроме денег (TASWM-353): токены вход/выход и время работы.
|
|
4523
|
+
"ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
|
|
4524
|
+
"ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
|
|
4525
|
+
"ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0"
|
|
3549
4526
|
]) {
|
|
3550
4527
|
try {
|
|
3551
4528
|
this.db.exec(alter);
|
|
@@ -3838,7 +4815,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
3838
4815
|
const repoN = normalizeRepo(repo);
|
|
3839
4816
|
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
3840
4817
|
if (already) return { imported: false, runs: already.run_count };
|
|
3841
|
-
if (!
|
|
4818
|
+
if (!existsSync11(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
3842
4819
|
const src = new this.databaseCtor(abs);
|
|
3843
4820
|
try {
|
|
3844
4821
|
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
@@ -4206,9 +5183,9 @@ var init_droneStore = __esm({
|
|
|
4206
5183
|
});
|
|
4207
5184
|
|
|
4208
5185
|
// src/overmind.ts
|
|
4209
|
-
import { homedir as
|
|
4210
|
-
import { existsSync as
|
|
4211
|
-
import { basename as basename4, dirname as
|
|
5186
|
+
import { homedir as homedir5 } from "os";
|
|
5187
|
+
import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync12, renameSync as renameSync10 } from "fs";
|
|
5188
|
+
import { basename as basename4, dirname as dirname4, join as join29 } from "path";
|
|
4212
5189
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
4213
5190
|
import { spawn as spawn3 } from "child_process";
|
|
4214
5191
|
|
|
@@ -4288,13 +5265,13 @@ init_config();
|
|
|
4288
5265
|
init_resources();
|
|
4289
5266
|
init_names2();
|
|
4290
5267
|
init_storm();
|
|
4291
|
-
import { appendFileSync, existsSync as
|
|
5268
|
+
import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
4292
5269
|
import { dirname } from "path";
|
|
4293
5270
|
import { randomUUID } from "crypto";
|
|
4294
5271
|
|
|
4295
5272
|
// src/logTail.ts
|
|
4296
|
-
import { closeSync, existsSync as
|
|
4297
|
-
import { join as
|
|
5273
|
+
import { closeSync, existsSync as existsSync4, openSync, readSync, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
|
|
5274
|
+
import { join as join6 } from "path";
|
|
4298
5275
|
var TAIL_LINES_FALLBACK = 200;
|
|
4299
5276
|
var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
|
|
4300
5277
|
function fileHeader(name, size, shown) {
|
|
@@ -4304,7 +5281,7 @@ function fileHeader(name, size, shown) {
|
|
|
4304
5281
|
function readTail(path, opts) {
|
|
4305
5282
|
let size;
|
|
4306
5283
|
try {
|
|
4307
|
-
size =
|
|
5284
|
+
size = statSync3(path).size;
|
|
4308
5285
|
} catch {
|
|
4309
5286
|
return null;
|
|
4310
5287
|
}
|
|
@@ -4335,10 +5312,10 @@ function readTail(path, opts) {
|
|
|
4335
5312
|
return { text, size, shown: Buffer.byteLength(text, "utf8") };
|
|
4336
5313
|
}
|
|
4337
5314
|
function latestRunLaunch(runLogsDir, localRunId) {
|
|
4338
|
-
const dir =
|
|
5315
|
+
const dir = join6(runLogsDir, String(localRunId));
|
|
4339
5316
|
let files;
|
|
4340
5317
|
try {
|
|
4341
|
-
files =
|
|
5318
|
+
files = readdirSync4(dir);
|
|
4342
5319
|
} catch {
|
|
4343
5320
|
return null;
|
|
4344
5321
|
}
|
|
@@ -4349,13 +5326,13 @@ function latestRunLaunch(runLogsDir, localRunId) {
|
|
|
4349
5326
|
}
|
|
4350
5327
|
if (indexes.size === 0) return null;
|
|
4351
5328
|
const index = Math.max(...indexes);
|
|
4352
|
-
const out = [
|
|
4353
|
-
const err =
|
|
5329
|
+
const out = [join6(dir, `${index}.json`), join6(dir, `${index}.json.done`)].find((p) => existsSync4(p)) ?? null;
|
|
5330
|
+
const err = existsSync4(join6(dir, `${index}.err.log`)) ? join6(dir, `${index}.err.log`) : null;
|
|
4354
5331
|
return { index, total: indexes.size, out, err };
|
|
4355
5332
|
}
|
|
4356
5333
|
function runLogTail(runLogsDir, localRunId, opts) {
|
|
4357
5334
|
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 (${
|
|
5335
|
+
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
5336
|
const parts = [];
|
|
4360
5337
|
let budget = opts.maxBytes;
|
|
4361
5338
|
if (launch.err) {
|
|
@@ -4488,6 +5465,15 @@ var SwarmClient = class _SwarmClient {
|
|
|
4488
5465
|
nextRunId = 1;
|
|
4489
5466
|
nextEventId = 1;
|
|
4490
5467
|
nextQuestionId = 1;
|
|
5468
|
+
// TASWM-350: ключ НЕЗАКРЫТОЙ попытки взять работу. Живёт от первого вызова claim до
|
|
5469
|
+
// первого настоящего ответа прода; всё это время ретраи идут с ним, и повтор после
|
|
5470
|
+
// обрыва связи не превращается во второе взятие. См. claim().
|
|
5471
|
+
pendingClaimKey = null;
|
|
5472
|
+
// TASWM-357: последнее НАПЕЧАТАННОЕ про пропущенные claim'ом задачи (родители и
|
|
5473
|
+
// заблокированные). Дедуп по тексту: claim идёт раз в ~60 c, а набор пропущенных меняется
|
|
5474
|
+
// редко — без этого лог был бы простынёй из одной и той же строки, и в ней потонуло бы
|
|
5475
|
+
// всё остальное. Меняется набор (эпик закрыли / блокер прошли) → печатаем снова.
|
|
5476
|
+
lastClaimSkipLog = null;
|
|
4491
5477
|
// Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
|
|
4492
5478
|
// Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
|
|
4493
5479
|
// переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
|
|
@@ -4498,6 +5484,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
4498
5484
|
// ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
|
|
4499
5485
|
// каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
|
|
4500
5486
|
notifyLevelUnsupported = false;
|
|
5487
|
+
// TAS-485: то же для гашения разрешившихся тревог (swarm_notify_resolve) — прод старее
|
|
5488
|
+
// демона не знает тула; ретраить это каждый тик бессмысленно, см. resolveAnomalies.
|
|
5489
|
+
notifyResolveUnsupported = false;
|
|
4501
5490
|
// Корреляция с продом.
|
|
4502
5491
|
keyByLocal = /* @__PURE__ */ new Map();
|
|
4503
5492
|
// local run id → runKey
|
|
@@ -4540,24 +5529,24 @@ var SwarmClient = class _SwarmClient {
|
|
|
4540
5529
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
4541
5530
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
4542
5531
|
this.host = opts.host ?? null;
|
|
4543
|
-
this.repoExists = opts.repoExists ??
|
|
5532
|
+
this.repoExists = opts.repoExists ?? existsSync5;
|
|
4544
5533
|
this.questionSyncMs = opts.questionSyncMs ?? 3e3;
|
|
4545
5534
|
this.outboxPath = opts.outboxPath;
|
|
4546
5535
|
this.mapPath = `${opts.outboxPath}.prodmap.json`;
|
|
4547
5536
|
this.stormPath = `${opts.outboxPath}.storm.jsonl`;
|
|
4548
|
-
|
|
5537
|
+
mkdirSync4(dirname(this.outboxPath), { recursive: true });
|
|
4549
5538
|
this.recover();
|
|
4550
5539
|
}
|
|
4551
5540
|
// ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
|
|
4552
5541
|
recover() {
|
|
4553
|
-
if (
|
|
5542
|
+
if (existsSync5(this.mapPath)) {
|
|
4554
5543
|
try {
|
|
4555
5544
|
const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
|
|
4556
5545
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
4557
5546
|
} catch {
|
|
4558
5547
|
}
|
|
4559
5548
|
}
|
|
4560
|
-
if (
|
|
5549
|
+
if (existsSync5(this.outboxPath)) {
|
|
4561
5550
|
const txt = readFileSync4(this.outboxPath, "utf8");
|
|
4562
5551
|
for (const line of txt.split("\n")) {
|
|
4563
5552
|
const s = line.trim();
|
|
@@ -4578,7 +5567,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
4578
5567
|
// eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
|
|
4579
5568
|
// переприсваиваем локально (id событий — только порядок, не корреляция с продом).
|
|
4580
5569
|
replayGlobalEvents() {
|
|
4581
|
-
if (!
|
|
5570
|
+
if (!existsSync5(this.stormPath)) return;
|
|
4582
5571
|
let txt;
|
|
4583
5572
|
try {
|
|
4584
5573
|
txt = readFileSync4(this.stormPath, "utf8");
|
|
@@ -4647,8 +5636,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
4647
5636
|
this.scheduleFlush();
|
|
4648
5637
|
}
|
|
4649
5638
|
// Прод-патч прогона: локальные поля Run → поля swarm_run_upsert (см. TASWM-74).
|
|
4650
|
-
// pid/worktree_path (рантайм),
|
|
4651
|
-
//
|
|
5639
|
+
// pid/worktree_path (рантайм), снимок задачи (task_*, прод берёт join'ом из Task)
|
|
5640
|
+
// и priority (локальная очередь ЭТОГО демона, TASWM-269)
|
|
4652
5641
|
// в прод НЕ уходят. kind → role (плоский enum). Self-ref
|
|
4653
5642
|
// прогонов hero_run_id/resolves_run_id/superseded_by → heroId/resolvesRunId/
|
|
4654
5643
|
// supersededById как ОТЛОЖЕННЫЕ ссылки (RunRef) — cuid резолвится на флаше.
|
|
@@ -4659,6 +5648,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
4659
5648
|
if ("model" in patch) out.model = patch.model ?? null;
|
|
4660
5649
|
if ("name" in patch) out.droneName = patch.name ?? null;
|
|
4661
5650
|
if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
|
|
5651
|
+
if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
|
|
5652
|
+
if ("tokens_out" in patch && patch.tokens_out !== void 0) out.tokensOut = patch.tokens_out;
|
|
5653
|
+
if ("duration_ms" in patch && patch.duration_ms !== void 0) out.durationMs = patch.duration_ms;
|
|
5654
|
+
if ("respawns" in patch && patch.respawns !== void 0) out.respawns = patch.respawns;
|
|
4662
5655
|
if ("pr_url" in patch) out.prUrl = patch.pr_url ?? null;
|
|
4663
5656
|
if ("branch" in patch) out.branch = patch.branch ?? null;
|
|
4664
5657
|
if ("repo" in patch) out.repo = patch.repo ?? null;
|
|
@@ -4724,6 +5717,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
4724
5717
|
depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
|
|
4725
5718
|
model: input.model ?? null,
|
|
4726
5719
|
cost_usd: 0,
|
|
5720
|
+
tokens_in: 0,
|
|
5721
|
+
// копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
|
|
5722
|
+
tokens_out: 0,
|
|
5723
|
+
duration_ms: 0,
|
|
4727
5724
|
pr_url: null,
|
|
4728
5725
|
superseded_by: null,
|
|
4729
5726
|
decision: input.decision ?? null,
|
|
@@ -4846,6 +5843,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
4846
5843
|
if (!d) return 0;
|
|
4847
5844
|
d.respawns += 1;
|
|
4848
5845
|
d.updated_at = stamp();
|
|
5846
|
+
this.enqueueRun(d, this.runFields({ respawns: d.respawns }));
|
|
4849
5847
|
return d.respawns;
|
|
4850
5848
|
}
|
|
4851
5849
|
cancel(id, reason) {
|
|
@@ -5261,7 +6259,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
5261
6259
|
* транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
|
|
5262
6260
|
*/
|
|
5263
6261
|
reconcileRuntime(opts) {
|
|
5264
|
-
const wtExists = opts.worktreeExists ??
|
|
6262
|
+
const wtExists = opts.worktreeExists ?? existsSync5;
|
|
5265
6263
|
const pidByWorktree = /* @__PURE__ */ new Map();
|
|
5266
6264
|
try {
|
|
5267
6265
|
for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
|
|
@@ -5349,23 +6347,69 @@ var SwarmClient = class _SwarmClient {
|
|
|
5349
6347
|
* шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
|
|
5350
6348
|
*/
|
|
5351
6349
|
async claim() {
|
|
6350
|
+
this.pendingClaimKey ??= randomUUID();
|
|
5352
6351
|
let res;
|
|
5353
6352
|
try {
|
|
5354
6353
|
res = await this.call("swarm_claim_work", {
|
|
5355
6354
|
...this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {},
|
|
5356
6355
|
// TASWM-281: взятая из общей очереди задача сразу принадлежит ЭТОЙ машине —
|
|
5357
6356
|
// прод помечает созданный прогон hostId, чужой демон его не увидит.
|
|
5358
|
-
...this.host ? { hostId: this.host.hostId } : {}
|
|
6357
|
+
...this.host ? { hostId: this.host.hostId } : {},
|
|
6358
|
+
idempotencyKey: this.pendingClaimKey
|
|
5359
6359
|
});
|
|
5360
6360
|
} catch {
|
|
5361
6361
|
return null;
|
|
5362
6362
|
}
|
|
5363
6363
|
if (res?.isError) return null;
|
|
6364
|
+
this.pendingClaimKey = null;
|
|
5364
6365
|
const sc = res.structuredContent ?? {};
|
|
6366
|
+
this.logClaimSkips(sc);
|
|
5365
6367
|
if (!sc.claimed || typeof sc.run !== "object" || sc.run === null) return null;
|
|
5366
6368
|
const local = this.hydrateRun(sc.run);
|
|
5367
6369
|
return local ? this.mat(local) : null;
|
|
5368
6370
|
}
|
|
6371
|
+
/**
|
|
6372
|
+
* TASWM-357: назвать в логе задачи, которые прод НЕ отдал в работу осознанно — родителей с
|
|
6373
|
+
* незавершёнными подзадачами (работа всегда в листьях) и листья с незакрытым блокером
|
|
6374
|
+
* (явный порядок, связь задач «блокирует»). Печатаем ЗДЕСЬ, а не на проде: оба фильтра
|
|
6375
|
+
* живут SQL-условиями и молчат по природе, а `display` claim'а демон не логирует — без
|
|
6376
|
+
* этой строки «почему эпик стоит нетронутым» не видно ниоткуда, и оператор ровно так же
|
|
6377
|
+
* гадает, как гадал, пока дрон вставал на эпике с вопросом «меня спавнили на родителя».
|
|
6378
|
+
*
|
|
6379
|
+
* Формат прода — `data.skippedParents: [{slug,title,openChildren}]` и
|
|
6380
|
+
* `data.skippedBlocked: [{slug,title,blockers[]}]`. Читаем защищённо: у старого прода
|
|
6381
|
+
* полей нет вовсе (тогда молчим), а разбирать чужую строку `display` вместо структуры —
|
|
6382
|
+
* тот же класс хрупкости, что уже кусал нас в notifyAnomaly.
|
|
6383
|
+
*/
|
|
6384
|
+
logClaimSkips(sc) {
|
|
6385
|
+
const rows = (raw) => (Array.isArray(raw) ? raw : []).map((p) => typeof p === "object" && p !== null ? p : null).filter((p) => p !== null && typeof p.slug === "string");
|
|
6386
|
+
const others = (flag) => flag === true ? " \u0438 \u0434\u0440\u0443\u0433\u0438\u0435" : "";
|
|
6387
|
+
const lines = [];
|
|
6388
|
+
const parents = rows(sc.skippedParents).map(
|
|
6389
|
+
(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)`
|
|
6390
|
+
);
|
|
6391
|
+
if (parents.length > 0) {
|
|
6392
|
+
lines.push(
|
|
6393
|
+
`[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)}`
|
|
6394
|
+
);
|
|
6395
|
+
}
|
|
6396
|
+
const blocked = rows(sc.skippedBlocked).map(
|
|
6397
|
+
(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"}`
|
|
6398
|
+
);
|
|
6399
|
+
if (blocked.length > 0) {
|
|
6400
|
+
lines.push(
|
|
6401
|
+
`[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)}`
|
|
6402
|
+
);
|
|
6403
|
+
}
|
|
6404
|
+
const text = lines.join("\n");
|
|
6405
|
+
if (text.length === 0) {
|
|
6406
|
+
this.lastClaimSkipLog = null;
|
|
6407
|
+
return;
|
|
6408
|
+
}
|
|
6409
|
+
if (text === this.lastClaimSkipLog) return;
|
|
6410
|
+
this.lastClaimSkipLog = text;
|
|
6411
|
+
for (const line of lines) console.log(line);
|
|
6412
|
+
}
|
|
5369
6413
|
/**
|
|
5370
6414
|
* TASWM-281: пометить своими прогоны, под которыми на ЭТОЙ машине нашёлся ЖИВОЙ
|
|
5371
6415
|
* процесс (после reconcileRuntime на старте). Это ровно те прогоны, где сомнений в
|
|
@@ -5639,12 +6683,20 @@ var SwarmClient = class _SwarmClient {
|
|
|
5639
6683
|
* Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
|
|
5640
6684
|
* означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
|
|
5641
6685
|
*
|
|
6686
|
+
* TASWM-352: тем же пульсом уходит ЁМКОСТЬ этой машины (`capacity` — живой потолок
|
|
6687
|
+
* дронов ЭТОГО демона: машинный оверрайд, иначе общая настройка владельца). По ней
|
|
6688
|
+
* планировщик роя раскладывает очередь между машинами: до этого у прода было ОДНО
|
|
6689
|
+
* число на владельца, и ноутбук с рабочей станцией делили его поровну, хотя тянут
|
|
6690
|
+
* разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
|
|
6691
|
+
* трогает прошлое значение — та же логика, что у repoFullNames.
|
|
6692
|
+
*
|
|
5642
6693
|
* Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
|
|
5643
6694
|
*/
|
|
5644
|
-
async heartbeat(daemonVersion, pause) {
|
|
6695
|
+
async heartbeat(daemonVersion, pause, capacity) {
|
|
5645
6696
|
try {
|
|
5646
6697
|
const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
|
|
5647
6698
|
if (daemonVersion) args.daemonVersion = daemonVersion;
|
|
6699
|
+
if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
|
|
5648
6700
|
if (pause !== void 0) {
|
|
5649
6701
|
args.paused = !!pause?.paused;
|
|
5650
6702
|
if (pause?.reason) args.pauseReason = pause.reason;
|
|
@@ -5750,6 +6802,35 @@ var SwarmClient = class _SwarmClient {
|
|
|
5750
6802
|
const res = await this.call("swarm_notify", base3);
|
|
5751
6803
|
if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
|
|
5752
6804
|
}
|
|
6805
|
+
/**
|
|
6806
|
+
* TAS-485: погасить в инбоксе владельца тревоги, чьё условие ушло. Ключи — те же, с
|
|
6807
|
+
* которыми уходил notifyAnomaly (прод сам ставит им префикс `swarm:`).
|
|
6808
|
+
*
|
|
6809
|
+
* Бросает при сбое — СОЗНАТЕЛЬНО, как notifyAnomaly: вызывающий (tickOnce) вычёркивает
|
|
6810
|
+
* ключи из durable-очереди только после успеха, иначе «погашено» разошлось бы с тем,
|
|
6811
|
+
* что человек видит в инбоксе, и разошлось бы навсегда — повторить будет некому.
|
|
6812
|
+
*
|
|
6813
|
+
* ДЕПЛОЙ-ОКНО. Демон из свежего чекаута против ещё не задеплоенного прода получит
|
|
6814
|
+
* «неизвестный тул». Это не ошибка доставки, а отсутствие фичи на той стороне: ретраить
|
|
6815
|
+
* её каждые ~4 секунды бессмысленно, поэтому первую такую осечку запоминаем на жизнь
|
|
6816
|
+
* процесса и дальше молча пропускаем гашение (надзор в остальном работает как раньше).
|
|
6817
|
+
* Тот же приём, что у notifyAnomaly с аргументом `level`.
|
|
6818
|
+
*/
|
|
6819
|
+
async resolveAnomalies(keys) {
|
|
6820
|
+
const tags = [...new Set(keys.filter((k) => k.length > 0 && k.length <= 120))];
|
|
6821
|
+
if (tags.length === 0 || this.notifyResolveUnsupported) return;
|
|
6822
|
+
const res = await this.call("swarm_notify_resolve", { tags });
|
|
6823
|
+
if (!res?.isError) return;
|
|
6824
|
+
const text = JSON.stringify(res);
|
|
6825
|
+
if (text.includes("swarm_notify_resolve") && (text.includes("Unknown tool") || text.includes("\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D"))) {
|
|
6826
|
+
this.notifyResolveUnsupported = true;
|
|
6827
|
+
console.warn(
|
|
6828
|
+
"[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."
|
|
6829
|
+
);
|
|
6830
|
+
return;
|
|
6831
|
+
}
|
|
6832
|
+
throw new Error(`swarm_notify_resolve \u043E\u0442\u0432\u0435\u0440\u0433 ${tags.length} \u043A\u043B\u044E\u0447(\u0435\u0439)`);
|
|
6833
|
+
}
|
|
5753
6834
|
/**
|
|
5754
6835
|
* TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
|
|
5755
6836
|
* через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
|
|
@@ -5814,6 +6895,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
5814
6895
|
depends_on: [],
|
|
5815
6896
|
model: null,
|
|
5816
6897
|
cost_usd: 0,
|
|
6898
|
+
tokens_in: 0,
|
|
6899
|
+
// токены и время работы (TASWM-353) — прод-поля, заполняются ниже
|
|
6900
|
+
tokens_out: 0,
|
|
6901
|
+
duration_ms: 0,
|
|
5817
6902
|
pr_url: null,
|
|
5818
6903
|
superseded_by: null,
|
|
5819
6904
|
decision: null,
|
|
@@ -5824,9 +6909,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
5824
6909
|
review_verdict: null,
|
|
5825
6910
|
review_phase: null,
|
|
5826
6911
|
// вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
|
|
5827
|
-
// Локальная бухгалтерия (в прод-схему не
|
|
5828
|
-
//
|
|
5829
|
-
//
|
|
6912
|
+
// Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
|
|
6913
|
+
// существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
|
|
6914
|
+
// в отличие от respawns, — авто-ревью v2, TASWM-162.
|
|
5830
6915
|
review_cycle: 0,
|
|
5831
6916
|
task_title: null,
|
|
5832
6917
|
task_desc: null,
|
|
@@ -5878,6 +6963,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
5878
6963
|
local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
|
|
5879
6964
|
local.model = pr.model ?? null;
|
|
5880
6965
|
local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
|
|
6966
|
+
local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
|
|
6967
|
+
local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
|
|
6968
|
+
local.duration_ms = typeof pr.durationMs === "number" ? pr.durationMs : 0;
|
|
5881
6969
|
local.pr_url = pr.prUrl ?? null;
|
|
5882
6970
|
local.stop_reason = pr.stopReason ?? null;
|
|
5883
6971
|
local.respawns = typeof pr.respawns === "number" ? pr.respawns : 0;
|
|
@@ -6135,13 +7223,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
6135
7223
|
}
|
|
6136
7224
|
persistPending() {
|
|
6137
7225
|
try {
|
|
6138
|
-
|
|
7226
|
+
writeFileSync4(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
|
|
6139
7227
|
} catch {
|
|
6140
7228
|
}
|
|
6141
7229
|
}
|
|
6142
7230
|
persistMap() {
|
|
6143
7231
|
try {
|
|
6144
|
-
|
|
7232
|
+
writeFileSync4(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
|
|
6145
7233
|
} catch {
|
|
6146
7234
|
}
|
|
6147
7235
|
}
|
|
@@ -6171,11 +7259,11 @@ init_config();
|
|
|
6171
7259
|
init_heartbeat();
|
|
6172
7260
|
|
|
6173
7261
|
// src/logRotate.ts
|
|
6174
|
-
import { copyFileSync, renameSync as renameSync4, statSync as
|
|
7262
|
+
import { copyFileSync, renameSync as renameSync4, statSync as statSync4, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
|
|
6175
7263
|
var realIo = {
|
|
6176
7264
|
size: (path) => {
|
|
6177
7265
|
try {
|
|
6178
|
-
return
|
|
7266
|
+
return statSync4(path).size;
|
|
6179
7267
|
} catch {
|
|
6180
7268
|
return null;
|
|
6181
7269
|
}
|
|
@@ -6184,7 +7272,7 @@ var realIo = {
|
|
|
6184
7272
|
rename: renameSync4,
|
|
6185
7273
|
truncate: (path) => truncateSync(path, 0),
|
|
6186
7274
|
unlink: unlinkSync2,
|
|
6187
|
-
exists:
|
|
7275
|
+
exists: existsSync6
|
|
6188
7276
|
};
|
|
6189
7277
|
var realOwnerIo = {
|
|
6190
7278
|
statFd: (fd) => {
|
|
@@ -6197,7 +7285,7 @@ var realOwnerIo = {
|
|
|
6197
7285
|
},
|
|
6198
7286
|
statPath: (path) => {
|
|
6199
7287
|
try {
|
|
6200
|
-
const s =
|
|
7288
|
+
const s = statSync4(path);
|
|
6201
7289
|
return { dev: s.dev, ino: s.ino };
|
|
6202
7290
|
} catch {
|
|
6203
7291
|
return null;
|
|
@@ -6268,13 +7356,13 @@ async function buildRepoRegistry(paths) {
|
|
|
6268
7356
|
}
|
|
6269
7357
|
|
|
6270
7358
|
// src/repoMemory.ts
|
|
6271
|
-
import { readFileSync as readFileSync6, writeFileSync as
|
|
6272
|
-
import { join as
|
|
7359
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
7360
|
+
import { join as join7 } from "path";
|
|
6273
7361
|
var FILE = "repos.json";
|
|
6274
7362
|
function loadRememberedRepos(home) {
|
|
6275
7363
|
let raw;
|
|
6276
7364
|
try {
|
|
6277
|
-
raw = readFileSync6(
|
|
7365
|
+
raw = readFileSync6(join7(home, FILE), "utf8");
|
|
6278
7366
|
} catch {
|
|
6279
7367
|
return [];
|
|
6280
7368
|
}
|
|
@@ -6289,7 +7377,7 @@ function loadRememberedRepos(home) {
|
|
|
6289
7377
|
function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
|
|
6290
7378
|
const clean = paths.filter((p) => p.trim().length > 0);
|
|
6291
7379
|
if (clean.length === 0) return;
|
|
6292
|
-
|
|
7380
|
+
writeFileSync6(join7(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
|
|
6293
7381
|
`);
|
|
6294
7382
|
}
|
|
6295
7383
|
function pickRepoPaths(args) {
|
|
@@ -6302,13 +7390,13 @@ function pickRepoPaths(args) {
|
|
|
6302
7390
|
// src/repoDiscovery.ts
|
|
6303
7391
|
init_src();
|
|
6304
7392
|
init_config();
|
|
6305
|
-
import { homedir as
|
|
6306
|
-
import { readFileSync as readFileSync8, renameSync as renameSync5, writeFileSync as
|
|
6307
|
-
import { isAbsolute, join as
|
|
7393
|
+
import { homedir as homedir3 } from "os";
|
|
7394
|
+
import { readFileSync as readFileSync8, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
7395
|
+
import { isAbsolute, join as join9, resolve as resolve2 } from "path";
|
|
6308
7396
|
|
|
6309
7397
|
// src/repoScan.ts
|
|
6310
|
-
import { readdirSync as
|
|
6311
|
-
import { join as
|
|
7398
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
|
|
7399
|
+
import { join as join8 } from "path";
|
|
6312
7400
|
var SKIP = /* @__PURE__ */ new Set([
|
|
6313
7401
|
"node_modules",
|
|
6314
7402
|
"vendor",
|
|
@@ -6338,7 +7426,7 @@ function parseOriginFromGitConfig(text) {
|
|
|
6338
7426
|
}
|
|
6339
7427
|
function originOf(dir) {
|
|
6340
7428
|
try {
|
|
6341
|
-
return parseOriginFromGitConfig(readFileSync7(
|
|
7429
|
+
return parseOriginFromGitConfig(readFileSync7(join8(dir, ".git", "config"), "utf8"));
|
|
6342
7430
|
} catch {
|
|
6343
7431
|
return null;
|
|
6344
7432
|
}
|
|
@@ -6367,15 +7455,15 @@ function walk(dir, depth, maxDepth, found) {
|
|
|
6367
7455
|
}
|
|
6368
7456
|
let entries;
|
|
6369
7457
|
try {
|
|
6370
|
-
entries =
|
|
7458
|
+
entries = readdirSync5(dir);
|
|
6371
7459
|
} catch {
|
|
6372
7460
|
return;
|
|
6373
7461
|
}
|
|
6374
7462
|
for (const name of entries) {
|
|
6375
7463
|
if (name.startsWith(".") || SKIP.has(name)) continue;
|
|
6376
|
-
const child =
|
|
7464
|
+
const child = join8(dir, name);
|
|
6377
7465
|
try {
|
|
6378
|
-
if (!
|
|
7466
|
+
if (!statSync5(child).isDirectory()) continue;
|
|
6379
7467
|
} catch {
|
|
6380
7468
|
continue;
|
|
6381
7469
|
}
|
|
@@ -6395,13 +7483,13 @@ function defaultScanRoots(home, knownPaths = []) {
|
|
|
6395
7483
|
if (parent && parent !== p) add(parent);
|
|
6396
7484
|
}
|
|
6397
7485
|
for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
|
|
6398
|
-
add(
|
|
7486
|
+
add(join8(home, name));
|
|
6399
7487
|
}
|
|
6400
7488
|
return out;
|
|
6401
7489
|
}
|
|
6402
7490
|
|
|
6403
7491
|
// src/repoDiscovery.ts
|
|
6404
|
-
function resolveScanRoots(raw, home =
|
|
7492
|
+
function resolveScanRoots(raw, home = homedir3(), knownPaths = []) {
|
|
6405
7493
|
const v = (raw ?? "").trim();
|
|
6406
7494
|
if (!v) return defaultScanRoots(home, knownPaths);
|
|
6407
7495
|
if (["off", "none", "0"].includes(v.toLowerCase())) return [];
|
|
@@ -6409,7 +7497,7 @@ function resolveScanRoots(raw, home = homedir2(), knownPaths = []) {
|
|
|
6409
7497
|
for (const part of v.split(",")) {
|
|
6410
7498
|
const p = part.trim();
|
|
6411
7499
|
if (!p) continue;
|
|
6412
|
-
const abs = p === "~" ? home : p.startsWith("~/") ?
|
|
7500
|
+
const abs = p === "~" ? home : p.startsWith("~/") ? join9(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
|
|
6413
7501
|
if (!out.includes(abs)) out.push(abs);
|
|
6414
7502
|
}
|
|
6415
7503
|
return out;
|
|
@@ -6547,7 +7635,7 @@ function readRepoMapCache(path) {
|
|
|
6547
7635
|
function writeRepoMapCache(path, entries) {
|
|
6548
7636
|
try {
|
|
6549
7637
|
const tmp = `${path}.tmp`;
|
|
6550
|
-
|
|
7638
|
+
writeFileSync7(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
|
|
6551
7639
|
renameSync5(tmp, path);
|
|
6552
7640
|
} catch {
|
|
6553
7641
|
}
|
|
@@ -6555,17 +7643,17 @@ function writeRepoMapCache(path, entries) {
|
|
|
6555
7643
|
|
|
6556
7644
|
// src/hostIdentity.ts
|
|
6557
7645
|
import { hostname } from "os";
|
|
6558
|
-
import { existsSync as
|
|
6559
|
-
import { join as
|
|
7646
|
+
import { existsSync as existsSync7, readFileSync as readFileSync9, renameSync as renameSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
7647
|
+
import { join as join10 } from "path";
|
|
6560
7648
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6561
7649
|
function hostIdentityPath(home) {
|
|
6562
|
-
return
|
|
7650
|
+
return join10(home, "host.json");
|
|
6563
7651
|
}
|
|
6564
7652
|
function loadHostIdentity(deps) {
|
|
6565
|
-
const exists = deps.exists ??
|
|
7653
|
+
const exists = deps.exists ?? existsSync7;
|
|
6566
7654
|
const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
|
|
6567
7655
|
const writeFile2 = deps.writeFile ?? ((p, data) => {
|
|
6568
|
-
|
|
7656
|
+
writeFileSync8(`${p}.tmp`, data);
|
|
6569
7657
|
renameSync6(`${p}.tmp`, p);
|
|
6570
7658
|
});
|
|
6571
7659
|
const host = deps.hostname ?? hostname;
|
|
@@ -6748,87 +7836,15 @@ function buildReliefCostCandidates(cost, t, sustainMs, build) {
|
|
|
6748
7836
|
|
|
6749
7837
|
// src/launcher.ts
|
|
6750
7838
|
import { spawn } from "child_process";
|
|
6751
|
-
import { closeSync as closeSync2, existsSync as
|
|
7839
|
+
import { closeSync as closeSync2, existsSync as existsSync8, mkdirSync as mkdirSync6, openSync as openSync2, readdirSync as readdirSync7 } from "fs";
|
|
6752
7840
|
import { createRequire } from "module";
|
|
6753
|
-
import { homedir as
|
|
6754
|
-
import { dirname as dirname2, join as
|
|
7841
|
+
import { homedir as homedir4 } from "os";
|
|
7842
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
6755
7843
|
import { fileURLToPath } from "url";
|
|
6756
7844
|
|
|
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;
|
|
6805
|
-
}
|
|
6806
|
-
var warnedNoWindowsClaude = false;
|
|
6807
|
-
var cachedWindowsBin = null;
|
|
6808
|
-
var windowsScanDone = false;
|
|
6809
|
-
function resolveClaudeBin(opts = {}) {
|
|
6810
|
-
const env = opts.env ?? process.env;
|
|
6811
|
-
const platform = opts.platform ?? process.platform;
|
|
6812
|
-
const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
|
|
6813
|
-
if (explicit) return explicit;
|
|
6814
|
-
if (platform !== "win32") return CLAUDE_FALLBACK;
|
|
6815
|
-
if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
|
|
6816
|
-
windowsScanDone = true;
|
|
6817
|
-
const found = findWindowsClaude({ ...opts, env });
|
|
6818
|
-
cachedWindowsBin = found;
|
|
6819
|
-
if (found) return found;
|
|
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."
|
|
6824
|
-
);
|
|
6825
|
-
}
|
|
6826
|
-
return CLAUDE_FALLBACK;
|
|
6827
|
-
}
|
|
6828
|
-
|
|
6829
7845
|
// src/droneHome.ts
|
|
6830
|
-
import { lstatSync, mkdirSync as
|
|
6831
|
-
import { join as
|
|
7846
|
+
import { lstatSync, mkdirSync as mkdirSync5, readdirSync as readdirSync6, rmSync, statSync as statSync6, symlinkSync as symlinkSync2 } from "fs";
|
|
7847
|
+
import { join as join11, relative, sep } from "path";
|
|
6832
7848
|
var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
|
|
6833
7849
|
function droneHomeDeny(env = process.env) {
|
|
6834
7850
|
const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -6846,34 +7862,34 @@ function buildDroneHome(opts) {
|
|
|
6846
7862
|
const { realHome, droneHome } = opts;
|
|
6847
7863
|
const deny = opts.deny ?? droneHomeDeny();
|
|
6848
7864
|
const platform = opts.platform ?? process.platform;
|
|
6849
|
-
|
|
6850
|
-
const wanted = new Set(mirroredEntries(
|
|
7865
|
+
mkdirSync5(droneHome, { recursive: true, mode: 448 });
|
|
7866
|
+
const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
|
|
6851
7867
|
const rel = relative(realHome, droneHome);
|
|
6852
7868
|
if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
|
|
6853
|
-
for (const existing of
|
|
7869
|
+
for (const existing of readdirSync6(droneHome)) {
|
|
6854
7870
|
if (wanted.has(existing)) continue;
|
|
6855
|
-
const p =
|
|
7871
|
+
const p = join11(droneHome, existing);
|
|
6856
7872
|
try {
|
|
6857
7873
|
if (lstatSync(p).isSymbolicLink()) rmSync(p);
|
|
6858
7874
|
} catch {
|
|
6859
7875
|
}
|
|
6860
7876
|
}
|
|
6861
7877
|
for (const name of wanted) {
|
|
6862
|
-
const link =
|
|
7878
|
+
const link = join11(droneHome, name);
|
|
6863
7879
|
try {
|
|
6864
7880
|
lstatSync(link);
|
|
6865
7881
|
continue;
|
|
6866
7882
|
} catch {
|
|
6867
7883
|
}
|
|
6868
|
-
const target =
|
|
7884
|
+
const target = join11(realHome, name);
|
|
6869
7885
|
let isDir;
|
|
6870
7886
|
try {
|
|
6871
|
-
isDir =
|
|
7887
|
+
isDir = statSync6(target).isDirectory();
|
|
6872
7888
|
} catch {
|
|
6873
7889
|
continue;
|
|
6874
7890
|
}
|
|
6875
7891
|
try {
|
|
6876
|
-
|
|
7892
|
+
symlinkSync2(target, link, isDir ? "junction" : "file");
|
|
6877
7893
|
} catch (e) {
|
|
6878
7894
|
if (e.code !== "EEXIST") throw e;
|
|
6879
7895
|
}
|
|
@@ -6886,10 +7902,10 @@ var REAL_PATH_ENV_PINS = [
|
|
|
6886
7902
|
{ env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
|
|
6887
7903
|
];
|
|
6888
7904
|
function realPathEnvPins(realHome) {
|
|
6889
|
-
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env,
|
|
7905
|
+
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join11(realHome, ...segments)]));
|
|
6890
7906
|
}
|
|
6891
|
-
function
|
|
6892
|
-
|
|
7907
|
+
function dronePrivatePaths(realHome, swarmHome) {
|
|
7908
|
+
return [
|
|
6893
7909
|
// НЕ `${swarmHome}/**`. Worktree дрона живёт ВНУТРИ swarmHome
|
|
6894
7910
|
// (`<swarmHome>/worktrees.noindex/<repo>/<slug>`), и правило на весь каталог закрывало
|
|
6895
7911
|
// дрону его собственный рабочий каталог: Read/Write/Edit отвечали «File is in a directory
|
|
@@ -6910,11 +7926,6 @@ function droneDenyRules(realHome, swarmHome) {
|
|
|
6910
7926
|
`${realHome}/.claude/.credentials.json`,
|
|
6911
7927
|
`${realHome}/.npmrc`
|
|
6912
7928
|
];
|
|
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
7929
|
}
|
|
6919
7930
|
|
|
6920
7931
|
// src/testForks.ts
|
|
@@ -6946,6 +7957,7 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
|
|
|
6946
7957
|
}
|
|
6947
7958
|
|
|
6948
7959
|
// src/launcher.ts
|
|
7960
|
+
init_executors();
|
|
6949
7961
|
var requireFromHere = createRequire(import.meta.url);
|
|
6950
7962
|
function resolveTsxCli() {
|
|
6951
7963
|
try {
|
|
@@ -6956,29 +7968,12 @@ function resolveTsxCli() {
|
|
|
6956
7968
|
);
|
|
6957
7969
|
}
|
|
6958
7970
|
}
|
|
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
|
-
}
|
|
7971
|
+
var MCP_SERVER_NAME = "overmind";
|
|
6977
7972
|
function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
|
|
6978
7973
|
const env = { ...base3 };
|
|
6979
7974
|
const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
|
|
6980
7975
|
if (isolate && !base3.TURBO_CACHE_DIR) {
|
|
6981
|
-
env.TURBO_CACHE_DIR =
|
|
7976
|
+
env.TURBO_CACHE_DIR = join12(worktree, ".turbo", "cache");
|
|
6982
7977
|
}
|
|
6983
7978
|
Object.assign(env, droneTestEnv(droneCap, base3));
|
|
6984
7979
|
for (const key of DAEMON_STATE_ENV) delete env[key];
|
|
@@ -7006,49 +8001,20 @@ var DAEMON_STATE_ENV = [
|
|
|
7006
8001
|
"TASKLESS_TOKEN"
|
|
7007
8002
|
];
|
|
7008
8003
|
function bundledRoleDir(kind) {
|
|
7009
|
-
return
|
|
8004
|
+
return join12(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
|
|
7010
8005
|
}
|
|
7011
8006
|
function roleDir(worktree, kind) {
|
|
7012
|
-
return
|
|
8007
|
+
return join12(worktree, ".claude", "roles", kind);
|
|
7013
8008
|
}
|
|
7014
8009
|
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;
|
|
8010
|
+
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync8(dir));
|
|
7045
8011
|
}
|
|
7046
8012
|
function nextRunLogPath(runLogsDir, runId) {
|
|
7047
|
-
const dir =
|
|
7048
|
-
|
|
8013
|
+
const dir = join12(runLogsDir, String(runId));
|
|
8014
|
+
mkdirSync6(dir, { recursive: true });
|
|
7049
8015
|
let files = [];
|
|
7050
8016
|
try {
|
|
7051
|
-
files =
|
|
8017
|
+
files = readdirSync7(dir);
|
|
7052
8018
|
} catch {
|
|
7053
8019
|
}
|
|
7054
8020
|
let next = 0;
|
|
@@ -7056,7 +8022,7 @@ function nextRunLogPath(runLogsDir, runId) {
|
|
|
7056
8022
|
const m = /^(\d+)\.json/.exec(f);
|
|
7057
8023
|
if (m) next = Math.max(next, Number(m[1]) + 1);
|
|
7058
8024
|
}
|
|
7059
|
-
return
|
|
8025
|
+
return join12(dir, `${next}.json`);
|
|
7060
8026
|
}
|
|
7061
8027
|
function openNextRunLog(runLogsDir, runId) {
|
|
7062
8028
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
@@ -7091,57 +8057,63 @@ function makeLauncher(cfg) {
|
|
|
7091
8057
|
);
|
|
7092
8058
|
}
|
|
7093
8059
|
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
|
-
}
|
|
8060
|
+
const mcp = {
|
|
8061
|
+
name: MCP_SERVER_NAME,
|
|
8062
|
+
command: process.execPath,
|
|
8063
|
+
args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["--conditions=development", resolveTsxCli(), cfg.overmindEntry, "--drone", "--run", String(run.id)],
|
|
8064
|
+
// SWARM_DRONE_CHANNEL_URL/TOKEN (TASWM-295) ИЛИ SWARM_DB (откат) — см. channelEnv выше.
|
|
8065
|
+
// Канал: дрон говорит только с демоном, файл базы ему не отдаётся вовсе. Токен подписан
|
|
8066
|
+
// на СТАБИЛЬНЫЙ ключ прогона, поэтому переживает рестарт демона.
|
|
8067
|
+
// SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — доверенный
|
|
8068
|
+
// источник «чья это сессия» (read_task и отчёт о сдаче доверяют ему, а не строке,
|
|
8069
|
+
// найденной по номеру прогона).
|
|
8070
|
+
// SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID (TASWM-263) и SWARM_RUN_NAME
|
|
8071
|
+
// (TASWM-265): роль, связи и имя прогона той же спавн-фиксацией. Нужны РЕЖИМУ ОТКАТА —
|
|
8072
|
+
// ensureRunHydrated заводит ими стаб-строку в sqlite, если демон в client-режиме её там
|
|
8073
|
+
// не заводил. На канале прогон отдаёт демон целиком, и стаб не нужен.
|
|
8074
|
+
// SWARM_WORKSPACE_ID (TASWM-288): ВОРКСПЕЙС прогона — той же спавн-фиксацией: задачные
|
|
8075
|
+
// вызовы (read_task, отчёт, тулы Героя) дрон делает НЕ через канал, а прямо в Taskless,
|
|
8076
|
+
// и воркспейс им нужен верно сразу. Без него read_task/отчёт дрона
|
|
8077
|
+
// ушли бы с глобальным пином демона — ровно то, что чинит эта задача. Пусто
|
|
8078
|
+
// (прогон до фичи / sqlite-бэкенд) → ключ не кладём, дрон берёт глобальный пин.
|
|
8079
|
+
// SWARM_HOME (TASWM-289): дрон едет с ТЕНЕВЫМ HOME, в котором `.taskless-swarm`
|
|
8080
|
+
// нет вовсе, — а дрон-MCP из этого же дерева процессов читает oauth.json
|
|
8081
|
+
// (connectTaskless в runDroneServer), пульс овермайнда и workspace.json. Наследовать
|
|
8082
|
+
// ему нечего, поэтому реальный SWARM_HOME кладём явно. Дрон этот конфиг прочитать
|
|
8083
|
+
// может, но нового ему это не даёт: его собственный worktree лежит ВНУТРИ
|
|
8084
|
+
// SWARM_HOME, путь он и так знает (см. «чего этот модуль не делает» в droneHome.ts).
|
|
8085
|
+
env: {
|
|
8086
|
+
...channelEnv,
|
|
8087
|
+
...cfg.droneIsolation ? { SWARM_HOME: cfg.droneIsolation.swarmHome } : {},
|
|
8088
|
+
SWARM_TASK_SLUG: run.task_slug,
|
|
8089
|
+
SWARM_RUN_KIND: run.kind,
|
|
8090
|
+
SWARM_RUN_NAME: run.name,
|
|
8091
|
+
...run.workspace_id ? { SWARM_WORKSPACE_ID: run.workspace_id } : {},
|
|
8092
|
+
...run.reviews_run_id != null ? { SWARM_REVIEWS_RUN_ID: String(run.reviews_run_id) } : {},
|
|
8093
|
+
...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
|
|
7131
8094
|
}
|
|
7132
8095
|
};
|
|
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();
|
|
8096
|
+
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
|
|
8097
|
+
const realHome = cfg.droneIsolation?.realHome ?? homedir4();
|
|
7138
8098
|
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"))) {
|
|
8099
|
+
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join12(cfg.droneIsolation.swarmHome, "token"))) {
|
|
7141
8100
|
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 ${
|
|
8101
|
+
`[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
8102
|
);
|
|
7144
8103
|
}
|
|
8104
|
+
const executor = cfg.executor ?? resolveExecutor();
|
|
8105
|
+
const plan = executor.plan({
|
|
8106
|
+
worktree,
|
|
8107
|
+
prompt,
|
|
8108
|
+
continueSession: !!continueSession,
|
|
8109
|
+
model,
|
|
8110
|
+
mcp,
|
|
8111
|
+
skillDirs: roleDirs(worktree, run.kind),
|
|
8112
|
+
// Закрытые дрону пути имеют смысл только при теневом HOME: без него дрон и так
|
|
8113
|
+
// сидит в HOME демона, и запирать половину дверей — самообман.
|
|
8114
|
+
denyPaths: cfg.droneIsolation ? dronePrivatePaths(realHome, cfg.droneIsolation.swarmHome) : [],
|
|
8115
|
+
realHome
|
|
8116
|
+
});
|
|
7145
8117
|
let stdio = "ignore";
|
|
7146
8118
|
let outFd;
|
|
7147
8119
|
let errFd;
|
|
@@ -7151,111 +8123,79 @@ function makeLauncher(cfg) {
|
|
|
7151
8123
|
errFd = fds.err ?? void 0;
|
|
7152
8124
|
stdio = ["ignore", outFd, errFd ?? "ignore"];
|
|
7153
8125
|
}
|
|
7154
|
-
const child = spawn(cfg.
|
|
8126
|
+
const child = spawn(cfg.bin ?? plan.bin, plan.args, {
|
|
7155
8127
|
cwd: worktree,
|
|
7156
8128
|
detached: true,
|
|
7157
8129
|
stdio,
|
|
7158
8130
|
// Пер-worktree TURBO_CACHE_DIR (TASWM-232): дрон не отыгрывает кеш соседнего
|
|
7159
|
-
// worktree, проверки исполняются реально. Теневой HOME + чистка env от состояния
|
|
7160
|
-
// демона (TASWM-289). Доля тестовых форков от числа дронов на хосте (TASWM-246).
|
|
7161
|
-
// Остальной env демона наследуется, как раньше.
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
if (
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
}
|
|
7171
|
-
|
|
7172
|
-
// src/overmind.ts
|
|
7173
|
-
init_procCleanup();
|
|
7174
|
-
|
|
7175
|
-
// src/cost.ts
|
|
7176
|
-
import { readFileSync as readFileSync10, readdirSync as readdirSync6, renameSync as renameSync7 } from "fs";
|
|
7177
|
-
import { join as join10 } from "path";
|
|
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);
|
|
8131
|
+
// worktree, проверки исполняются реально. Теневой HOME + чистка env от состояния
|
|
8132
|
+
// демона (TASWM-289). Доля тестовых форков от числа дронов на хосте (TASWM-246).
|
|
8133
|
+
// Остальной env демона наследуется, как раньше. Добавки адаптера — последними:
|
|
8134
|
+
// они про его собственное состояние, и перебивать их ройным нечем.
|
|
8135
|
+
env: { ...droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null), ...plan.env }
|
|
8136
|
+
});
|
|
8137
|
+
if (outFd !== void 0) closeSync2(outFd);
|
|
8138
|
+
if (errFd !== void 0) closeSync2(errFd);
|
|
8139
|
+
child.unref();
|
|
8140
|
+
if (child.pid == null) throw new Error("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043D\u0435\u0442 pid)");
|
|
8141
|
+
return child.pid;
|
|
8142
|
+
};
|
|
7222
8143
|
}
|
|
7223
8144
|
|
|
8145
|
+
// src/overmind.ts
|
|
8146
|
+
init_procCleanup();
|
|
8147
|
+
|
|
7224
8148
|
// src/cost.ts
|
|
8149
|
+
init_executors();
|
|
8150
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync8, renameSync as renameSync7 } from "fs";
|
|
8151
|
+
import { join as join13 } from "path";
|
|
7225
8152
|
var UNPROCESSED_LOG_RE = /^\d+\.json$/;
|
|
7226
8153
|
function unprocessedLogFiles(dir) {
|
|
7227
8154
|
try {
|
|
7228
|
-
return
|
|
8155
|
+
return readdirSync8(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
|
|
7229
8156
|
} catch {
|
|
7230
8157
|
return [];
|
|
7231
8158
|
}
|
|
7232
8159
|
}
|
|
7233
8160
|
function accountRunCost(deps, run) {
|
|
7234
|
-
const dir =
|
|
8161
|
+
const dir = join13(deps.runLogsDir, String(run.id));
|
|
7235
8162
|
const files = unprocessedLogFiles(dir);
|
|
7236
8163
|
if (files.length === 0) return;
|
|
7237
8164
|
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
|
|
8165
|
+
const executor = deps.executor ?? resolveExecutor();
|
|
7238
8166
|
let added = 0;
|
|
8167
|
+
let tokensIn = 0;
|
|
8168
|
+
let tokensOut = 0;
|
|
8169
|
+
let durationMs = 0;
|
|
7239
8170
|
const processed = [];
|
|
7240
8171
|
for (const f of files) {
|
|
7241
|
-
const full =
|
|
8172
|
+
const full = join13(dir, f);
|
|
7242
8173
|
let raw = null;
|
|
7243
8174
|
try {
|
|
7244
8175
|
raw = readFileSync10(full, "utf8");
|
|
7245
8176
|
} catch {
|
|
7246
8177
|
continue;
|
|
7247
8178
|
}
|
|
7248
|
-
const result =
|
|
8179
|
+
const result = executor.parseResult(raw, model);
|
|
7249
8180
|
if (!result) {
|
|
7250
8181
|
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
8182
|
} else {
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
8183
|
+
if (result.warning) console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${result.warning}`);
|
|
8184
|
+
added += result.costUsd;
|
|
8185
|
+
tokensIn += result.tokensIn ?? 0;
|
|
8186
|
+
tokensOut += result.tokensOut ?? 0;
|
|
8187
|
+
durationMs += result.durationMs ?? 0;
|
|
7255
8188
|
}
|
|
7256
8189
|
processed.push(full);
|
|
7257
8190
|
}
|
|
7258
|
-
if (added > 0
|
|
8191
|
+
if (added > 0 || tokensIn > 0 || tokensOut > 0 || durationMs > 0) {
|
|
8192
|
+
deps.db.updateRun(run.id, {
|
|
8193
|
+
cost_usd: run.cost_usd + added,
|
|
8194
|
+
tokens_in: run.tokens_in + tokensIn,
|
|
8195
|
+
tokens_out: run.tokens_out + tokensOut,
|
|
8196
|
+
duration_ms: run.duration_ms + durationMs
|
|
8197
|
+
});
|
|
8198
|
+
}
|
|
7259
8199
|
for (const full of processed) {
|
|
7260
8200
|
try {
|
|
7261
8201
|
renameSync7(full, `${full}.done`);
|
|
@@ -7268,14 +8208,15 @@ function makeCostAccountant(deps) {
|
|
|
7268
8208
|
}
|
|
7269
8209
|
|
|
7270
8210
|
// src/runOutcome.ts
|
|
7271
|
-
|
|
7272
|
-
import {
|
|
8211
|
+
init_executors();
|
|
8212
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync11 } from "fs";
|
|
8213
|
+
import { join as join14 } from "path";
|
|
7273
8214
|
var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
|
|
7274
|
-
function classifyExit(runLogsDir, runId) {
|
|
7275
|
-
const dir =
|
|
8215
|
+
function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
|
|
8216
|
+
const dir = join14(runLogsDir, String(runId));
|
|
7276
8217
|
let files;
|
|
7277
8218
|
try {
|
|
7278
|
-
files =
|
|
8219
|
+
files = readdirSync9(dir);
|
|
7279
8220
|
} catch {
|
|
7280
8221
|
return "unknown";
|
|
7281
8222
|
}
|
|
@@ -7289,14 +8230,15 @@ function classifyExit(runLogsDir, runId) {
|
|
|
7289
8230
|
if (!latest) return "unknown";
|
|
7290
8231
|
let raw;
|
|
7291
8232
|
try {
|
|
7292
|
-
raw = readFileSync11(
|
|
8233
|
+
raw = readFileSync11(join14(dir, latest.name), "utf8");
|
|
7293
8234
|
} catch {
|
|
7294
8235
|
return "unknown";
|
|
7295
8236
|
}
|
|
7296
|
-
return
|
|
8237
|
+
return executor.parseResult(raw, null) ? "graceful" : "crash";
|
|
7297
8238
|
}
|
|
7298
|
-
function makeExitClassifier(runLogsDir) {
|
|
7299
|
-
|
|
8239
|
+
function makeExitClassifier(runLogsDir, executor) {
|
|
8240
|
+
const exec = executor ?? resolveExecutor();
|
|
8241
|
+
return (run) => classifyExit(runLogsDir, run.id, exec);
|
|
7300
8242
|
}
|
|
7301
8243
|
|
|
7302
8244
|
// src/orchestrator.ts
|
|
@@ -7304,20 +8246,90 @@ init_src();
|
|
|
7304
8246
|
init_git();
|
|
7305
8247
|
|
|
7306
8248
|
// src/deps.ts
|
|
7307
|
-
import { existsSync as
|
|
7308
|
-
import { join as
|
|
8249
|
+
import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync12 } from "fs";
|
|
8250
|
+
import { join as join17 } from "path";
|
|
7309
8251
|
import { execa } from "execa";
|
|
8252
|
+
|
|
8253
|
+
// src/prepareLock.ts
|
|
8254
|
+
import { createHash } from "crypto";
|
|
8255
|
+
import { closeSync as closeSync3, openSync as openSync3, rmSync as rmSync2, statSync as statSync8, writeSync } from "fs";
|
|
8256
|
+
import { join as join16 } from "path";
|
|
8257
|
+
import { tmpdir } from "os";
|
|
8258
|
+
var WAIT_TIMEOUT_MS = 5 * 6e4;
|
|
8259
|
+
var POLL_MS = 300;
|
|
8260
|
+
var STALE_MS = 20 * 6e4;
|
|
8261
|
+
function machineLockPath(key, dir = tmpdir()) {
|
|
8262
|
+
return join16(dir, `swarm-prepare-${createHash("sha1").update(key).digest("hex").slice(0, 16)}.lock`);
|
|
8263
|
+
}
|
|
8264
|
+
function defaultTryCreate(path, content) {
|
|
8265
|
+
try {
|
|
8266
|
+
const fd = openSync3(path, "wx");
|
|
8267
|
+
try {
|
|
8268
|
+
writeSync(fd, content);
|
|
8269
|
+
} finally {
|
|
8270
|
+
closeSync3(fd);
|
|
8271
|
+
}
|
|
8272
|
+
return true;
|
|
8273
|
+
} catch {
|
|
8274
|
+
return false;
|
|
8275
|
+
}
|
|
8276
|
+
}
|
|
8277
|
+
function defaultAgeMs(path) {
|
|
8278
|
+
try {
|
|
8279
|
+
return Date.now() - statSync8(path).mtimeMs;
|
|
8280
|
+
} catch {
|
|
8281
|
+
return null;
|
|
8282
|
+
}
|
|
8283
|
+
}
|
|
8284
|
+
async function withMachineLock(key, fn, io = {}) {
|
|
8285
|
+
const tryCreate = io.tryCreate ?? defaultTryCreate;
|
|
8286
|
+
const remove = io.remove ?? ((p) => {
|
|
8287
|
+
try {
|
|
8288
|
+
rmSync2(p, { force: true });
|
|
8289
|
+
} catch {
|
|
8290
|
+
}
|
|
8291
|
+
});
|
|
8292
|
+
const ageMs2 = io.ageMs ?? defaultAgeMs;
|
|
8293
|
+
const wait = io.wait ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
8294
|
+
const now = io.now ?? Date.now;
|
|
8295
|
+
const path = machineLockPath(key, io.dir ?? tmpdir());
|
|
8296
|
+
const deadline = now() + WAIT_TIMEOUT_MS;
|
|
8297
|
+
let locked = false;
|
|
8298
|
+
for (; ; ) {
|
|
8299
|
+
if (tryCreate(path, `${process.pid}
|
|
8300
|
+
`)) {
|
|
8301
|
+
locked = true;
|
|
8302
|
+
break;
|
|
8303
|
+
}
|
|
8304
|
+
const age = ageMs2(path);
|
|
8305
|
+
if (age !== null && age > STALE_MS) remove(path);
|
|
8306
|
+
if (now() >= deadline) break;
|
|
8307
|
+
await wait(POLL_MS);
|
|
8308
|
+
}
|
|
8309
|
+
try {
|
|
8310
|
+
return await fn(locked);
|
|
8311
|
+
} finally {
|
|
8312
|
+
if (locked) remove(path);
|
|
8313
|
+
}
|
|
8314
|
+
}
|
|
8315
|
+
|
|
8316
|
+
// src/deps.ts
|
|
7310
8317
|
var PREPARE_TIMEOUT_MS = 15 * 6e4;
|
|
7311
8318
|
var MAX_PLAN_COMMANDS = 12;
|
|
7312
8319
|
async function runCommand(cmd, worktree) {
|
|
7313
|
-
const
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
8320
|
+
const exec = async () => {
|
|
8321
|
+
const res = await execa(cmd.run, {
|
|
8322
|
+
cwd: cmd.cwd ?? worktree,
|
|
8323
|
+
shell: true,
|
|
8324
|
+
// команда — строка целиком (у настройки/CI иначе никак), поэтому именно шелл
|
|
8325
|
+
reject: false,
|
|
8326
|
+
timeout: PREPARE_TIMEOUT_MS,
|
|
8327
|
+
...cmd.env ? { env: cmd.env } : {}
|
|
8328
|
+
// execa мержит с окружением процесса, не заменяет его
|
|
8329
|
+
});
|
|
8330
|
+
return res.exitCode === 0;
|
|
8331
|
+
};
|
|
8332
|
+
return cmd.lock ? withMachineLock(cmd.lock, exec) : exec();
|
|
7321
8333
|
}
|
|
7322
8334
|
function safeReadText(p) {
|
|
7323
8335
|
try {
|
|
@@ -7328,14 +8340,14 @@ function safeReadText(p) {
|
|
|
7328
8340
|
}
|
|
7329
8341
|
function safeListDirs(p) {
|
|
7330
8342
|
try {
|
|
7331
|
-
return
|
|
8343
|
+
return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
7332
8344
|
} catch {
|
|
7333
8345
|
return [];
|
|
7334
8346
|
}
|
|
7335
8347
|
}
|
|
7336
8348
|
function safeListFiles(p) {
|
|
7337
8349
|
try {
|
|
7338
|
-
return
|
|
8350
|
+
return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
|
|
7339
8351
|
} catch {
|
|
7340
8352
|
return [];
|
|
7341
8353
|
}
|
|
@@ -7361,21 +8373,21 @@ function overridePlan(override) {
|
|
|
7361
8373
|
};
|
|
7362
8374
|
}
|
|
7363
8375
|
function detectPackageManager(worktree, io = {}) {
|
|
7364
|
-
const exists = io.exists ??
|
|
8376
|
+
const exists = io.exists ?? existsSync10;
|
|
7365
8377
|
const readText = io.readText ?? safeReadText;
|
|
7366
|
-
const hasPkgJson = exists(
|
|
8378
|
+
const hasPkgJson = exists(join17(worktree, "package.json"));
|
|
7367
8379
|
if (!hasPkgJson) {
|
|
7368
|
-
if (exists(
|
|
8380
|
+
if (exists(join17(worktree, "deno.json")) || exists(join17(worktree, "deno.jsonc")) || exists(join17(worktree, "deno.lock"))) return "deno";
|
|
7369
8381
|
return null;
|
|
7370
8382
|
}
|
|
7371
|
-
const pkg = readJson(readText,
|
|
8383
|
+
const pkg = readJson(readText, join17(worktree, "package.json"));
|
|
7372
8384
|
const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
|
|
7373
8385
|
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(
|
|
8386
|
+
if (exists(join17(worktree, "bun.lock")) || exists(join17(worktree, "bun.lockb"))) return "bun";
|
|
8387
|
+
if (exists(join17(worktree, "pnpm-lock.yaml"))) return "pnpm";
|
|
8388
|
+
if (exists(join17(worktree, "yarn.lock"))) return "yarn";
|
|
8389
|
+
if (exists(join17(worktree, "package-lock.json"))) return "npm";
|
|
8390
|
+
if (exists(join17(worktree, "deno.lock"))) return "deno";
|
|
7379
8391
|
return "npm";
|
|
7380
8392
|
}
|
|
7381
8393
|
function jsInstallCommand(pm) {
|
|
@@ -7428,7 +8440,7 @@ var ECOSYSTEM_MARKERS = [
|
|
|
7428
8440
|
// конфигурация — дело сборки, не подготовки
|
|
7429
8441
|
];
|
|
7430
8442
|
function detectPlan(worktree, io) {
|
|
7431
|
-
const exists = io.exists ??
|
|
8443
|
+
const exists = io.exists ?? existsSync10;
|
|
7432
8444
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
7433
8445
|
const pm = detectPackageManager(worktree, io);
|
|
7434
8446
|
if (pm) {
|
|
@@ -7436,20 +8448,21 @@ function detectPlan(worktree, io) {
|
|
|
7436
8448
|
// node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
|
|
7437
8449
|
// У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
|
|
7438
8450
|
// заново, но он дешёвый: тянет из того же общего кэша.
|
|
7439
|
-
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists:
|
|
8451
|
+
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join17(worktree, "node_modules"), source: "detect" }
|
|
7440
8452
|
];
|
|
7441
8453
|
for (const target of workspaceBuildTargets(worktree, io)) {
|
|
7442
8454
|
commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
|
|
7443
8455
|
}
|
|
8456
|
+
commands.push(...migrateCommands(worktree, pm, io));
|
|
7444
8457
|
return { source: "detect", ecosystem: `node/${pm}`, commands };
|
|
7445
8458
|
}
|
|
7446
8459
|
for (const marker of ECOSYSTEM_MARKERS) {
|
|
7447
|
-
const hit = marker.files.some((f) => exists(
|
|
8460
|
+
const hit = marker.files.some((f) => exists(join17(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
|
|
7448
8461
|
if (!hit) continue;
|
|
7449
8462
|
let ecosystem = marker.ecosystem;
|
|
7450
8463
|
let command = marker.command;
|
|
7451
8464
|
for (const refine of marker.refine ?? []) {
|
|
7452
|
-
if (!exists(
|
|
8465
|
+
if (!exists(join17(worktree, refine.file))) continue;
|
|
7453
8466
|
ecosystem = refine.ecosystem;
|
|
7454
8467
|
command = refine.command;
|
|
7455
8468
|
break;
|
|
@@ -7506,12 +8519,12 @@ var CI_INSTALL_PATTERNS = [
|
|
|
7506
8519
|
function ciWorkflowPlan(worktree, io) {
|
|
7507
8520
|
const readText = io.readText ?? safeReadText;
|
|
7508
8521
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
7509
|
-
const dir =
|
|
8522
|
+
const dir = join17(worktree, ".github", "workflows");
|
|
7510
8523
|
const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
|
|
7511
8524
|
const seen = /* @__PURE__ */ new Set();
|
|
7512
8525
|
const commands = [];
|
|
7513
8526
|
for (const name of files.sort()) {
|
|
7514
|
-
const yaml = readText(
|
|
8527
|
+
const yaml = readText(join17(dir, name));
|
|
7515
8528
|
if (yaml === null) continue;
|
|
7516
8529
|
for (const raw of workflowRunLines(yaml)) {
|
|
7517
8530
|
const line = raw.replace(/\s+/g, " ").trim();
|
|
@@ -7526,7 +8539,7 @@ function ciWorkflowPlan(worktree, io) {
|
|
|
7526
8539
|
return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
|
|
7527
8540
|
}
|
|
7528
8541
|
function workspaceGlobs(worktree, readText) {
|
|
7529
|
-
const yaml = readText(
|
|
8542
|
+
const yaml = readText(join17(worktree, "pnpm-workspace.yaml"));
|
|
7530
8543
|
if (yaml !== null) {
|
|
7531
8544
|
const globs = [];
|
|
7532
8545
|
let inPackages = false;
|
|
@@ -7546,7 +8559,7 @@ function workspaceGlobs(worktree, readText) {
|
|
|
7546
8559
|
}
|
|
7547
8560
|
if (globs.length) return globs;
|
|
7548
8561
|
}
|
|
7549
|
-
const pkg = readJson(readText,
|
|
8562
|
+
const pkg = readJson(readText, join17(worktree, "package.json"));
|
|
7550
8563
|
const ws = pkg?.workspaces;
|
|
7551
8564
|
const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
|
|
7552
8565
|
return list.filter((g) => typeof g === "string");
|
|
@@ -7557,14 +8570,14 @@ function workspaceDirs(worktree, globs, io) {
|
|
|
7557
8570
|
if (glob.startsWith("!")) continue;
|
|
7558
8571
|
const star = glob.indexOf("*");
|
|
7559
8572
|
if (star === -1) {
|
|
7560
|
-
const dir =
|
|
7561
|
-
if (io.exists(
|
|
8573
|
+
const dir = join17(worktree, glob);
|
|
8574
|
+
if (io.exists(join17(dir, "package.json"))) dirs.add(dir);
|
|
7562
8575
|
continue;
|
|
7563
8576
|
}
|
|
7564
|
-
const base3 =
|
|
8577
|
+
const base3 = join17(worktree, glob.slice(0, star).replace(/\/$/, ""));
|
|
7565
8578
|
for (const name of io.listDirs(base3)) {
|
|
7566
|
-
const dir =
|
|
7567
|
-
if (io.exists(
|
|
8579
|
+
const dir = join17(base3, name);
|
|
8580
|
+
if (io.exists(join17(dir, "package.json"))) dirs.add(dir);
|
|
7568
8581
|
}
|
|
7569
8582
|
}
|
|
7570
8583
|
return [...dirs];
|
|
@@ -7585,18 +8598,18 @@ function entryPaths(pkg) {
|
|
|
7585
8598
|
return out;
|
|
7586
8599
|
}
|
|
7587
8600
|
function workspaceBuildTargets(worktree, io = {}) {
|
|
7588
|
-
const exists = io.exists ??
|
|
8601
|
+
const exists = io.exists ?? existsSync10;
|
|
7589
8602
|
const readText = io.readText ?? safeReadText;
|
|
7590
8603
|
const listDirs = io.listDirs ?? safeListDirs;
|
|
7591
8604
|
const globs = workspaceGlobs(worktree, readText);
|
|
7592
8605
|
if (!globs.length) return [];
|
|
7593
8606
|
const pkgs = [];
|
|
7594
8607
|
for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
|
|
7595
|
-
const pkg = readJson(readText,
|
|
8608
|
+
const pkg = readJson(readText, join17(dir, "package.json"));
|
|
7596
8609
|
if (!pkg || typeof pkg.name !== "string") continue;
|
|
7597
8610
|
const scripts = pkg.scripts ?? {};
|
|
7598
8611
|
const entries = entryPaths(pkg);
|
|
7599
|
-
const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(
|
|
8612
|
+
const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join17(dir, e)));
|
|
7600
8613
|
const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
|
|
7601
8614
|
pkgs.push({ name: pkg.name, dir, deps, needsBuild });
|
|
7602
8615
|
}
|
|
@@ -7618,13 +8631,66 @@ function workspaceBuildTargets(worktree, io = {}) {
|
|
|
7618
8631
|
for (const p of targets.values()) visit(p);
|
|
7619
8632
|
return ordered;
|
|
7620
8633
|
}
|
|
8634
|
+
var LOCAL_DB_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]", "host.docker.internal"]);
|
|
8635
|
+
function devDatabaseUrl(worktree, io = {}) {
|
|
8636
|
+
const readText = io.readText ?? safeReadText;
|
|
8637
|
+
const fromEnv = (io.env ?? process.env).DATABASE_URL;
|
|
8638
|
+
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
8639
|
+
for (const file of [".env", ".env.example"]) {
|
|
8640
|
+
const raw = readText(join17(worktree, file));
|
|
8641
|
+
if (raw === null) continue;
|
|
8642
|
+
const m = /^\s*(?:export\s+)?DATABASE_URL\s*=\s*(.*)$/m.exec(raw);
|
|
8643
|
+
const value = (m?.[1] ?? "").trim().replace(/^['"]|['"]$/g, "");
|
|
8644
|
+
if (value) return value;
|
|
8645
|
+
}
|
|
8646
|
+
return null;
|
|
8647
|
+
}
|
|
8648
|
+
function isLocalDatabase(url) {
|
|
8649
|
+
try {
|
|
8650
|
+
return LOCAL_DB_HOSTS.has(new URL(url).hostname);
|
|
8651
|
+
} catch {
|
|
8652
|
+
return false;
|
|
8653
|
+
}
|
|
8654
|
+
}
|
|
8655
|
+
function prismaPackages(worktree, io) {
|
|
8656
|
+
const exists = io.exists ?? existsSync10;
|
|
8657
|
+
const readText = io.readText ?? safeReadText;
|
|
8658
|
+
const listDirs = io.listDirs ?? safeListDirs;
|
|
8659
|
+
const dirs = [worktree, ...workspaceDirs(worktree, workspaceGlobs(worktree, readText), { exists, listDirs })];
|
|
8660
|
+
return dirs.filter((dir) => exists(join17(dir, "prisma", "schema.prisma")) && listDirs(join17(dir, "prisma", "migrations")).length > 0);
|
|
8661
|
+
}
|
|
8662
|
+
function jsExecBin(pm, command) {
|
|
8663
|
+
switch (pm) {
|
|
8664
|
+
case "npm":
|
|
8665
|
+
return `npm exec -- ${command}`;
|
|
8666
|
+
case "bun":
|
|
8667
|
+
return `bun x ${command}`;
|
|
8668
|
+
default:
|
|
8669
|
+
return `${pm} exec ${command}`;
|
|
8670
|
+
}
|
|
8671
|
+
}
|
|
8672
|
+
function migrateCommands(worktree, pm, io) {
|
|
8673
|
+
if (pm === "deno") return [];
|
|
8674
|
+
const url = devDatabaseUrl(worktree, io);
|
|
8675
|
+
if (!url || !isLocalDatabase(url)) return [];
|
|
8676
|
+
return prismaPackages(worktree, io).map((dir) => ({
|
|
8677
|
+
label: `\u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0411\u0414${dir === worktree ? "" : ` (${dir.slice(worktree.length + 1)})`}`,
|
|
8678
|
+
run: jsExecBin(pm, "prisma migrate deploy"),
|
|
8679
|
+
cwd: dir,
|
|
8680
|
+
env: { DATABASE_URL: url },
|
|
8681
|
+
// Ключ мьютекса — сама база: дроны разных репозиториев с разными базами друг друга не ждут.
|
|
8682
|
+
lock: `migrate:${url}`,
|
|
8683
|
+
optional: true,
|
|
8684
|
+
source: "detect"
|
|
8685
|
+
}));
|
|
8686
|
+
}
|
|
7621
8687
|
function planPrepare(worktree, io = {}) {
|
|
7622
8688
|
const plan = overridePlan(io.override) ?? detectPlan(worktree, io) ?? ciWorkflowPlan(worktree, io);
|
|
7623
8689
|
if (!plan) return { source: "none", ecosystem: null, commands: [] };
|
|
7624
8690
|
return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
|
|
7625
8691
|
}
|
|
7626
8692
|
async function ensureWorktreeDeps(worktree, io = {}) {
|
|
7627
|
-
const exists = io.exists ??
|
|
8693
|
+
const exists = io.exists ?? existsSync10;
|
|
7628
8694
|
const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
|
|
7629
8695
|
const approve = io.approve ?? (() => true);
|
|
7630
8696
|
const plan = planPrepare(worktree, io);
|
|
@@ -7635,7 +8701,8 @@ async function ensureWorktreeDeps(worktree, io = {}) {
|
|
|
7635
8701
|
ran: [],
|
|
7636
8702
|
failed: [],
|
|
7637
8703
|
skipped: [],
|
|
7638
|
-
blocked: []
|
|
8704
|
+
blocked: [],
|
|
8705
|
+
warned: []
|
|
7639
8706
|
};
|
|
7640
8707
|
for (const cmd of plan.commands) {
|
|
7641
8708
|
if (cmd.skipIfExists && exists(cmd.skipIfExists)) {
|
|
@@ -7650,6 +8717,8 @@ async function ensureWorktreeDeps(worktree, io = {}) {
|
|
|
7650
8717
|
if (await exec(cmd)) {
|
|
7651
8718
|
report.ran.push(cmd.label);
|
|
7652
8719
|
report.outcome = "prepared";
|
|
8720
|
+
} else if (cmd.optional) {
|
|
8721
|
+
report.warned.push(cmd.label);
|
|
7653
8722
|
} else {
|
|
7654
8723
|
report.failed.push(cmd.label);
|
|
7655
8724
|
report.outcome = "failed";
|
|
@@ -7666,13 +8735,17 @@ function depsEvents(report) {
|
|
|
7666
8735
|
events.push(
|
|
7667
8736
|
`\u26A0 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 worktree${where} \u0423\u041F\u0410\u041B\u0410 \u043D\u0430 \xAB${report.failed.join(", ")}\xBB \u2014 \u0434\u0440\u043E\u043D \u0441\u0442\u0430\u0440\u0442\u0443\u0435\u0442 \u0411\u0415\u0417 \u0433\u043E\u0442\u043E\u0432\u043E\u0433\u043E \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F: pre-commit \u0445\u0443\u043A \u043C\u043E\u0436\u0435\u0442 \u043C\u043E\u043B\u0447\u0430 \u043D\u0435 \u0441\u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C, typecheck/\u0442\u0435\u0441\u0442\u044B \u0431\u0443\u0434\u0443\u0442 \u043A\u0440\u0430\u0441\u043D\u044B\u043C\u0438 \u043D\u0435 \u043F\u043E \u0434\u0435\u043B\u0443 (TASWM-312)`
|
|
7668
8737
|
);
|
|
8738
|
+
if (report.warned.length)
|
|
8739
|
+
events.push(
|
|
8740
|
+
`\u26A0 \u0448\u0430\u0433 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0438 \xAB${report.warned.join(", ")}\xBB \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B \u2014 worktree \u0433\u043E\u0442\u043E\u0432, \u043D\u043E \u0411\u0410\u0417\u0410 \u041C\u041E\u0413\u041B\u0410 \u041E\u0421\u0422\u0410\u0422\u042C\u0421\u042F \u041D\u0410 \u0421\u0422\u0410\u0420\u041E\u0419 \u0421\u0425\u0415\u041C\u0415 (\u0447\u0430\u0449\u0435 \u0432\u0441\u0435\u0433\u043E \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0430\u044F \u0431\u0430\u0437\u0430 \u043F\u0440\u043E\u0441\u0442\u043E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u0430) \u2014 TASWM-362`
|
|
8741
|
+
);
|
|
7669
8742
|
if (report.blocked.length)
|
|
7670
8743
|
events.push(`\u26A0 \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0438 \u043D\u0435 \u043F\u0443\u0449\u0435\u043D\u0430 \u0433\u0435\u0439\u0442\u043E\u043C \u0434\u043E\u0432\u0435\u0440\u0438\u044F: ${report.blocked.join(", ")} \u2014 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u043D\u0435 \u0433\u043E\u0442\u043E\u0432\u0438\u043B\u0438 (TASWM-312)`);
|
|
7671
8744
|
if (report.outcome === "not-detected")
|
|
7672
8745
|
events.push(
|
|
7673
8746
|
`\u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D\u0430: \u043F\u0440\u043E\u0435\u043A\u0442 \u043D\u0435 \u043E\u043F\u043E\u0437\u043D\u0430\u043D (\u043D\u0435\u0442 \u0437\u043D\u0430\u043A\u043E\u043C\u044B\u0445 \u043C\u0430\u0440\u043A\u0435\u0440\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432, \u0432 .github/workflows \u043A\u043E\u043C\u0430\u043D\u0434 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438 \u043D\u0435 \u043D\u0430\u0448\u043B\u0438) \u2014 \u0434\u0435\u043C\u043E\u043D \u041D\u0418\u0427\u0415\u0413\u041E \u043D\u0435 \u0441\u0442\u0430\u0432\u0438\u043B; \u0437\u0430\u0434\u0430\u0442\u044C \u043A\u043E\u043C\u0430\u043D\u0434\u0443 \u043C\u043E\u0436\u043D\u043E \u0432 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u0445 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (swarmConfig.prepare) \u2014 TASWM-312`
|
|
7674
8747
|
);
|
|
7675
|
-
if (report.outcome === "nothing-to-do" && !report.skipped.length) {
|
|
8748
|
+
if (report.outcome === "nothing-to-do" && !report.skipped.length && !report.warned.length) {
|
|
7676
8749
|
if (report.source === "project-config") events.push(`\u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0430 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u043E\u0439 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (swarmConfig.prepare \u043F\u0443\u0441\u0442) \u2014 TASWM-312`);
|
|
7677
8750
|
else if (report.ecosystem) events.push(`\u043F\u0440\u043E\u0435\u043A\u0442${where}: \u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043F\u0435\u0440\u0435\u0434 \u0441\u0442\u0430\u0440\u0442\u043E\u043C \u043D\u0435\u0447\u0435\u0433\u043E, \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F \u2014 TASWM-312`);
|
|
7678
8751
|
}
|
|
@@ -7691,6 +8764,9 @@ function depsPromptNotice(report) {
|
|
|
7691
8764
|
if (problems.length) {
|
|
7692
8765
|
return `\u26A0 \u041E\u041A\u0420\u0423\u0416\u0415\u041D\u0418\u0415 WORKTREE \u041D\u0415 \u041F\u041E\u0414\u0413\u041E\u0422\u041E\u0412\u041B\u0415\u041D\u041E (TASWM-312): ${problems.join("; ")}. \u041F\u043E\u043A\u0430 \u044D\u0442\u043E \u0442\u0430\u043A, \u0413\u0415\u0419\u0422 \u041C\u041E\u0416\u0415\u0422 \u041D\u0415 \u0420\u0410\u0411\u041E\u0422\u0410\u0422\u042C: pre-commit \u0445\u0443\u043A \u043C\u043E\u043B\u0447\u0430 \u043D\u0435 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0441\u044F (\u043F\u0435\u0440\u0432\u044B\u0439 \u043A\u043E\u043C\u043C\u0438\u0442 \u0443\u0439\u0434\u0451\u0442 \u0431\u0435\u0437 \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A), \u0430 typecheck/\u0442\u0435\u0441\u0442\u044B \u0431\u0443\u0434\u0443\u0442 \u043A\u0440\u0430\u0441\u043D\u044B\u043C\u0438 \u043D\u0435 \u043F\u043E \u0434\u0435\u043B\u0443. \u041F\u0435\u0440\u0432\u044B\u043C \u0434\u0435\u043B\u043E\u043C \u043F\u043E\u0447\u0438\u043D\u0438 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u0441\u0430\u043C (\u043F\u043E\u0441\u0442\u0430\u0432\u044C \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u0442\u0430\u043A, \u043A\u0430\u043A \u044D\u0442\u043E \u0434\u0435\u043B\u0430\u0435\u0442 README/CI \u043F\u0440\u043E\u0435\u043A\u0442\u0430) \u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0442\u043E\u043C \u0432\u0435\u0440\u044C \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430\u043C \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A; \u043D\u0435 \u0441\u0434\u0430\u0432\u0430\u0439 \u0440\u0430\u0431\u043E\u0442\u0443, \u043E\u0431\u044A\u044F\u0441\u043D\u044F\u044F \u043F\u0430\u0434\u0435\u043D\u0438\u044F \xAB\u0441\u043B\u043E\u043C\u0430\u043D\u043D\u044B\u043C \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435\u043C\xBB, \u043D\u0435 \u043F\u043E\u043F\u0440\u043E\u0431\u043E\u0432\u0430\u0432 \u044D\u0442\u043E.`;
|
|
7693
8766
|
}
|
|
8767
|
+
if (report.warned.length) {
|
|
8768
|
+
return `\u2139 \u0411\u0410\u0417\u0410 \u041C\u041E\u0413\u041B\u0410 \u041E\u0421\u0422\u0410\u0422\u042C\u0421\u042F \u041D\u0410 \u0421\u0422\u0410\u0420\u041E\u0419 \u0421\u0425\u0415\u041C\u0415 (TASWM-362): \u0448\u0430\u0433 \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0438 \xAB${report.warned.join(", ")}\xBB \u043D\u0435 \u043F\u0440\u043E\u0448\u0451\u043B \u2014 \u043E\u0431\u044B\u0447\u043D\u043E \u043F\u043E\u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0430\u044F \u0431\u0430\u0437\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u0430. \u0421\u0430\u043C worktree \u0433\u043E\u0442\u043E\u0432, typecheck \u0438 \u044E\u043D\u0438\u0442-\u0442\u0435\u0441\u0442\u044B \u0434\u043E\u0441\u0442\u043E\u0432\u0435\u0440\u043D\u044B. \u0415\u0441\u043B\u0438 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0448\u044C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0438 \u0443\u0432\u0438\u0434\u0438\u0448\u044C \u043E\u0448\u0438\u0431\u043A\u0443 \u043F\u0440\u043E \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0449\u0443\u044E \u043A\u043E\u043B\u043E\u043D\u043A\u0443/\u0442\u0430\u0431\u043B\u0438\u0446\u0443 \u2014 \u044D\u0442\u043E \u043E\u043D\u0430: \u043F\u043E\u0434\u043D\u0438\u043C\u0438 \u0431\u0430\u0437\u0443 \u0438 \u043D\u0430\u043A\u0430\u0442\u0438 \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0442\u0430\u043A, \u043A\u0430\u043A \u044D\u0442\u043E \u0434\u0435\u043B\u0430\u0435\u0442 dev-\u0441\u043A\u0440\u0438\u043F\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.`;
|
|
8769
|
+
}
|
|
7694
8770
|
if (report.outcome === "not-detected") {
|
|
7695
8771
|
return `\u2139 \u0414\u0415\u041C\u041E\u041D \u041D\u0415 \u0413\u041E\u0422\u041E\u0412\u0418\u041B \u041E\u041A\u0420\u0423\u0416\u0415\u041D\u0418\u0415 (TASWM-312): \u043F\u0440\u043E\u0435\u043A\u0442 \u043D\u0435 \u043E\u043F\u043E\u0437\u043D\u0430\u043D \u043F\u043E \u043C\u0430\u0440\u043A\u0435\u0440\u043D\u044B\u043C \u0444\u0430\u0439\u043B\u0430\u043C, \u043A\u043E\u043C\u0430\u043D\u0434 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0438 \u0432 .github/workflows \u0442\u043E\u0436\u0435 \u043D\u0435\u0442. \u0417\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438, \u0435\u0441\u043B\u0438 \u043E\u043D\u0438 \u043D\u0443\u0436\u043D\u044B, \u043D\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u044B \u2014 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 README/CI \u0438 \u043F\u043E\u0441\u0442\u0430\u0432\u044C \u0438\u0445 \u0441\u0430\u043C \u041F\u0415\u0420\u0415\u0414 \u0442\u0435\u043C, \u043A\u0430\u043A \u0432\u0435\u0440\u0438\u0442\u044C typecheck/\u0442\u0435\u0441\u0442\u0430\u043C.`;
|
|
7696
8772
|
}
|
|
@@ -7715,7 +8791,9 @@ var BaseBranchMissingError = class extends Error {
|
|
|
7715
8791
|
branch;
|
|
7716
8792
|
repo;
|
|
7717
8793
|
};
|
|
7718
|
-
var
|
|
8794
|
+
var SpawnDeferredError = class extends Error {
|
|
8795
|
+
};
|
|
8796
|
+
var AreaBusyError = class extends SpawnDeferredError {
|
|
7719
8797
|
constructor(area) {
|
|
7720
8798
|
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
8799
|
this.area = area;
|
|
@@ -7723,6 +8801,14 @@ var AreaBusyError = class extends Error {
|
|
|
7723
8801
|
}
|
|
7724
8802
|
area;
|
|
7725
8803
|
};
|
|
8804
|
+
var FileOverlapError = class extends SpawnDeferredError {
|
|
8805
|
+
constructor(message, blockedByRunId) {
|
|
8806
|
+
super(message);
|
|
8807
|
+
this.blockedByRunId = blockedByRunId;
|
|
8808
|
+
this.name = "FileOverlapError";
|
|
8809
|
+
}
|
|
8810
|
+
blockedByRunId;
|
|
8811
|
+
};
|
|
7726
8812
|
function healTaskSlug(db, run, task) {
|
|
7727
8813
|
const slug = task?.slug;
|
|
7728
8814
|
if (!slug || slug === run.task_slug || !isTaskSlug(slug) || isTaskSlug(run.task_slug)) return run;
|
|
@@ -7732,7 +8818,9 @@ function healTaskSlug(db, run, task) {
|
|
|
7732
8818
|
async function spawnDrone(deps, run) {
|
|
7733
8819
|
const repo = normalizeRepo(run.repo || deps.repo || "");
|
|
7734
8820
|
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
|
-
|
|
8821
|
+
const conflict = deps.conflicts ? await deps.conflicts(run, repo) : { kind: "unknown" };
|
|
8822
|
+
if (conflict.kind === "wait") throw new FileOverlapError(conflict.message, conflict.blockedBy.id);
|
|
8823
|
+
if (conflict.kind === "unknown" && run.area && deps.db.runningAreas(repo).includes(run.area)) {
|
|
7736
8824
|
throw new AreaBusyError(run.area);
|
|
7737
8825
|
}
|
|
7738
8826
|
const branch = run.task_slug;
|
|
@@ -7783,13 +8871,13 @@ function isGithubPrUrl(url) {
|
|
|
7783
8871
|
return GH_PR_RE.test(url.trim());
|
|
7784
8872
|
}
|
|
7785
8873
|
function softStatus() {
|
|
7786
|
-
return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [] };
|
|
8874
|
+
return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [], headBranch: null };
|
|
7787
8875
|
}
|
|
7788
8876
|
async function prStatus(prUrl, run = realRunner) {
|
|
7789
8877
|
if (!isGithubPrUrl(prUrl)) return softStatus();
|
|
7790
8878
|
let out;
|
|
7791
8879
|
try {
|
|
7792
|
-
out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files"]);
|
|
8880
|
+
out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files,headRefName"]);
|
|
7793
8881
|
} catch {
|
|
7794
8882
|
return softStatus();
|
|
7795
8883
|
}
|
|
@@ -7810,9 +8898,20 @@ async function prStatus(prUrl, run = realRunner) {
|
|
|
7810
8898
|
// не смержен, открыт и GitHub уверенно говорит CONFLICTING
|
|
7811
8899
|
conflicting: !merged && state === "OPEN" && mergeable === "CONFLICTING",
|
|
7812
8900
|
isDraft: j.isDraft === true,
|
|
7813
|
-
files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : []
|
|
8901
|
+
files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : [],
|
|
8902
|
+
headBranch: typeof j.headRefName === "string" && j.headRefName ? j.headRefName : null
|
|
7814
8903
|
};
|
|
7815
8904
|
}
|
|
8905
|
+
async function prHeadBranch(prUrl, run = realRunner) {
|
|
8906
|
+
if (!isGithubPrUrl(prUrl)) return null;
|
|
8907
|
+
try {
|
|
8908
|
+
const out = await run(["pr", "view", prUrl, "--json", "headRefName"]);
|
|
8909
|
+
const j = JSON.parse(out);
|
|
8910
|
+
return typeof j.headRefName === "string" && j.headRefName ? j.headRefName : null;
|
|
8911
|
+
} catch {
|
|
8912
|
+
return null;
|
|
8913
|
+
}
|
|
8914
|
+
}
|
|
7816
8915
|
async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
|
|
7817
8916
|
if (!branch || !repoDir) return [];
|
|
7818
8917
|
try {
|
|
@@ -8020,6 +9119,22 @@ function reviewTransitionPatch(run, prUrl) {
|
|
|
8020
9119
|
}
|
|
8021
9120
|
return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
|
|
8022
9121
|
}
|
|
9122
|
+
function branchFromPrPatch(currentBranch, headBranch) {
|
|
9123
|
+
if (!headBranch || headBranch === currentBranch) return null;
|
|
9124
|
+
return {
|
|
9125
|
+
patch: { branch: headBranch },
|
|
9126
|
+
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)`
|
|
9127
|
+
};
|
|
9128
|
+
}
|
|
9129
|
+
function prLiveness(st) {
|
|
9130
|
+
if (!st) return "unknown";
|
|
9131
|
+
if (st.merged) return "merged";
|
|
9132
|
+
const state = (st.state ?? "").toUpperCase();
|
|
9133
|
+
if (state === "MERGED") return "merged";
|
|
9134
|
+
if (state === "CLOSED") return "closed";
|
|
9135
|
+
if (state === "UNKNOWN") return "unknown";
|
|
9136
|
+
return "open";
|
|
9137
|
+
}
|
|
8023
9138
|
function applyReviewTransition(db, run, prUrl) {
|
|
8024
9139
|
const { patch, ...t } = reviewTransitionPatch(run, prUrl);
|
|
8025
9140
|
db.updateRun(run.id, patch);
|
|
@@ -8028,14 +9143,14 @@ function applyReviewTransition(db, run, prUrl) {
|
|
|
8028
9143
|
|
|
8029
9144
|
// src/droneServer.ts
|
|
8030
9145
|
init_src();
|
|
8031
|
-
function freezeInstruction(
|
|
9146
|
+
function freezeInstruction(branch, reason = "\u0435\u0433\u043E \u043F\u0443\u043B\u044C\u0441 (heartbeat) \u0443\u0441\u0442\u0430\u0440\u0435\u043B") {
|
|
8032
9147
|
return [
|
|
8033
9148
|
`\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
9149
|
"\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
9150
|
"\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
9151
|
"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
9152
|
" \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 ${
|
|
9153
|
+
`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
9154
|
"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
9155
|
"\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
9156
|
"\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 +9183,12 @@ async function buildDroneServer(store, opts = {}) {
|
|
|
8068
9183
|
const slugForInstruction = (fallback) => opts.expectedTaskSlug ?? fallback ?? "<slug>";
|
|
8069
9184
|
const freeze = async (withdraw = false) => {
|
|
8070
9185
|
let slug;
|
|
9186
|
+
let branch;
|
|
8071
9187
|
try {
|
|
8072
9188
|
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
9189
|
const run = await store.myRun();
|
|
8074
9190
|
slug = run?.task_slug;
|
|
9191
|
+
branch = !opts.expectedTaskSlug || opts.expectedTaskSlug === run?.task_slug ? run?.branch : null;
|
|
8075
9192
|
if (run && run.status !== "paused") {
|
|
8076
9193
|
await store.updateRun(null, { status: "paused" });
|
|
8077
9194
|
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 +9196,7 @@ async function buildDroneServer(store, opts = {}) {
|
|
|
8079
9196
|
} catch (e) {
|
|
8080
9197
|
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
9198
|
}
|
|
8082
|
-
return { content: [{ type: "text", text: freezeInstruction(slugForInstruction(slug), opts.deadReason) }] };
|
|
9199
|
+
return { content: [{ type: "text", text: freezeInstruction(branch || slugForInstruction(slug), opts.deadReason) }] };
|
|
8083
9200
|
};
|
|
8084
9201
|
const guard = (fn) => async (a) => {
|
|
8085
9202
|
try {
|
|
@@ -8103,7 +9220,7 @@ async function buildDroneServer(store, opts = {}) {
|
|
|
8103
9220
|
);
|
|
8104
9221
|
server.tool(
|
|
8105
9222
|
"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).",
|
|
9223
|
+
"\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
9224
|
{},
|
|
8108
9225
|
async () => {
|
|
8109
9226
|
if (!opts.taskless) {
|
|
@@ -8316,9 +9433,12 @@ ${report}${shots.warning}`,
|
|
|
8316
9433
|
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
9434
|
}
|
|
8318
9435
|
};
|
|
9436
|
+
const headBranch = opts.prHeadBranch ? await opts.prHeadBranch(pr_url).catch(() => null) : null;
|
|
9437
|
+
const branchFix = branchFromPrPatch(run?.branch, headBranch);
|
|
9438
|
+
if (branchFix) await store.addEvent(null, "step", branchFix.event);
|
|
8319
9439
|
if (run) {
|
|
8320
9440
|
const t = reviewTransitionPatch(run, pr_url);
|
|
8321
|
-
await store.updateRun(null, t.patch);
|
|
9441
|
+
await store.updateRun(null, { ...t.patch, ...branchFix?.patch ?? {} });
|
|
8322
9442
|
await store.addEvent(null, "pr_opened", pr_url);
|
|
8323
9443
|
await postReport(reportSlug(run.task_slug), t.cycle ?? run.review_cycle ?? 0);
|
|
8324
9444
|
if (t.isFixIteration) {
|
|
@@ -8331,7 +9451,7 @@ ${report}${shots.warning}`,
|
|
|
8331
9451
|
}
|
|
8332
9452
|
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
9453
|
}
|
|
8334
|
-
await store.updateRun(null, { status: "review", pr_url });
|
|
9454
|
+
await store.updateRun(null, { status: "review", pr_url, ...branchFix?.patch ?? {} });
|
|
8335
9455
|
await store.addEvent(null, "pr_opened", pr_url);
|
|
8336
9456
|
const fallbackSlug = reportSlug(void 0);
|
|
8337
9457
|
if (fallbackSlug) await postReport(fallbackSlug, void 0);
|
|
@@ -8496,7 +9616,7 @@ async function runDroneServer(runId) {
|
|
|
8496
9616
|
);
|
|
8497
9617
|
const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
|
|
8498
9618
|
const expectedKind = toRunKind(process.env.SWARM_RUN_KIND);
|
|
8499
|
-
const common = { taskless, runKind: expectedKind, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
|
|
9619
|
+
const common = { taskless, runKind: expectedKind, prHeadBranch, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
|
|
8500
9620
|
const channelUrl = process.env.SWARM_DRONE_CHANNEL_URL;
|
|
8501
9621
|
const channelToken = process.env.SWARM_DRONE_CHANNEL_TOKEN;
|
|
8502
9622
|
if (channelUrl && channelToken) {
|
|
@@ -8525,7 +9645,7 @@ async function runDroneServer(runId) {
|
|
|
8525
9645
|
|
|
8526
9646
|
// src/droneBridge.ts
|
|
8527
9647
|
init_src();
|
|
8528
|
-
import { createHash as
|
|
9648
|
+
import { createHash as createHash3 } from "crypto";
|
|
8529
9649
|
|
|
8530
9650
|
// src/anomalies.ts
|
|
8531
9651
|
init_src();
|
|
@@ -8589,12 +9709,15 @@ var AnomalyTracker = class {
|
|
|
8589
9709
|
for (const [k, v] of Object.entries(restored.since)) this.since.set(k, v);
|
|
8590
9710
|
for (const [k, v] of Object.entries(restored.lastFiredAt)) this.lastFiredAt.set(k, v);
|
|
8591
9711
|
for (const [k, v] of Object.entries(restored.fires)) this.fires.set(k, v);
|
|
9712
|
+
for (const k of restored.resolved ?? []) this.resolved.add(k);
|
|
8592
9713
|
}
|
|
8593
9714
|
}
|
|
8594
9715
|
renotifyCooldownMs;
|
|
8595
9716
|
since = /* @__PURE__ */ new Map();
|
|
8596
9717
|
lastFiredAt = /* @__PURE__ */ new Map();
|
|
8597
9718
|
fires = /* @__PURE__ */ new Map();
|
|
9719
|
+
/** TAS-485: очередь «уже звонили, а условие ушло» — к гашению в инбоксе. */
|
|
9720
|
+
resolved = /* @__PURE__ */ new Set();
|
|
8598
9721
|
maxRepeats;
|
|
8599
9722
|
io;
|
|
8600
9723
|
/** Кандидаты, готовые к сигналу (отлежали minDurationMs, не в кулдауне) — НИЧЕГО не коммитит. */
|
|
@@ -8615,6 +9738,7 @@ var AnomalyTracker = class {
|
|
|
8615
9738
|
for (const map of [this.since, this.lastFiredAt, this.fires]) {
|
|
8616
9739
|
for (const k of [...map.keys()]) {
|
|
8617
9740
|
if (!seen.has(k)) {
|
|
9741
|
+
if (map === this.lastFiredAt) this.resolved.add(k);
|
|
8618
9742
|
map.delete(k);
|
|
8619
9743
|
pruned = true;
|
|
8620
9744
|
}
|
|
@@ -8627,8 +9751,25 @@ var AnomalyTracker = class {
|
|
|
8627
9751
|
commit(key, now) {
|
|
8628
9752
|
this.lastFiredAt.set(key, now);
|
|
8629
9753
|
this.fires.set(key, (this.fires.get(key) ?? 0) + 1);
|
|
9754
|
+
this.resolved.delete(key);
|
|
8630
9755
|
this.persist();
|
|
8631
9756
|
}
|
|
9757
|
+
/**
|
|
9758
|
+
* TAS-485: ключи, чьё условие ушло ПОСЛЕ того, как о нём звонили, — их записи
|
|
9759
|
+
* в инбоксе пора погасить. Список только ЧИТАЕТСЯ: вычёркивает доставленное
|
|
9760
|
+
* вызывающий (`forgetResolved`) и только после успеха — та же дисциплина, что
|
|
9761
|
+
* у eligible/commit. Проглоти мы сбой доставки как успех, тревога о том, чего
|
|
9762
|
+
* нет, осталась бы висеть навсегда и молча.
|
|
9763
|
+
*/
|
|
9764
|
+
pendingResolved() {
|
|
9765
|
+
return [...this.resolved];
|
|
9766
|
+
}
|
|
9767
|
+
/** Гашение по этим ключам доставлено — забываем их. */
|
|
9768
|
+
forgetResolved(keys) {
|
|
9769
|
+
let changed = false;
|
|
9770
|
+
for (const k of keys) changed = this.resolved.delete(k) || changed;
|
|
9771
|
+
if (changed) this.persist();
|
|
9772
|
+
}
|
|
8632
9773
|
/**
|
|
8633
9774
|
* Следующий сигнал по этому ключу — ПОСЛЕДНИЙ разрешённый в эпизоде: вызывающий меняет
|
|
8634
9775
|
* формулировку (LAST_REMINDER_SUFFIX), чтобы затихание не выглядело как «рой забыл».
|
|
@@ -8644,7 +9785,8 @@ var AnomalyTracker = class {
|
|
|
8644
9785
|
this.io.save({
|
|
8645
9786
|
since: Object.fromEntries(this.since),
|
|
8646
9787
|
lastFiredAt: Object.fromEntries(this.lastFiredAt),
|
|
8647
|
-
fires: Object.fromEntries(this.fires)
|
|
9788
|
+
fires: Object.fromEntries(this.fires),
|
|
9789
|
+
resolved: [...this.resolved]
|
|
8648
9790
|
});
|
|
8649
9791
|
} catch (e) {
|
|
8650
9792
|
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 +9840,9 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8698
9840
|
const changedAt = sqliteMs(run.updated_at);
|
|
8699
9841
|
return changedAt >= q.from && (q.to == null || changedAt <= q.to);
|
|
8700
9842
|
};
|
|
9843
|
+
const closedBeforeStart = (run) => opts.daemonStartedAt != null && sqliteMs(run.updated_at) < opts.daemonStartedAt;
|
|
8701
9844
|
const recentFailed = allRuns.filter(
|
|
8702
|
-
(r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r)
|
|
9845
|
+
(r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r)
|
|
8703
9846
|
);
|
|
8704
9847
|
if (recentFailed.length >= thresholds.failedSpikeCount) {
|
|
8705
9848
|
candidates.push({
|
|
@@ -8736,6 +9879,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8736
9879
|
if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
|
|
8737
9880
|
if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
|
|
8738
9881
|
if (killedByOperator(run)) continue;
|
|
9882
|
+
if (closedBeforeStart(run)) continue;
|
|
8739
9883
|
const repeat = isRepeatDown(run);
|
|
8740
9884
|
candidates.push({
|
|
8741
9885
|
key: `run_stalled:${run.id}`,
|
|
@@ -8756,6 +9900,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8756
9900
|
if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
|
|
8757
9901
|
if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
|
|
8758
9902
|
if (killedByOperator(run)) continue;
|
|
9903
|
+
if (closedBeforeStart(run)) continue;
|
|
8759
9904
|
const repeat = isRepeatDown(run);
|
|
8760
9905
|
candidates.push({
|
|
8761
9906
|
key: `run_failed:${run.id}`,
|
|
@@ -8895,7 +10040,7 @@ function questionKey(runId, text) {
|
|
|
8895
10040
|
return `${runId} ${text.trim().replace(/\s+/g, " ")}`;
|
|
8896
10041
|
}
|
|
8897
10042
|
function noteKey(runId, text) {
|
|
8898
|
-
return `note:${runId}:${
|
|
10043
|
+
return `note:${runId}:${createHash3("sha1").update(questionKey(runId, text)).digest("hex").slice(0, 16)}`;
|
|
8899
10044
|
}
|
|
8900
10045
|
async function syncDroneChannel(opts) {
|
|
8901
10046
|
const result = { imported: [], answered: [], notes: [] };
|
|
@@ -9175,22 +10320,22 @@ function makeExclusiveReader(opts) {
|
|
|
9175
10320
|
}
|
|
9176
10321
|
|
|
9177
10322
|
// src/envGate.ts
|
|
9178
|
-
import { existsSync as
|
|
9179
|
-
import { join as
|
|
10323
|
+
import { existsSync as existsSync13 } from "fs";
|
|
10324
|
+
import { join as join24 } from "path";
|
|
9180
10325
|
import { createConnection } from "net";
|
|
9181
10326
|
import { execa as execa2 } from "execa";
|
|
9182
10327
|
|
|
9183
10328
|
// src/platform.ts
|
|
9184
|
-
import { existsSync as
|
|
9185
|
-
import { delimiter, join as
|
|
10329
|
+
import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
|
|
10330
|
+
import { delimiter, join as join23 } from "path";
|
|
9186
10331
|
function resolveCommandPath(name, env = process.env, platform = process.platform) {
|
|
9187
|
-
if (name.includes("/") || name.includes("\\")) return
|
|
10332
|
+
if (name.includes("/") || name.includes("\\")) return existsSync12(name) ? name : null;
|
|
9188
10333
|
const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
|
|
9189
10334
|
const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
|
|
9190
10335
|
for (const dir of dirs) {
|
|
9191
10336
|
for (const ext of exts) {
|
|
9192
|
-
const candidate =
|
|
9193
|
-
if (
|
|
10337
|
+
const candidate = join23(dir, name + ext);
|
|
10338
|
+
if (existsSync12(candidate)) return candidate;
|
|
9194
10339
|
}
|
|
9195
10340
|
}
|
|
9196
10341
|
return null;
|
|
@@ -9198,6 +10343,31 @@ function resolveCommandPath(name, env = process.env, platform = process.platform
|
|
|
9198
10343
|
function hasCommandInPath(name, env = process.env, platform = process.platform) {
|
|
9199
10344
|
return resolveCommandPath(name, env, platform) !== null;
|
|
9200
10345
|
}
|
|
10346
|
+
function detectGlobalPm(entry, io = {}) {
|
|
10347
|
+
const p = (entry ?? "").replace(/\\/g, "/");
|
|
10348
|
+
if (!p) return "unknown";
|
|
10349
|
+
if (p.endsWith(".ts") || /\/packages\/swarm-daemon\/(src|dist)\//.test(p)) return "sources";
|
|
10350
|
+
if (/\/\.bun\/install\/global\//.test(p) || /\/\.bun\/bin\//.test(p)) return "bun";
|
|
10351
|
+
if (/\/pnpm\//.test(p) || /\/\.pnpm-global\//.test(p) || /\/Library\/pnpm\//.test(p)) return "pnpm";
|
|
10352
|
+
if (/\/\.yarn\//.test(p) || /\/Yarn\/Data\/global\//.test(p) || /\/yarn\/global\//.test(p)) return "yarn";
|
|
10353
|
+
if (/\/node_modules\//.test(p)) return "npm";
|
|
10354
|
+
const env = io.env ?? process.env;
|
|
10355
|
+
const platform = io.platform ?? process.platform;
|
|
10356
|
+
const has = io.hasCommand ?? ((n) => hasCommandInPath(n, env, platform));
|
|
10357
|
+
return has("npm") ? "npm" : "unknown";
|
|
10358
|
+
}
|
|
10359
|
+
function globalInstallCommand(pm, spec) {
|
|
10360
|
+
switch (pm) {
|
|
10361
|
+
case "pnpm":
|
|
10362
|
+
return `pnpm add -g ${spec}`;
|
|
10363
|
+
case "bun":
|
|
10364
|
+
return `bun add -g ${spec}`;
|
|
10365
|
+
case "yarn":
|
|
10366
|
+
return `yarn global add ${spec}`;
|
|
10367
|
+
default:
|
|
10368
|
+
return `npm i -g ${spec}`;
|
|
10369
|
+
}
|
|
10370
|
+
}
|
|
9201
10371
|
|
|
9202
10372
|
// src/envGate.ts
|
|
9203
10373
|
var PROBE_TIMEOUT_MS = 2500;
|
|
@@ -9248,8 +10418,8 @@ var COMPOSE_MARKERS = [
|
|
|
9248
10418
|
"compose.yaml"
|
|
9249
10419
|
];
|
|
9250
10420
|
function detectRequirements(repo, io = {}) {
|
|
9251
|
-
const exists = io.exists ??
|
|
9252
|
-
const marker = COMPOSE_MARKERS.find((f) => exists(
|
|
10421
|
+
const exists = io.exists ?? existsSync13;
|
|
10422
|
+
const marker = COMPOSE_MARKERS.find((f) => exists(join24(repo, f)));
|
|
9253
10423
|
return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
|
|
9254
10424
|
}
|
|
9255
10425
|
function resolveRequirements(repo, declared, io = {}) {
|
|
@@ -9385,8 +10555,8 @@ import { basename as basename3 } from "path";
|
|
|
9385
10555
|
import { serve as serve2 } from "@hono/node-server";
|
|
9386
10556
|
|
|
9387
10557
|
// src/diff.ts
|
|
9388
|
-
import { statSync as
|
|
9389
|
-
import { join as
|
|
10558
|
+
import { statSync as statSync9 } from "fs";
|
|
10559
|
+
import { join as join25 } from "path";
|
|
9390
10560
|
import { $ as $4 } from "execa";
|
|
9391
10561
|
var MAX_FILES = 200;
|
|
9392
10562
|
var MAX_LINES_PER_FILE = 1500;
|
|
@@ -9529,7 +10699,7 @@ async function resolveBase(wt, baseBranch) {
|
|
|
9529
10699
|
}
|
|
9530
10700
|
function mtime(path) {
|
|
9531
10701
|
try {
|
|
9532
|
-
return
|
|
10702
|
+
return statSync9(path).mtimeMs;
|
|
9533
10703
|
} catch {
|
|
9534
10704
|
return 0;
|
|
9535
10705
|
}
|
|
@@ -9554,7 +10724,7 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
9554
10724
|
files.push(pf);
|
|
9555
10725
|
}
|
|
9556
10726
|
}
|
|
9557
|
-
files.sort((a, b) => mtime(
|
|
10727
|
+
files.sort((a, b) => mtime(join25(worktree, b.path)) - mtime(join25(worktree, a.path)));
|
|
9558
10728
|
const truncatedFiles = files.length > MAX_FILES;
|
|
9559
10729
|
if (truncatedFiles) files = files.slice(0, MAX_FILES);
|
|
9560
10730
|
const added = files.reduce((s, f) => s + f.added, 0);
|
|
@@ -9569,11 +10739,25 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
9569
10739
|
...truncatedFiles ? { truncated: true } : {}
|
|
9570
10740
|
};
|
|
9571
10741
|
}
|
|
10742
|
+
async function changedPaths(worktree, opts = {}) {
|
|
10743
|
+
if (!worktree) return [];
|
|
10744
|
+
try {
|
|
10745
|
+
const isWt = (await $4({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
|
|
10746
|
+
if (isWt !== "true") return [];
|
|
10747
|
+
const from = await resolveBase(worktree, opts.baseBranch) ?? "HEAD";
|
|
10748
|
+
const tracked = (await $4({ reject: false })`git -C ${worktree} diff --name-only --find-renames ${from} --`).stdout;
|
|
10749
|
+
const untracked = (await $4({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
|
|
10750
|
+
const all = [...tracked.split("\n"), ...untracked.split("\0")].map((s) => s.trim()).filter(Boolean);
|
|
10751
|
+
return [...new Set(all)].sort();
|
|
10752
|
+
} catch {
|
|
10753
|
+
return [];
|
|
10754
|
+
}
|
|
10755
|
+
}
|
|
9572
10756
|
|
|
9573
10757
|
// package.json
|
|
9574
10758
|
var package_default = {
|
|
9575
10759
|
name: "@taskless-app/swarm",
|
|
9576
|
-
version: "0.
|
|
10760
|
+
version: "0.6.1",
|
|
9577
10761
|
type: "module",
|
|
9578
10762
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
9579
10763
|
keywords: [
|
|
@@ -9622,7 +10806,8 @@ var package_default = {
|
|
|
9622
10806
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
9623
10807
|
execa: "^10.0.0",
|
|
9624
10808
|
zod: "3.25.76"
|
|
9625
|
-
}
|
|
10809
|
+
},
|
|
10810
|
+
gitHead: "415ad928e18430894ae24ac2e4267073f5c6da14"
|
|
9626
10811
|
};
|
|
9627
10812
|
|
|
9628
10813
|
// src/version.ts
|
|
@@ -9690,6 +10875,10 @@ function roySnapshot(db, now = Date.now(), overmindLog = [], paused = false, dae
|
|
|
9690
10875
|
stopReason: r.stop_reason,
|
|
9691
10876
|
reason,
|
|
9692
10877
|
updated: r.updated_at,
|
|
10878
|
+
startedAt: r.created_at,
|
|
10879
|
+
// TASWM-351: возраст прогона на карточке
|
|
10880
|
+
costUsd: r.cost_usd || 0,
|
|
10881
|
+
// TASWM-351: то же поле, что уходит в прод (swarm_run_upsert{costUsd})
|
|
9693
10882
|
step,
|
|
9694
10883
|
hasDecision: withQ.has(r.id),
|
|
9695
10884
|
rescued: evs.some((e) => e.kind === "rescued"),
|
|
@@ -11197,47 +12386,10 @@ function closeWebServer(server) {
|
|
|
11197
12386
|
});
|
|
11198
12387
|
}
|
|
11199
12388
|
|
|
11200
|
-
// src/daemonVersion.ts
|
|
11201
|
-
init_git();
|
|
11202
|
-
import { dirname as dirname4 } from "path";
|
|
11203
|
-
import { $ as $5 } from "execa";
|
|
11204
|
-
async function captureDaemonStart(entryFile, now = () => /* @__PURE__ */ new Date()) {
|
|
11205
|
-
try {
|
|
11206
|
-
const repo = await repoRoot(dirname4(entryFile));
|
|
11207
|
-
const sha = (await $5`git -C ${repo} rev-parse HEAD`).stdout.trim();
|
|
11208
|
-
if (!sha) return null;
|
|
11209
|
-
return { repo, startedAt: now().toISOString(), startCommit: sha };
|
|
11210
|
-
} catch {
|
|
11211
|
-
return null;
|
|
11212
|
-
}
|
|
11213
|
-
}
|
|
11214
|
-
var DAEMON_CODE_PATHS = ["packages/swarm-daemon", "packages/swarm-core"];
|
|
11215
|
-
async function checkDaemonVersion(start, now = () => /* @__PURE__ */ new Date()) {
|
|
11216
|
-
const base3 = { startedAt: start.startedAt, startCommit: start.startCommit, checkedAt: now().toISOString() };
|
|
11217
|
-
try {
|
|
11218
|
-
await $5({ reject: false })`git -C ${start.repo} fetch --quiet origin`;
|
|
11219
|
-
const mainRef = await remoteMainRef(start.repo);
|
|
11220
|
-
const mainCommit = (await $5`git -C ${start.repo} rev-parse ${mainRef}`).stdout.trim();
|
|
11221
|
-
if (!mainCommit || mainCommit === start.startCommit) {
|
|
11222
|
-
return { ...base3, mainCommit: mainCommit || null, stale: false, staleFiles: [] };
|
|
11223
|
-
}
|
|
11224
|
-
const ancestor = await $5({ reject: false })`git -C ${start.repo} merge-base --is-ancestor ${start.startCommit} ${mainCommit}`;
|
|
11225
|
-
if (ancestor.exitCode !== 0) {
|
|
11226
|
-
return { ...base3, mainCommit, stale: false, staleFiles: [] };
|
|
11227
|
-
}
|
|
11228
|
-
const diff = await $5({ reject: false })(
|
|
11229
|
-
"git",
|
|
11230
|
-
["-C", start.repo, "diff", "--name-only", start.startCommit, mainCommit, "--", ...DAEMON_CODE_PATHS]
|
|
11231
|
-
);
|
|
11232
|
-
const staleFiles = diff.stdout.trim() ? diff.stdout.trim().split("\n") : [];
|
|
11233
|
-
return { ...base3, mainCommit, stale: staleFiles.length > 0, staleFiles: staleFiles.slice(0, 20) };
|
|
11234
|
-
} catch {
|
|
11235
|
-
return { ...base3, mainCommit: null, stale: false, staleFiles: [] };
|
|
11236
|
-
}
|
|
11237
|
-
}
|
|
11238
|
-
|
|
11239
12389
|
// src/release.ts
|
|
11240
|
-
|
|
12390
|
+
init_src();
|
|
12391
|
+
init_claudeBin();
|
|
12392
|
+
var SWARM_PACKAGE = SWARM_PACKAGE_NAME;
|
|
11241
12393
|
var LATEST_TIMEOUT_MS = 4e3;
|
|
11242
12394
|
async function fetchLatestVersion(opts = {}) {
|
|
11243
12395
|
const fetchFn = opts.fetchFn ?? fetch;
|
|
@@ -11268,12 +12420,30 @@ function outdatedLine(current, latest, updateCommand = "swarm update") {
|
|
|
11268
12420
|
return `\u26A0 \u0432\u0435\u0440\u0441\u0438\u044F \u0440\u043E\u044F \u0443\u0441\u0442\u0430\u0440\u0435\u043B\u0430: \u0443 \u0442\u0435\u0431\u044F v${current}, \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest} \u2014 \u043E\u0431\u043D\u043E\u0432\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: ${updateCommand}`;
|
|
11269
12421
|
}
|
|
11270
12422
|
|
|
12423
|
+
// src/daemonVersion.ts
|
|
12424
|
+
function captureDaemonStart(now = () => /* @__PURE__ */ new Date()) {
|
|
12425
|
+
return { startedAt: now().toISOString(), version: PACKAGE_VERSION };
|
|
12426
|
+
}
|
|
12427
|
+
var DAEMON_CODE_PATHS = ["packages/swarm-daemon", "packages/swarm-core"];
|
|
12428
|
+
async function checkDaemonVersion(start, opts = {}) {
|
|
12429
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
12430
|
+
const fetchLatest = opts.latest ?? (() => fetchLatestVersion());
|
|
12431
|
+
const latestVersion = await fetchLatest().catch(() => null);
|
|
12432
|
+
return {
|
|
12433
|
+
startedAt: start.startedAt,
|
|
12434
|
+
version: start.version,
|
|
12435
|
+
latestVersion,
|
|
12436
|
+
stale: isOutdated(start.version, latestVersion),
|
|
12437
|
+
checkedAt: now().toISOString()
|
|
12438
|
+
};
|
|
12439
|
+
}
|
|
12440
|
+
|
|
11271
12441
|
// src/hostCommands.ts
|
|
11272
12442
|
init_src();
|
|
11273
12443
|
import { readFileSync as readFileSync18, renameSync as renameSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
11274
|
-
import { join as
|
|
12444
|
+
import { join as join26 } from "path";
|
|
11275
12445
|
function hostCommandStatePath(home) {
|
|
11276
|
-
return
|
|
12446
|
+
return join26(home, "host-command.json");
|
|
11277
12447
|
}
|
|
11278
12448
|
function readState(path) {
|
|
11279
12449
|
try {
|
|
@@ -11383,6 +12553,28 @@ async function executeOne(cmd, opts) {
|
|
|
11383
12553
|
await ack(cmd.id, "done", { result: `\u043F\u0440\u043E\u0433\u043E\u043D ${slug} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D` });
|
|
11384
12554
|
return false;
|
|
11385
12555
|
}
|
|
12556
|
+
// TASWM-351: «перезапустить с того же места». Отказ побудки (дрон уже работает,
|
|
12557
|
+
// продолжать нечем) уезжает как `rejected` с ЕЁ формулировкой — повторять такую
|
|
12558
|
+
// команду бессмысленно, и это ровно та разница между rejected и failed, ради
|
|
12559
|
+
// которой они разделены (см. HOST_COMMAND_STATUSES).
|
|
12560
|
+
case "wake_run": {
|
|
12561
|
+
const slug = cmd.runSlug?.trim();
|
|
12562
|
+
if (!slug) {
|
|
12563
|
+
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" });
|
|
12564
|
+
return false;
|
|
12565
|
+
}
|
|
12566
|
+
const woken = await effects.wakeRun(slug);
|
|
12567
|
+
if (!woken) {
|
|
12568
|
+
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` });
|
|
12569
|
+
return false;
|
|
12570
|
+
}
|
|
12571
|
+
if (!woken.ok) {
|
|
12572
|
+
await ack(cmd.id, "rejected", { result: woken.why });
|
|
12573
|
+
return false;
|
|
12574
|
+
}
|
|
12575
|
+
await ack(cmd.id, "done", { result: woken.why });
|
|
12576
|
+
return false;
|
|
12577
|
+
}
|
|
11386
12578
|
case "restart":
|
|
11387
12579
|
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
12580
|
case "update": {
|
|
@@ -11394,6 +12586,11 @@ async function executeOne(cmd, opts) {
|
|
|
11394
12586
|
effects.narrate(`\u26A0\uFE0F \u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0448\u0430\u0433\u0435 \xAB${UPDATE_STEP_LABELS[res.step]}\xBB: ${res.message}`);
|
|
11395
12587
|
return false;
|
|
11396
12588
|
}
|
|
12589
|
+
if (res.restart === false) {
|
|
12590
|
+
await ack(cmd.id, "done", { step: res.step, result: res.message });
|
|
12591
|
+
effects.narrate(`\u2705 ${res.message}`);
|
|
12592
|
+
return false;
|
|
12593
|
+
}
|
|
11397
12594
|
return await doRestart(cmd, opts, res.message);
|
|
11398
12595
|
}
|
|
11399
12596
|
}
|
|
@@ -11420,12 +12617,12 @@ async function doRestart(cmd, opts, note) {
|
|
|
11420
12617
|
|
|
11421
12618
|
// src/selfUpdate.ts
|
|
11422
12619
|
init_git();
|
|
11423
|
-
import { existsSync as
|
|
11424
|
-
import { join as
|
|
11425
|
-
import { $ as $
|
|
12620
|
+
import { existsSync as existsSync14 } from "fs";
|
|
12621
|
+
import { join as join27 } from "path";
|
|
12622
|
+
import { $ as $5 } from "execa";
|
|
11426
12623
|
var DAEMON_PACKAGE = "@taskless-app/swarm";
|
|
11427
12624
|
var defaultRun = async (cmd, args, cwd) => {
|
|
11428
|
-
const res = await $
|
|
12625
|
+
const res = await $5({ reject: false, cwd })(cmd, args);
|
|
11429
12626
|
return { exitCode: res.exitCode ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
11430
12627
|
};
|
|
11431
12628
|
function tail(text, max = 400) {
|
|
@@ -11434,7 +12631,7 @@ function tail(text, max = 400) {
|
|
|
11434
12631
|
}
|
|
11435
12632
|
async function runSelfUpdate(io) {
|
|
11436
12633
|
const run = io.run ?? defaultRun;
|
|
11437
|
-
const exists = io.exists ??
|
|
12634
|
+
const exists = io.exists ?? existsSync14;
|
|
11438
12635
|
const repo = io.repo;
|
|
11439
12636
|
await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
|
|
11440
12637
|
const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
|
|
@@ -11463,7 +12660,7 @@ async function runSelfUpdate(io) {
|
|
|
11463
12660
|
};
|
|
11464
12661
|
}
|
|
11465
12662
|
await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
|
|
11466
|
-
if (!exists(
|
|
12663
|
+
if (!exists(join27(repo, "pnpm-lock.yaml"))) {
|
|
11467
12664
|
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
12665
|
}
|
|
11469
12666
|
const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
|
|
@@ -11482,6 +12679,31 @@ async function runSelfUpdate(io) {
|
|
|
11482
12679
|
function defaultSelfUpdateIO(repo, onStep) {
|
|
11483
12680
|
return { repo, run: defaultRun, mainRef: remoteMainRef, onStep };
|
|
11484
12681
|
}
|
|
12682
|
+
async function runPackageSelfUpdate(io) {
|
|
12683
|
+
await io.onStep("fetch", "\u0441\u043C\u043E\u0442\u0440\u044E, \u0447\u0442\u043E \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043E");
|
|
12684
|
+
const latest = await io.latest();
|
|
12685
|
+
if (!latest) {
|
|
12686
|
+
return { ok: false, step: "fetch", message: "\u0434\u043E registry.npmjs.org \u043D\u0435 \u0434\u043E\u0437\u0432\u043E\u043D\u0438\u043B\u0438\u0441\u044C \u2014 \u043A\u0430\u043A\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430, \u0443\u0437\u043D\u0430\u0442\u044C \u043D\u0435\u0447\u0435\u043C" };
|
|
12687
|
+
}
|
|
12688
|
+
if (!isOutdated(io.currentVersion, latest)) {
|
|
12689
|
+
return { ok: true, restart: false, step: "install", message: `\u0443\u0436\u0435 \u0441\u0432\u0435\u0436\u0430\u044F \u0432\u0435\u0440\u0441\u0438\u044F: v${io.currentVersion} (\u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u0430 v${latest})` };
|
|
12690
|
+
}
|
|
12691
|
+
await io.onStep("install", `\u0441\u0442\u0430\u0432\u043B\u044E v${latest}`);
|
|
12692
|
+
const command = io.installCommand(`${SWARM_PACKAGE}@${latest}`);
|
|
12693
|
+
const code = await io.exec(command);
|
|
12694
|
+
if (code !== 0) {
|
|
12695
|
+
return { ok: false, step: "install", message: `\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043D\u0435 \u043F\u0440\u043E\u0448\u043B\u0430 (\u043A\u043E\u0434 ${code}): ${command} \u2014 \u0432\u044B\u043F\u043E\u043B\u043D\u0438 \u044D\u0442\u0443 \u0441\u0442\u0440\u043E\u043A\u0443 \u0440\u0443\u043A\u0430\u043C\u0438 \u0438 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438, \u043D\u0430 \u0447\u0451\u043C \u0441\u043F\u043E\u0442\u044B\u043A\u0430\u0435\u0442\u0441\u044F` };
|
|
12696
|
+
}
|
|
12697
|
+
return { ok: true, step: "install", message: `\u043E\u0431\u043D\u043E\u0432\u0438\u043B\u0441\u044F \u0434\u043E v${latest} (\u0431\u044B\u043B\u043E v${io.currentVersion})` };
|
|
12698
|
+
}
|
|
12699
|
+
|
|
12700
|
+
// src/cliIo.ts
|
|
12701
|
+
import { createInterface } from "readline/promises";
|
|
12702
|
+
import { execa as execa3 } from "execa";
|
|
12703
|
+
async function execLoud(command) {
|
|
12704
|
+
const res = await execa3(command, { shell: true, reject: false, stdio: "inherit" });
|
|
12705
|
+
return res.exitCode ?? 1;
|
|
12706
|
+
}
|
|
11485
12707
|
|
|
11486
12708
|
// src/narration.ts
|
|
11487
12709
|
var RING_LIMIT = 20;
|
|
@@ -11497,11 +12719,12 @@ function makeOvermindNarrator() {
|
|
|
11497
12719
|
}
|
|
11498
12720
|
|
|
11499
12721
|
// src/probe.ts
|
|
12722
|
+
init_executors();
|
|
11500
12723
|
import { spawn as spawn2 } from "child_process";
|
|
11501
|
-
var LIMIT_RE = /(weekly limit|usage limit|hit your .*limit)/i;
|
|
11502
12724
|
var RESET_RE = /resets[^\n]*/i;
|
|
11503
|
-
function
|
|
11504
|
-
const
|
|
12725
|
+
function executorHealthy(opts = {}) {
|
|
12726
|
+
const executor = opts.executor ?? resolveExecutor();
|
|
12727
|
+
const bin = opts.bin ?? executor.resolveBin();
|
|
11505
12728
|
const timeoutMs = opts.timeoutMs ?? 2e4;
|
|
11506
12729
|
return new Promise((resolve4) => {
|
|
11507
12730
|
let done = false;
|
|
@@ -11516,7 +12739,7 @@ function anthropicHealthy(opts = {}) {
|
|
|
11516
12739
|
}
|
|
11517
12740
|
resolve4(r);
|
|
11518
12741
|
};
|
|
11519
|
-
const child = spawn2(bin, [...
|
|
12742
|
+
const child = spawn2(bin, [...executor.probe.args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
11520
12743
|
child.stdout?.on("data", (d) => {
|
|
11521
12744
|
out += d.toString();
|
|
11522
12745
|
});
|
|
@@ -11527,7 +12750,9 @@ function anthropicHealthy(opts = {}) {
|
|
|
11527
12750
|
child.on("error", () => finish({ ok: false, reason: "exec", resetHint: null }));
|
|
11528
12751
|
child.on("exit", (code) => {
|
|
11529
12752
|
if (code === 0) return finish({ ok: true, reason: "ok", resetHint: null });
|
|
11530
|
-
if (
|
|
12753
|
+
if (executor.probe.limitPattern.test(out)) {
|
|
12754
|
+
return finish({ ok: false, reason: "limit", resetHint: out.match(RESET_RE)?.[0]?.trim() ?? null });
|
|
12755
|
+
}
|
|
11531
12756
|
finish({ ok: false, reason: "provider", resetHint: null });
|
|
11532
12757
|
});
|
|
11533
12758
|
});
|
|
@@ -11553,18 +12778,21 @@ async function fetchExternalStorm(url, opts = {}) {
|
|
|
11553
12778
|
}
|
|
11554
12779
|
|
|
11555
12780
|
// src/anomalyState.ts
|
|
11556
|
-
import { existsSync as
|
|
11557
|
-
import { join as
|
|
12781
|
+
import { existsSync as existsSync15, readFileSync as readFileSync19, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
|
|
12782
|
+
import { join as join28 } from "path";
|
|
11558
12783
|
var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
11559
|
-
var
|
|
12784
|
+
var MAX_RESOLVED = 200;
|
|
12785
|
+
function emptyState() {
|
|
12786
|
+
return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
|
|
12787
|
+
}
|
|
11560
12788
|
function anomalyStatePath(home) {
|
|
11561
|
-
return
|
|
12789
|
+
return join28(home, "anomaly-state.json");
|
|
11562
12790
|
}
|
|
11563
12791
|
function loadAnomalyState(home, now = Date.now(), io = {}) {
|
|
11564
|
-
const exists = io.exists ??
|
|
12792
|
+
const exists = io.exists ?? existsSync15;
|
|
11565
12793
|
const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
|
|
11566
12794
|
const path = anomalyStatePath(home);
|
|
11567
|
-
if (!exists(path)) return
|
|
12795
|
+
if (!exists(path)) return emptyState();
|
|
11568
12796
|
try {
|
|
11569
12797
|
const raw = JSON.parse(readFile2(path));
|
|
11570
12798
|
const since = numbers(raw.since);
|
|
@@ -11579,10 +12807,14 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
|
|
|
11579
12807
|
since: pick(since, fresh),
|
|
11580
12808
|
lastFiredAt: pick(lastFiredAt, fresh),
|
|
11581
12809
|
fires: pick(fires, fresh),
|
|
12810
|
+
// TAS-485: очередь гашения НЕ фильтруем тем же TTL, что ключи. Она про
|
|
12811
|
+
// уведомление, которое УЖЕ висит у человека в инбоксе: пролежав неделю
|
|
12812
|
+
// недоставленным, гашение не становится лишним — оно становится нужнее.
|
|
12813
|
+
resolved: strings(raw.resolved).slice(0, MAX_RESOLVED),
|
|
11582
12814
|
stopQuiet: parseQuiet(raw.stopQuiet)
|
|
11583
12815
|
};
|
|
11584
12816
|
} catch {
|
|
11585
|
-
return
|
|
12817
|
+
return emptyState();
|
|
11586
12818
|
}
|
|
11587
12819
|
}
|
|
11588
12820
|
function saveAnomalyState(home, state, io = {}) {
|
|
@@ -11605,9 +12837,14 @@ var AnomalyStateStore = class {
|
|
|
11605
12837
|
/** Инъекция в AnomalyTracker (см. AnomalyTrackerOpts.io). */
|
|
11606
12838
|
trackerIo() {
|
|
11607
12839
|
return {
|
|
11608
|
-
load: () => ({
|
|
12840
|
+
load: () => ({
|
|
12841
|
+
since: this.state.since,
|
|
12842
|
+
lastFiredAt: this.state.lastFiredAt,
|
|
12843
|
+
fires: this.state.fires,
|
|
12844
|
+
resolved: this.state.resolved
|
|
12845
|
+
}),
|
|
11609
12846
|
save: (snapshot) => {
|
|
11610
|
-
this.state = { ...snapshot, stopQuiet: this.state.stopQuiet };
|
|
12847
|
+
this.state = { ...snapshot, resolved: (snapshot.resolved ?? []).slice(0, MAX_RESOLVED), stopQuiet: this.state.stopQuiet };
|
|
11611
12848
|
this.flush();
|
|
11612
12849
|
}
|
|
11613
12850
|
};
|
|
@@ -11649,6 +12886,10 @@ function numbers(raw) {
|
|
|
11649
12886
|
}
|
|
11650
12887
|
return out;
|
|
11651
12888
|
}
|
|
12889
|
+
function strings(raw) {
|
|
12890
|
+
if (!Array.isArray(raw)) return [];
|
|
12891
|
+
return raw.filter((v) => typeof v === "string" && v.length > 0);
|
|
12892
|
+
}
|
|
11652
12893
|
function pick(src, keys) {
|
|
11653
12894
|
const out = {};
|
|
11654
12895
|
for (const k of keys) if (src[k] != null) out[k] = src[k];
|
|
@@ -11681,7 +12922,7 @@ function isRepeatEvent(prev, cur) {
|
|
|
11681
12922
|
}
|
|
11682
12923
|
function swarmLastActivityMs(db) {
|
|
11683
12924
|
let latest = null;
|
|
11684
|
-
const
|
|
12925
|
+
const bump2 = (iso) => {
|
|
11685
12926
|
const ms = sqliteMs(iso);
|
|
11686
12927
|
if (latest == null || ms > latest) latest = ms;
|
|
11687
12928
|
};
|
|
@@ -11689,12 +12930,12 @@ function swarmLastActivityMs(db) {
|
|
|
11689
12930
|
if (isTerminalStatus(run.status)) continue;
|
|
11690
12931
|
const evs = db.events(run.id);
|
|
11691
12932
|
if (!evs.length) {
|
|
11692
|
-
|
|
12933
|
+
bump2(run.updated_at);
|
|
11693
12934
|
continue;
|
|
11694
12935
|
}
|
|
11695
12936
|
let i = evs.length - 1;
|
|
11696
12937
|
while (i > 0 && isRepeatEvent(evs[i - 1], evs[i])) i--;
|
|
11697
|
-
|
|
12938
|
+
bump2(evs[i].at);
|
|
11698
12939
|
}
|
|
11699
12940
|
return latest;
|
|
11700
12941
|
}
|
|
@@ -11820,7 +13061,193 @@ function decideWatchdogAction(db, now, state, thresholds, droneCap, wt, exclusiv
|
|
|
11820
13061
|
// src/overmind.ts
|
|
11821
13062
|
init_storm();
|
|
11822
13063
|
init_src();
|
|
13064
|
+
|
|
13065
|
+
// src/zones.ts
|
|
13066
|
+
init_src();
|
|
13067
|
+
|
|
13068
|
+
// src/gitHistory.ts
|
|
13069
|
+
import { $ as $6 } from "execa";
|
|
13070
|
+
var REC = "";
|
|
13071
|
+
var FIELD = "";
|
|
13072
|
+
var LOG_FORMAT = `${REC}%H${FIELD}%s`;
|
|
13073
|
+
var SLUG_IN_SUBJECT = /\b([A-Z][A-Z0-9]{0,7}-\d+)\b/;
|
|
13074
|
+
function slugFromSubject(subject) {
|
|
13075
|
+
return SLUG_IN_SUBJECT.exec(subject)?.[1] ?? null;
|
|
13076
|
+
}
|
|
13077
|
+
function unquotePath(raw) {
|
|
13078
|
+
const p = raw.trim();
|
|
13079
|
+
return p.length >= 2 && p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
|
|
13080
|
+
}
|
|
13081
|
+
function parseGitLog(out) {
|
|
13082
|
+
const commits = [];
|
|
13083
|
+
for (const chunk of out.split(REC)) {
|
|
13084
|
+
if (!chunk.trim()) continue;
|
|
13085
|
+
const [head, ...rest] = chunk.split("\n");
|
|
13086
|
+
const [sha, subject] = (head ?? "").split(FIELD);
|
|
13087
|
+
if (!sha) continue;
|
|
13088
|
+
const files = rest.map(unquotePath).filter(Boolean);
|
|
13089
|
+
commits.push({ sha, subject: subject ?? "", files });
|
|
13090
|
+
}
|
|
13091
|
+
return commits;
|
|
13092
|
+
}
|
|
13093
|
+
var NOISE_RE = /^(journal\/|changelog\/|docs\/|design\/|scratch\/|\.claude\/|.*\.lock$|pnpm-lock\.yaml$)/;
|
|
13094
|
+
function isNoisePath(path) {
|
|
13095
|
+
return NOISE_RE.test(path);
|
|
13096
|
+
}
|
|
13097
|
+
var GROUP_DEFAULTS = { maxFilesPerCommit: 120, ignore: isNoisePath };
|
|
13098
|
+
function groupTaskHistory(commits, opts = {}) {
|
|
13099
|
+
const o = { ...GROUP_DEFAULTS, ...opts };
|
|
13100
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
13101
|
+
for (const c of commits) {
|
|
13102
|
+
const slug = slugFromSubject(c.subject);
|
|
13103
|
+
if (!slug) continue;
|
|
13104
|
+
if (c.files.length > o.maxFilesPerCommit) continue;
|
|
13105
|
+
const files = c.files.filter((f) => !o.ignore(f));
|
|
13106
|
+
if (!files.length) continue;
|
|
13107
|
+
let acc = bySlug.get(slug);
|
|
13108
|
+
if (!acc) bySlug.set(slug, acc = { files: /* @__PURE__ */ new Set(), texts: [] });
|
|
13109
|
+
for (const f of files) acc.files.add(f);
|
|
13110
|
+
acc.texts.push(c.subject);
|
|
13111
|
+
}
|
|
13112
|
+
return [...bySlug.entries()].map(([slug, acc]) => ({
|
|
13113
|
+
slug,
|
|
13114
|
+
files: [...acc.files],
|
|
13115
|
+
text: acc.texts.join(" \xB7 ")
|
|
13116
|
+
}));
|
|
13117
|
+
}
|
|
13118
|
+
async function readCommitLog(repo, opts = {}) {
|
|
13119
|
+
const max = opts.maxCommits ?? 1500;
|
|
13120
|
+
try {
|
|
13121
|
+
const { stdout } = await $6({ cwd: repo, reject: false })`git log --no-merges --name-only --pretty=format:${LOG_FORMAT} -n ${String(max)} ${opts.ref ?? "HEAD"}`;
|
|
13122
|
+
return parseGitLog(stdout ?? "");
|
|
13123
|
+
} catch {
|
|
13124
|
+
return [];
|
|
13125
|
+
}
|
|
13126
|
+
}
|
|
13127
|
+
async function readTaskHistory(repo, opts = {}) {
|
|
13128
|
+
return groupTaskHistory(await readCommitLog(repo, opts), opts);
|
|
13129
|
+
}
|
|
13130
|
+
async function headSha(repo) {
|
|
13131
|
+
try {
|
|
13132
|
+
const { stdout, exitCode } = await $6({ cwd: repo, reject: false })`git rev-parse HEAD`;
|
|
13133
|
+
return exitCode === 0 ? stdout.trim() : null;
|
|
13134
|
+
} catch {
|
|
13135
|
+
return null;
|
|
13136
|
+
}
|
|
13137
|
+
}
|
|
13138
|
+
|
|
13139
|
+
// src/zones.ts
|
|
13140
|
+
function runText(run) {
|
|
13141
|
+
return [run.task_title ?? "", run.task_desc ?? ""].join(" ").trim();
|
|
13142
|
+
}
|
|
13143
|
+
function predictForRun(model, run) {
|
|
13144
|
+
return predictFiles(model, { zone: run.area, text: runText(run) });
|
|
13145
|
+
}
|
|
13146
|
+
function short(files, n = 3) {
|
|
13147
|
+
return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
|
|
13148
|
+
}
|
|
13149
|
+
var SPINE_PREDICTION_WEIGHT = 0.7;
|
|
13150
|
+
function decideConflict({ model, candidate, active }) {
|
|
13151
|
+
if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
|
|
13152
|
+
if (!hasZoneModel(model)) return { kind: "unknown" };
|
|
13153
|
+
const predicted = predictForRun(model, candidate);
|
|
13154
|
+
if (!predicted.length) return { kind: "unknown" };
|
|
13155
|
+
if (!active.length) return { kind: "clear" };
|
|
13156
|
+
const spine = new Set(model.spine);
|
|
13157
|
+
const candidateSpine = predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
|
|
13158
|
+
if (candidateSpine.length) {
|
|
13159
|
+
const blocker = active[0];
|
|
13160
|
+
return {
|
|
13161
|
+
kind: "wait",
|
|
13162
|
+
blockedBy: blocker.run,
|
|
13163
|
+
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})`
|
|
13164
|
+
};
|
|
13165
|
+
}
|
|
13166
|
+
for (const w of active) {
|
|
13167
|
+
if (!w.actual) continue;
|
|
13168
|
+
const theirSpine = w.files.filter((f) => spine.has(f));
|
|
13169
|
+
if (theirSpine.length && zoneForFiles(model, w.files)?.zone === SPINE_ZONE) {
|
|
13170
|
+
return {
|
|
13171
|
+
kind: "wait",
|
|
13172
|
+
blockedBy: w.run,
|
|
13173
|
+
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`
|
|
13174
|
+
};
|
|
13175
|
+
}
|
|
13176
|
+
}
|
|
13177
|
+
for (const w of active) {
|
|
13178
|
+
const risk = overlapRisk(predicted, w.files);
|
|
13179
|
+
if (!risk.overlap) continue;
|
|
13180
|
+
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)})`;
|
|
13181
|
+
return {
|
|
13182
|
+
kind: "wait",
|
|
13183
|
+
blockedBy: w.run,
|
|
13184
|
+
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)")
|
|
13185
|
+
};
|
|
13186
|
+
}
|
|
13187
|
+
return { kind: "clear" };
|
|
13188
|
+
}
|
|
13189
|
+
function suggestAreaForRun(model, run) {
|
|
13190
|
+
if (!hasZoneModel(model)) return null;
|
|
13191
|
+
const text = runText(run);
|
|
13192
|
+
if (!text) return null;
|
|
13193
|
+
const top = suggestZonesFromText(model, text)[0];
|
|
13194
|
+
return top && top.confidence >= 0.3 ? { zone: top.zone, confidence: top.confidence, why: top.why } : null;
|
|
13195
|
+
}
|
|
13196
|
+
var DEFAULT_TTL_MS = 10 * 6e4;
|
|
13197
|
+
var ZoneModels = class {
|
|
13198
|
+
constructor(opts = {}) {
|
|
13199
|
+
this.opts = opts;
|
|
13200
|
+
}
|
|
13201
|
+
opts;
|
|
13202
|
+
cache = /* @__PURE__ */ new Map();
|
|
13203
|
+
/** Модель репо. Истории нет / это не git — EMPTY_ZONE_MODEL (раздача на старом локе). */
|
|
13204
|
+
async forRepo(repo, now = Date.now()) {
|
|
13205
|
+
if (!repo) return EMPTY_ZONE_MODEL;
|
|
13206
|
+
const head = await headSha(repo);
|
|
13207
|
+
const hit = this.cache.get(repo);
|
|
13208
|
+
if (hit && hit.head === head && now - hit.builtAt < (this.opts.ttlMs ?? DEFAULT_TTL_MS)) return hit.model;
|
|
13209
|
+
const model = await this.build(repo);
|
|
13210
|
+
this.cache.set(repo, { head, builtAt: now, model });
|
|
13211
|
+
return model;
|
|
13212
|
+
}
|
|
13213
|
+
/** Сбросить кэш (тесты, `swarm zones --refresh`). */
|
|
13214
|
+
clear() {
|
|
13215
|
+
this.cache.clear();
|
|
13216
|
+
}
|
|
13217
|
+
async build(repo) {
|
|
13218
|
+
const history = await readTaskHistory(repo, { ...this.opts.maxCommits ? { maxCommits: this.opts.maxCommits } : {} });
|
|
13219
|
+
return buildZoneModel(withManualAreas(history, this.opts.manualAreas?.() ?? /* @__PURE__ */ new Map()));
|
|
13220
|
+
}
|
|
13221
|
+
};
|
|
13222
|
+
var ActiveFilesCache = class {
|
|
13223
|
+
constructor(read, ttlMs = 3e4) {
|
|
13224
|
+
this.read = read;
|
|
13225
|
+
this.ttlMs = ttlMs;
|
|
13226
|
+
}
|
|
13227
|
+
read;
|
|
13228
|
+
ttlMs;
|
|
13229
|
+
cache = /* @__PURE__ */ new Map();
|
|
13230
|
+
async filesOf(run, now = Date.now()) {
|
|
13231
|
+
const hit = this.cache.get(run.id);
|
|
13232
|
+
if (hit && now - hit.at < this.ttlMs) return hit.files;
|
|
13233
|
+
const files = await this.read(run);
|
|
13234
|
+
this.cache.set(run.id, { at: now, files });
|
|
13235
|
+
return files;
|
|
13236
|
+
}
|
|
13237
|
+
/** Забыть всех, кого нет среди живых (дрон закончил — его файлы больше не заняты). */
|
|
13238
|
+
prune(liveIds) {
|
|
13239
|
+
const live = new Set(liveIds);
|
|
13240
|
+
for (const id of [...this.cache.keys()]) if (!live.has(id)) this.cache.delete(id);
|
|
13241
|
+
}
|
|
13242
|
+
};
|
|
13243
|
+
function withManualAreas(history, areas) {
|
|
13244
|
+
return history.map((e) => ({ ...e, area: normalizeZoneName(areas.get(e.slug) ?? null) }));
|
|
13245
|
+
}
|
|
13246
|
+
|
|
13247
|
+
// src/overmind.ts
|
|
13248
|
+
var ANOMALY_RESOLVE_BATCH = 50;
|
|
11823
13249
|
var THIS_FILE = fileURLToPath2(import.meta.url);
|
|
13250
|
+
var executorHasSession = (p) => EXECUTOR.session.exists(p);
|
|
11824
13251
|
var OVERMIND_ENTRY = THIS_FILE;
|
|
11825
13252
|
var STALL_MS = Number(process.env.SWARM_STALL_MS) || 10 * 60 * 1e3;
|
|
11826
13253
|
var STALL_FLOOR_MS = Number(process.env.SWARM_STALL_FLOOR_MS) || 30 * 60 * 1e3;
|
|
@@ -11834,6 +13261,10 @@ var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
|
|
|
11834
13261
|
var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
|
|
11835
13262
|
var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
|
|
11836
13263
|
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";
|
|
13264
|
+
var REVIEW_UNRECOVERABLE_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u044C";
|
|
13265
|
+
function reviewUnrecoverableMark(branch) {
|
|
13266
|
+
return `${REVIEW_UNRECOVERABLE_MARK} (\u0432\u0435\u0442\u043A\u0430 ${branch})`;
|
|
13267
|
+
}
|
|
11837
13268
|
var REVIEW_CYCLE_LIMIT = Number(process.env.SWARM_REVIEW_CYCLE_LIMIT) || 3;
|
|
11838
13269
|
var REVIEW_CYCLE_ESCALATION_MARK = "\u0440\u0435-\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0446\u0438\u043A\u043B\u0438\u043B\u043E\u0441\u044C";
|
|
11839
13270
|
var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
|
|
@@ -11860,6 +13291,7 @@ var CHANGES_WAKE_ESCALATION_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\
|
|
|
11860
13291
|
var NEEDS_HUMAN_MARKS = [
|
|
11861
13292
|
RESOLVE_ESCALATION_MARK,
|
|
11862
13293
|
REVIEW_ESCALATION_MARK,
|
|
13294
|
+
REVIEW_UNRECOVERABLE_MARK,
|
|
11863
13295
|
REVIEW_CYCLE_ESCALATION_MARK,
|
|
11864
13296
|
CI_WAKE_ESCALATION_MARK,
|
|
11865
13297
|
CHANGES_WAKE_ESCALATION_MARK
|
|
@@ -11980,20 +13412,20 @@ function countActiveDrones(db) {
|
|
|
11980
13412
|
function countActiveReviewers(db) {
|
|
11981
13413
|
return db.listRuns().filter((r) => r.kind === "reviewer" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
|
|
11982
13414
|
}
|
|
11983
|
-
async function
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
deps.kill(rv.pid);
|
|
11989
|
-
} catch {
|
|
11990
|
-
}
|
|
13415
|
+
async function closeReviewerRun(deps, rv, reason) {
|
|
13416
|
+
if (rv.pid != null && deps.kill && deps.isAlive(rv.pid)) {
|
|
13417
|
+
try {
|
|
13418
|
+
deps.kill(rv.pid);
|
|
13419
|
+
} catch {
|
|
11991
13420
|
}
|
|
11992
|
-
if (rv.worktree_path) await deps.removeWorktree(rv.repo, rv.worktree_path).catch(() => {
|
|
11993
|
-
});
|
|
11994
|
-
deps.db.updateRun(rv.id, { status: "done" });
|
|
11995
|
-
deps.db.addEvent(rv.id, "done", "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E \u2014 \u0440\u0435\u0432\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E");
|
|
11996
13421
|
}
|
|
13422
|
+
if (rv.worktree_path) await deps.removeWorktree(rv.repo, rv.worktree_path).catch(() => {
|
|
13423
|
+
});
|
|
13424
|
+
deps.db.updateRun(rv.id, { status: "done", stop_reason: null });
|
|
13425
|
+
deps.db.addEvent(rv.id, "done", reason);
|
|
13426
|
+
}
|
|
13427
|
+
async function closeReviewersOnDeadPr(deps, reviewRunId, reason) {
|
|
13428
|
+
for (const rv of deps.db.activeReviewersFor(reviewRunId)) await closeReviewerRun(deps, rv, reason);
|
|
11997
13429
|
}
|
|
11998
13430
|
function ciWaitStartedMs(db, runId) {
|
|
11999
13431
|
const evs = db.events(runId);
|
|
@@ -12067,7 +13499,39 @@ async function finalizeMergedReview(deps, run) {
|
|
|
12067
13499
|
deps.db.addEvent(run.id, "merged", run.pr_url ?? "");
|
|
12068
13500
|
deps.db.updateRun(run.id, { status: "done" });
|
|
12069
13501
|
deps.db.addEvent(run.id, "done");
|
|
12070
|
-
await
|
|
13502
|
+
await closeReviewersOnDeadPr(deps, run.id, "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E \u2014 \u0440\u0435\u0432\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E");
|
|
13503
|
+
}
|
|
13504
|
+
async function finalizeClosedPrReview(deps, run, reason) {
|
|
13505
|
+
await closeReviewersOnDeadPr(deps, run.id, "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E");
|
|
13506
|
+
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
13507
|
+
});
|
|
13508
|
+
deps.db.updateRun(run.id, { status: "done" });
|
|
13509
|
+
deps.db.addEvent(run.id, "done", reason);
|
|
13510
|
+
}
|
|
13511
|
+
var REVIEW_PR_UNKNOWN_MARK = "\u0441\u0442\u0430\u0442\u0443\u0441 PR \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D";
|
|
13512
|
+
async function prGateForReviewer(deps, run, prUrl, when) {
|
|
13513
|
+
const st = await deps.prStatus(prUrl).catch(() => null);
|
|
13514
|
+
switch (prLiveness(st)) {
|
|
13515
|
+
case "merged":
|
|
13516
|
+
await finalizeMergedReview(deps, run);
|
|
13517
|
+
narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D, ${when} \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u043F\u0440\u043E\u0433\u043E\u043D, \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D.`);
|
|
13518
|
+
return { kind: "finalized" };
|
|
13519
|
+
case "closed":
|
|
13520
|
+
await finalizeClosedPrReview(deps, run, `PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 (${when}) \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E`);
|
|
13521
|
+
narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, ${when} \u2014 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E, \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442.`);
|
|
13522
|
+
return { kind: "finalized" };
|
|
13523
|
+
case "unknown":
|
|
13524
|
+
if (!deps.db.events(run.id).some((e) => (e.text ?? "").includes(REVIEW_PR_UNKNOWN_MARK))) {
|
|
13525
|
+
deps.db.addEvent(run.id, "step", `${REVIEW_PR_UNKNOWN_MARK} (gh \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B \u0438\u043B\u0438 \u0432 pr_url \u043D\u0435 PR) \u2014 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0435\u043C, \u0441\u043F\u0440\u043E\u0441\u0438\u043C \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C \u0442\u0438\u043A\u0435`);
|
|
13526
|
+
}
|
|
13527
|
+
return { kind: "wait" };
|
|
13528
|
+
default:
|
|
13529
|
+
if (st.conflicting || st.isDraft) {
|
|
13530
|
+
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, ${when} \u2014 \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D \u0434\u043E \u0441\u043B\u0435\u0434. \u0442\u0438\u043A\u0430`);
|
|
13531
|
+
return { kind: "wait" };
|
|
13532
|
+
}
|
|
13533
|
+
return { kind: "spawn", st };
|
|
13534
|
+
}
|
|
12071
13535
|
}
|
|
12072
13536
|
function doneMergedPrForSlug(db, slug) {
|
|
12073
13537
|
const done = db.listRuns("done").find((r) => r.task_slug === slug && r.pr_url && r.kind !== "hero" && r.resolves_run_id == null && r.reviews_run_id == null);
|
|
@@ -12130,6 +13594,22 @@ async function closeMergedTaskRuns(deps) {
|
|
|
12130
13594
|
}
|
|
12131
13595
|
}
|
|
12132
13596
|
}
|
|
13597
|
+
async function closeOrphanedReviewers(deps) {
|
|
13598
|
+
for (const rv of deps.db.listRuns()) {
|
|
13599
|
+
if (rv.kind !== "reviewer" || rv.reviews_run_id == null) continue;
|
|
13600
|
+
if (!REVIEWER_ALIVE_STATUSES.has(rv.status)) continue;
|
|
13601
|
+
const parent = deps.db.getRun(rv.reviews_run_id);
|
|
13602
|
+
if (parent && !isTerminalStatus(parent.status)) continue;
|
|
13603
|
+
try {
|
|
13604
|
+
const why = parent ? `\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442 (${STATUS_META[parent.status]?.label ?? parent.status})` : "\u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u0438\u0441\u0447\u0435\u0437";
|
|
13605
|
+
await closeReviewerRun(deps, rv, `${why} \u2014 PR \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u0440\u0435\u0432\u044C\u044E\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E`);
|
|
13606
|
+
if (parent) deps.db.addEvent(parent.id, "step", `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${rv.name} (#${rv.id}) \u0437\u0430\u043A\u0440\u044B\u0442: ${why}`);
|
|
13607
|
+
narrate(deps, `${droneLabel(rv)} \u043E\u0441\u0442\u0430\u043B\u0441\u044F \u043D\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u043E\u043C \u043F\u0440\u043E\u0433\u043E\u043D\u0435 \u2014 \u0433\u0430\u0448\u0443 \u0441\u0430\u043C, \u0441\u043B\u043E\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u043E\u0441\u0432\u043E\u0431\u043E\u0436\u0434\u0451\u043D.`);
|
|
13608
|
+
} catch (e) {
|
|
13609
|
+
console.error(`[overmind] \u0437\u0430\u043A\u0440\u044B\u0442\u0438\u0435 \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0435\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 #${rv.id} \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:`, e);
|
|
13610
|
+
}
|
|
13611
|
+
}
|
|
13612
|
+
}
|
|
12133
13613
|
var CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C (\u0441\u0435\u0442\u044C/\u043F\u0440\u043E\u0446\u0435\u0441\u0441) \u2014 \u0442\u044B headless-\u0434\u0440\u043E\u043D, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u0423 \u0416\u0415 \u0437\u0430\u0434\u0430\u0447\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 `git status`/`git log` \u0438 \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0441\u0432\u043E\u0438\u0445 \u0448\u0430\u0433\u043E\u0432: \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E \u2014 \u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438 \u0441 \u043C\u0435\u0441\u0442\u0430 \u043E\u0431\u0440\u044B\u0432\u0430. \u0414\u043E\u0432\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u043E \u043A\u043E\u043D\u0446\u0430, \u041A\u041E\u041C\u041C\u0418\u0422\u042C \u0418 \u041F\u0423\u0428\u042C \u043F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0448\u0430\u0433\u0430, \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url). \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 MCP-\u0442\u0443\u043B ask.";
|
|
12134
13614
|
function buildCiWakeNudge(summary) {
|
|
12135
13615
|
return [
|
|
@@ -12188,8 +13668,12 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
12188
13668
|
// авторские подписи внутри него уже приведены к одной строке (renderAttachmentsBlock).
|
|
12189
13669
|
...attachmentsBlock ? ["", attachmentsBlock] : [],
|
|
12190
13670
|
"",
|
|
12191
|
-
|
|
12192
|
-
|
|
13671
|
+
// TASWM-356: ветка worktree — НЕ обязательно слаг. У продолжения чужого PR она своя
|
|
13672
|
+
// (её завёл прошлый дрон), и `git push -u origin <slug>` оттуда либо откажет, либо заведёт
|
|
13673
|
+
// вторую ветку мимо PR. Пишем ту, что реально записана на прогоне; пусто (первый спавн —
|
|
13674
|
+
// ветку заводит spawnDrone из слага) — прежняя формулировка.
|
|
13675
|
+
`\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"}.`,
|
|
13676
|
+
"\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
13677
|
"\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
13678
|
// TASWM-293: живой случай SCE-5 — ревьюер заблокировал PR за отсутствие CI (у проекта
|
|
12195
13679
|
// его нет и не предполагается), автора разбудили «закрывать замечания», замечаний по коду
|
|
@@ -12206,7 +13690,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
12206
13690
|
`\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
13691
|
"",
|
|
12208
13692
|
"\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.
|
|
13693
|
+
`\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
13694
|
"\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
13695
|
'\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
13696
|
"",
|
|
@@ -12231,7 +13715,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
12231
13715
|
}
|
|
12232
13716
|
return parts.join("\n");
|
|
12233
13717
|
}
|
|
12234
|
-
var HERO_CHARTER_PATH =
|
|
13718
|
+
var HERO_CHARTER_PATH = join29(dirname4(THIS_FILE), "..", "docs", "HERO.md");
|
|
12235
13719
|
function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
|
|
12236
13720
|
const name = run.name || `#${run.id}`;
|
|
12237
13721
|
const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
|
|
@@ -12485,8 +13969,8 @@ async function wakeRun(deps, runId) {
|
|
|
12485
13969
|
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
13970
|
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
13971
|
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 ??
|
|
13972
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
13973
|
+
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
12490
13974
|
const wt = run.worktree_path;
|
|
12491
13975
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
12492
13976
|
const isService = run.reviews_run_id != null || run.resolves_run_id != null;
|
|
@@ -12570,9 +14054,9 @@ function repoHasCi(deps, run) {
|
|
|
12570
14054
|
function repoHasCiWorkflows(worktreePath, repo) {
|
|
12571
14055
|
for (const base3 of [worktreePath, repo]) {
|
|
12572
14056
|
if (!base3) continue;
|
|
12573
|
-
const dir =
|
|
14057
|
+
const dir = join29(base3, ".github", "workflows");
|
|
12574
14058
|
try {
|
|
12575
|
-
if (
|
|
14059
|
+
if (readdirSync12(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
|
|
12576
14060
|
} catch {
|
|
12577
14061
|
}
|
|
12578
14062
|
}
|
|
@@ -12719,8 +14203,8 @@ async function wakeReviewerOnCiReady(deps, run, over) {
|
|
|
12719
14203
|
return true;
|
|
12720
14204
|
}
|
|
12721
14205
|
function worktreeAndTranscriptAlive(deps, worktree) {
|
|
12722
|
-
const exists = deps.worktreeExists ??
|
|
12723
|
-
const transcript = deps.hasTranscript ??
|
|
14206
|
+
const exists = deps.worktreeExists ?? existsSync16;
|
|
14207
|
+
const transcript = deps.hasTranscript ?? executorHasSession;
|
|
12724
14208
|
return exists(worktree) && transcript(worktree);
|
|
12725
14209
|
}
|
|
12726
14210
|
async function spawnFreshReviewerAfterStall(deps, run, why) {
|
|
@@ -12733,10 +14217,9 @@ async function spawnFreshReviewerAfterStall(deps, run, why) {
|
|
|
12733
14217
|
if (run.review_cycle >= REVIEW_CYCLE_LIMIT || deps.db.reviewerAttemptCountFor(run.id) >= REVIEW_SPAWN_LIMIT) {
|
|
12734
14218
|
return;
|
|
12735
14219
|
}
|
|
12736
|
-
const
|
|
12737
|
-
if (
|
|
12738
|
-
|
|
12739
|
-
}
|
|
14220
|
+
const gate = await prGateForReviewer(deps, run, run.pr_url, "\u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B CI");
|
|
14221
|
+
if (gate.kind !== "spawn") return;
|
|
14222
|
+
const st = gate.st;
|
|
12740
14223
|
try {
|
|
12741
14224
|
await deps.spawnReviewer(run, st.files ?? []);
|
|
12742
14225
|
narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u043F\u043E\u0442\u0435\u0440\u044F\u043B\u043E \u0441\u0435\u0441\u0441\u0438\u044E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430, \u0436\u0434\u0430\u0432 CI (${why}) \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0441\u0440\u0430\u0437\u0443, \u043D\u0435 \u0436\u0434\u0443 \u043A\u0430\u0440\u0443\u0441\u0435\u043B\u044C.`);
|
|
@@ -12823,6 +14306,22 @@ async function envGateAllows(deps, run) {
|
|
|
12823
14306
|
function effectiveRepo(deps, run) {
|
|
12824
14307
|
return normalizeRepo(run.repo || deps.repoFallback || "");
|
|
12825
14308
|
}
|
|
14309
|
+
async function zoneConflictFor(db, models, activeFiles, run, repo, repoFallback) {
|
|
14310
|
+
const model = await models.forRepo(repo);
|
|
14311
|
+
if (!hasZoneModel(model)) return { kind: "unknown" };
|
|
14312
|
+
const live = db.listRuns().filter((r) => (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status));
|
|
14313
|
+
activeFiles.prune(live.map((r) => r.id));
|
|
14314
|
+
const active = [];
|
|
14315
|
+
for (const r of live) {
|
|
14316
|
+
if (r.id === run.id) continue;
|
|
14317
|
+
if (normalizeRepo(r.repo || repoFallback || "") !== repo) continue;
|
|
14318
|
+
const files = await activeFiles.filesOf(r);
|
|
14319
|
+
active.push(
|
|
14320
|
+
files.length ? { run: r, files, actual: true } : { run: r, files: predictForRun(model, r).map((p) => p.path), actual: false }
|
|
14321
|
+
);
|
|
14322
|
+
}
|
|
14323
|
+
return decideConflict({ model, candidate: run, active });
|
|
14324
|
+
}
|
|
12826
14325
|
function activeDronesOnRepo(deps, repo, exceptRunId) {
|
|
12827
14326
|
return deps.db.listRuns().filter(
|
|
12828
14327
|
(r) => r.id !== exceptRunId && (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status) && effectiveRepo(deps, r) === repo
|
|
@@ -12863,10 +14362,10 @@ async function exclusiveGateAllows(deps, run) {
|
|
|
12863
14362
|
return true;
|
|
12864
14363
|
}
|
|
12865
14364
|
function pruneGateLogs(deps) {
|
|
12866
|
-
if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size) return;
|
|
14365
|
+
if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size && !deps.overlapLogged?.size) return;
|
|
12867
14366
|
const open = /* @__PURE__ */ new Set();
|
|
12868
14367
|
for (const run of deps.db.listRuns()) if (!isTerminalStatus(run.status)) open.add(run.id);
|
|
12869
|
-
for (const map of [deps.envGateLogged, deps.exclusiveLogged]) {
|
|
14368
|
+
for (const map of [deps.envGateLogged, deps.exclusiveLogged, deps.overlapLogged]) {
|
|
12870
14369
|
if (!map) continue;
|
|
12871
14370
|
for (const id of [...map.keys()]) if (!open.has(id)) map.delete(id);
|
|
12872
14371
|
}
|
|
@@ -12887,6 +14386,18 @@ async function exclusivityBlockedRunIds(deps) {
|
|
|
12887
14386
|
}
|
|
12888
14387
|
return blocked;
|
|
12889
14388
|
}
|
|
14389
|
+
async function fillSuggestedArea(deps, run) {
|
|
14390
|
+
if (run.area || !deps.suggestArea) return;
|
|
14391
|
+
const guess = await deps.suggestArea(run).catch(() => null);
|
|
14392
|
+
if (!guess) return;
|
|
14393
|
+
deps.db.updateRun(run.id, { area: guess.zone });
|
|
14394
|
+
run.area = guess.zone;
|
|
14395
|
+
deps.db.addEvent(
|
|
14396
|
+
run.id,
|
|
14397
|
+
"step",
|
|
14398
|
+
`\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`
|
|
14399
|
+
);
|
|
14400
|
+
}
|
|
12890
14401
|
async function trySpawnQueuedRun(deps, run) {
|
|
12891
14402
|
if (deps.control?.paused) return "skipped";
|
|
12892
14403
|
const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
|
|
@@ -12902,11 +14413,21 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
12902
14413
|
if (!await exclusiveGateAllows(deps, run)) return "skipped";
|
|
12903
14414
|
if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
|
|
12904
14415
|
if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
|
|
14416
|
+
await fillSuggestedArea(deps, run);
|
|
12905
14417
|
try {
|
|
12906
14418
|
await deps.spawn(run);
|
|
12907
14419
|
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
14420
|
return "spawned";
|
|
12909
14421
|
} catch (e) {
|
|
14422
|
+
if (e instanceof FileOverlapError) {
|
|
14423
|
+
const logged = deps.overlapLogged ??= /* @__PURE__ */ new Map();
|
|
14424
|
+
if (logged.get(run.id) !== e.message) {
|
|
14425
|
+
logged.set(run.id, e.message);
|
|
14426
|
+
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)`);
|
|
14427
|
+
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.`);
|
|
14428
|
+
}
|
|
14429
|
+
return "skipped";
|
|
14430
|
+
}
|
|
12910
14431
|
if (e instanceof AreaBusyError) return "skipped";
|
|
12911
14432
|
deps.db.updateRun(run.id, { status: "failed" });
|
|
12912
14433
|
deps.db.addEvent(run.id, "failed", String(e));
|
|
@@ -12933,8 +14454,8 @@ async function resumeWatchdogParked(deps, droneCap) {
|
|
|
12933
14454
|
}
|
|
12934
14455
|
if (!candidate) return;
|
|
12935
14456
|
if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
|
|
12936
|
-
const worktreeExists = deps.worktreeExists ??
|
|
12937
|
-
const hasTranscript = deps.hasTranscript ??
|
|
14457
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
14458
|
+
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
12938
14459
|
const wt = candidate.worktree_path;
|
|
12939
14460
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
12940
14461
|
const mode = canContinue ? "continue" : "fresh";
|
|
@@ -13102,8 +14623,8 @@ async function manageRelief(deps, now, droneCap, stormActive) {
|
|
|
13102
14623
|
if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
|
|
13103
14624
|
if (!deps.respawn) return;
|
|
13104
14625
|
const wt = resumeCandidate.worktree_path;
|
|
13105
|
-
const worktreeExists = deps.worktreeExists ??
|
|
13106
|
-
const hasTranscript = deps.hasTranscript ??
|
|
14626
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
14627
|
+
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
13107
14628
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
13108
14629
|
const mode = canContinue ? "continue" : "fresh";
|
|
13109
14630
|
if (mode === "continue" && wt && deps.reclaimWorktree) {
|
|
@@ -13226,15 +14747,28 @@ ${journal}`);
|
|
|
13226
14747
|
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
14748
|
});
|
|
13228
14749
|
}
|
|
14750
|
+
function syncBranchFromPr(db, run, headBranch) {
|
|
14751
|
+
const fix = branchFromPrPatch(run.branch, headBranch);
|
|
14752
|
+
if (!fix) return run;
|
|
14753
|
+
const updated = db.updateRun(run.id, fix.patch) ?? run;
|
|
14754
|
+
db.addEvent(run.id, "step", fix.event);
|
|
14755
|
+
return updated;
|
|
14756
|
+
}
|
|
14757
|
+
async function worktreeBranchFor(run, prHead) {
|
|
14758
|
+
if (run.branch) return run.branch;
|
|
14759
|
+
const fromPr = run.pr_url && prHead ? await prHead(run.pr_url).catch(() => null) : null;
|
|
14760
|
+
return fromPr || run.task_slug;
|
|
14761
|
+
}
|
|
13229
14762
|
async function adoptDroneToReview(deps, run, prUrl) {
|
|
13230
14763
|
const t = applyReviewTransition(deps.db, run, prUrl);
|
|
14764
|
+
const adopted = syncBranchFromPr(deps.db, run, deps.prHeadBranch ? await deps.prHeadBranch(prUrl).catch(() => null) : null);
|
|
13231
14765
|
deps.db.addEvent(run.id, "pr_opened", prUrl);
|
|
13232
14766
|
deps.db.addEvent(
|
|
13233
14767
|
run.id,
|
|
13234
14768
|
"step",
|
|
13235
14769
|
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
14770
|
);
|
|
13237
|
-
await postAdoptionReport(deps,
|
|
14771
|
+
await postAdoptionReport(deps, adopted, prUrl, t.cycle ?? run.review_cycle ?? 0);
|
|
13238
14772
|
}
|
|
13239
14773
|
async function postScrapedVerdictReport(deps, runId, taskSlug, verdict, droneName, workspaceId, cycle) {
|
|
13240
14774
|
if (!deps.reportComment) return;
|
|
@@ -13264,8 +14798,8 @@ function buildEscalationCandidates(db) {
|
|
|
13264
14798
|
}
|
|
13265
14799
|
async function tickOnce(deps) {
|
|
13266
14800
|
const now = deps.now ? deps.now() : Date.now();
|
|
13267
|
-
const worktreeExists = deps.worktreeExists ??
|
|
13268
|
-
const hasTranscript = deps.hasTranscript ??
|
|
14801
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
14802
|
+
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
13269
14803
|
if (deps.rehydrate) {
|
|
13270
14804
|
const rh = deps.rehydrate;
|
|
13271
14805
|
const sinceLast = rh.lastAt == null ? Infinity : now - rh.lastAt;
|
|
@@ -13391,7 +14925,7 @@ async function tickOnce(deps) {
|
|
|
13391
14925
|
if (!stormActive) {
|
|
13392
14926
|
await resumeWatchdogParked(deps, droneCap);
|
|
13393
14927
|
}
|
|
13394
|
-
for (
|
|
14928
|
+
for (let run of deps.db.listRuns("review")) {
|
|
13395
14929
|
try {
|
|
13396
14930
|
if (run.resolves_run_id != null) {
|
|
13397
14931
|
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
@@ -13410,12 +14944,19 @@ async function tickOnce(deps) {
|
|
|
13410
14944
|
continue;
|
|
13411
14945
|
}
|
|
13412
14946
|
if (!run.pr_url) continue;
|
|
13413
|
-
const
|
|
14947
|
+
const prUrl = run.pr_url;
|
|
14948
|
+
const st = await deps.prStatus(prUrl);
|
|
14949
|
+
run = syncBranchFromPr(deps.db, run, st.headBranch);
|
|
13414
14950
|
if (st.merged) {
|
|
13415
14951
|
await finalizeMergedReview(deps, run);
|
|
13416
14952
|
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.`);
|
|
13417
14953
|
continue;
|
|
13418
14954
|
}
|
|
14955
|
+
if (prLiveness(st) === "closed") {
|
|
14956
|
+
await finalizeClosedPrReview(deps, run, "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E, \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442");
|
|
14957
|
+
narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430 \u2014 ${run.task_slug} \u0441\u043D\u0438\u043C\u0430\u0435\u0442\u0441\u044F \u0441 \u0440\u0435\u0432\u044C\u044E, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
|
|
14958
|
+
continue;
|
|
14959
|
+
}
|
|
13419
14960
|
if (touchesDaemonCode(st.files ?? [])) {
|
|
13420
14961
|
const already = deps.db.events(run.id).some((e) => (e.text ?? "").includes(SELF_REFERENCE_MARK));
|
|
13421
14962
|
if (!already) {
|
|
@@ -13479,11 +15020,13 @@ async function tickOnce(deps) {
|
|
|
13479
15020
|
continue;
|
|
13480
15021
|
}
|
|
13481
15022
|
const attempts = deps.db.reviewerAttemptCountFor(run.id);
|
|
15023
|
+
const runEvents = deps.db.events(run.id);
|
|
15024
|
+
const unrecoverableMark = reviewUnrecoverableMark(run.branch || run.task_slug);
|
|
13482
15025
|
let verdictFromComment = null;
|
|
13483
15026
|
let lastAttempt = null;
|
|
13484
15027
|
if (attempts > 0 && deps.verdictComment) {
|
|
13485
15028
|
lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
|
|
13486
|
-
verdictFromComment = await deps.verdictComment(
|
|
15029
|
+
verdictFromComment = await deps.verdictComment(prUrl, lastAttempt?.created_at ?? run.created_at).catch(() => null);
|
|
13487
15030
|
}
|
|
13488
15031
|
if (verdictFromComment) {
|
|
13489
15032
|
const mark = verdictCommentMark(verdictFromComment.createdAt);
|
|
@@ -13498,32 +15041,32 @@ async function tickOnce(deps) {
|
|
|
13498
15041
|
await wakeAuthorOnChanges(deps, run, now, droneCap);
|
|
13499
15042
|
}
|
|
13500
15043
|
}
|
|
15044
|
+
} else if (runEvents.some((e) => (e.text ?? "").includes(unrecoverableMark))) {
|
|
13501
15045
|
} else if (attempts >= REVIEW_SPAWN_LIMIT) {
|
|
13502
|
-
if (!
|
|
15046
|
+
if (!runEvents.some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
|
|
13503
15047
|
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
15048
|
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
15049
|
}
|
|
13506
15050
|
} else {
|
|
13507
15051
|
const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
|
|
13508
15052
|
if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk) {
|
|
13509
|
-
const
|
|
13510
|
-
if (
|
|
13511
|
-
|
|
13512
|
-
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.`);
|
|
13513
|
-
} else if (freshSt.state === "CLOSED") {
|
|
13514
|
-
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
13515
|
-
});
|
|
13516
|
-
deps.db.updateRun(run.id, { status: "done" });
|
|
13517
|
-
deps.db.addEvent(run.id, "done", "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E");
|
|
13518
|
-
narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
|
|
13519
|
-
} else if (freshSt.conflicting || freshSt.isDraft) {
|
|
13520
|
-
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
|
-
} else {
|
|
15053
|
+
const gate = await prGateForReviewer(deps, run, prUrl, "\u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430");
|
|
15054
|
+
if (gate.kind === "spawn") {
|
|
15055
|
+
run = syncBranchFromPr(deps.db, run, gate.st.headBranch);
|
|
13522
15056
|
try {
|
|
13523
15057
|
await deps.spawnReviewer(run, st.files ?? []);
|
|
13524
15058
|
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
15059
|
} catch (e) {
|
|
13526
|
-
|
|
15060
|
+
if (isUnrecoverableRefError(e)) {
|
|
15061
|
+
deps.db.addEvent(
|
|
15062
|
+
run.id,
|
|
15063
|
+
"step",
|
|
15064
|
+
`${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`
|
|
15065
|
+
);
|
|
15066
|
+
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.`);
|
|
15067
|
+
} else {
|
|
15068
|
+
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)}`);
|
|
15069
|
+
}
|
|
13527
15070
|
}
|
|
13528
15071
|
}
|
|
13529
15072
|
}
|
|
@@ -13534,6 +15077,7 @@ async function tickOnce(deps) {
|
|
|
13534
15077
|
}
|
|
13535
15078
|
}
|
|
13536
15079
|
await closeMergedTaskRuns(deps);
|
|
15080
|
+
await closeOrphanedReviewers(deps);
|
|
13537
15081
|
refreshReviewPhases(deps);
|
|
13538
15082
|
let liveChildPpids;
|
|
13539
15083
|
try {
|
|
@@ -13790,7 +15334,9 @@ async function tickOnce(deps) {
|
|
|
13790
15334
|
// resource-gate-stall (TASWM-212): та же причина, что видит лог manageRelief — один
|
|
13791
15335
|
// deps.gateReason() на тик (кэш hostReading() внутри resources.ts, лишнего спавна нет).
|
|
13792
15336
|
...deps.gateReason ? { gateReason: deps.gateReason() } : {},
|
|
13793
|
-
...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {}
|
|
15337
|
+
...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {},
|
|
15338
|
+
// Рестарт не переигрывает падения (TAS-485) — см. closedBeforeStart в anomalies.ts.
|
|
15339
|
+
...az.startedAt != null ? { daemonStartedAt: az.startedAt } : {}
|
|
13794
15340
|
}),
|
|
13795
15341
|
...buildEscalationCandidates(deps.db),
|
|
13796
15342
|
// ЦЕНА РАЗГРУЗКИ (TASWM-316): «машина не тянет» с числами и рекомендацией — та же
|
|
@@ -13820,6 +15366,20 @@ async function tickOnce(deps) {
|
|
|
13820
15366
|
}
|
|
13821
15367
|
narrate(deps, `\u26A0\uFE0F ${signal.title}`);
|
|
13822
15368
|
}
|
|
15369
|
+
const stale = az.tracker.pendingResolved();
|
|
15370
|
+
if (stale.length > 0) {
|
|
15371
|
+
if (!az.resolve) {
|
|
15372
|
+
az.tracker.forgetResolved(stale);
|
|
15373
|
+
} else {
|
|
15374
|
+
const batch = stale.slice(0, ANOMALY_RESOLVE_BATCH);
|
|
15375
|
+
try {
|
|
15376
|
+
await az.resolve(batch);
|
|
15377
|
+
az.tracker.forgetResolved(batch);
|
|
15378
|
+
} catch (e) {
|
|
15379
|
+
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);
|
|
15380
|
+
}
|
|
15381
|
+
}
|
|
15382
|
+
}
|
|
13823
15383
|
}
|
|
13824
15384
|
if (deps.watchdog) {
|
|
13825
15385
|
try {
|
|
@@ -13885,7 +15445,7 @@ async function cancelRun(deps, slug, reason) {
|
|
|
13885
15445
|
}
|
|
13886
15446
|
async function resolveConflict(deps, reviewRun) {
|
|
13887
15447
|
const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
|
|
13888
|
-
const branch = reviewRun
|
|
15448
|
+
const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
|
|
13889
15449
|
const resolver = deps.db.createRun({
|
|
13890
15450
|
task_slug: reviewRun.task_slug,
|
|
13891
15451
|
repo,
|
|
@@ -13895,7 +15455,7 @@ async function resolveConflict(deps, reviewRun) {
|
|
|
13895
15455
|
workspace_id: reviewRun.workspace_id
|
|
13896
15456
|
// та же задача, тот же воркспейс (TASWM-288)
|
|
13897
15457
|
});
|
|
13898
|
-
const wt =
|
|
15458
|
+
const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
|
|
13899
15459
|
try {
|
|
13900
15460
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
13901
15461
|
await deps.git.setWorktreeIdentity(wt, resolver.name);
|
|
@@ -13925,14 +15485,14 @@ async function resolveConflict(deps, reviewRun) {
|
|
|
13925
15485
|
await deps.git.removeWorktree(repo, wt).catch(() => {
|
|
13926
15486
|
});
|
|
13927
15487
|
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";
|
|
15488
|
+
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
15489
|
deps.db.addEvent(resolver.id, "failed", `${marker}: ${String(e)}`);
|
|
13930
15490
|
throw e;
|
|
13931
15491
|
}
|
|
13932
15492
|
}
|
|
13933
15493
|
async function spawnReviewer(deps, reviewRun, files) {
|
|
13934
15494
|
const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
|
|
13935
|
-
const branch = reviewRun
|
|
15495
|
+
const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
|
|
13936
15496
|
const priorReview = reviewRun.review_cycle > 0 ? lastChangesSummary(deps.db, reviewRun.id) : null;
|
|
13937
15497
|
const verifyMode = deps.verifyMode ? await deps.verifyMode(repo).catch(() => void 0) : void 0;
|
|
13938
15498
|
const checklist = buildReviewChecklist(reviewRun, files, priorReview, verifyMode);
|
|
@@ -13947,7 +15507,7 @@ async function spawnReviewer(deps, reviewRun, files) {
|
|
|
13947
15507
|
// (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
|
|
13948
15508
|
workspace_id: reviewRun.workspace_id
|
|
13949
15509
|
});
|
|
13950
|
-
const wt =
|
|
15510
|
+
const wt = join29(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
|
|
13951
15511
|
try {
|
|
13952
15512
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
13953
15513
|
await deps.git.setWorktreeIdentity(wt, reviewer.name);
|
|
@@ -13982,7 +15542,7 @@ async function respawnRun(deps, run, mode, nudge) {
|
|
|
13982
15542
|
]);
|
|
13983
15543
|
const base3 = effectiveBaseBranch(run.base_branch, projectBase);
|
|
13984
15544
|
const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
|
|
13985
|
-
if (!
|
|
15545
|
+
if (!existsSync16(wt)) {
|
|
13986
15546
|
const branch = runToSpawn.branch || runToSpawn.task_slug;
|
|
13987
15547
|
const source = await addWorktree(repo, wt, branch, {
|
|
13988
15548
|
...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
|
|
@@ -14011,7 +15571,7 @@ ${notice}` : freshPrompt;
|
|
|
14011
15571
|
deps.db.updateRun(run.id, { status: "running", pid });
|
|
14012
15572
|
}
|
|
14013
15573
|
async function syncCrashedOnStartup(deps) {
|
|
14014
|
-
const worktreeExists = deps.worktreeExists ??
|
|
15574
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
14015
15575
|
let orphaned = 0;
|
|
14016
15576
|
let merged = 0;
|
|
14017
15577
|
let requeued = 0;
|
|
@@ -14074,7 +15634,7 @@ async function makeStore(repo, worktreesDir, repoFlag) {
|
|
|
14074
15634
|
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
15635
|
}
|
|
14076
15636
|
}
|
|
14077
|
-
const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW,
|
|
15637
|
+
const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW, homedir5(), repoPaths);
|
|
14078
15638
|
const repoMap = emptyRepoMapState(scanRootsList);
|
|
14079
15639
|
if (SWARM_BACKEND !== "client") {
|
|
14080
15640
|
const { SwarmDb: SwarmDb2 } = await loadSqlite();
|
|
@@ -14148,10 +15708,10 @@ async function migrateWorktreesAtStartup() {
|
|
|
14148
15708
|
legacyDir: WORKTREES_LEGACY_DIR,
|
|
14149
15709
|
targetDir: WORKTREES_DIR,
|
|
14150
15710
|
envOverride: process.env.SWARM_WORKTREES || "",
|
|
14151
|
-
exists: (d) =>
|
|
15711
|
+
exists: (d) => existsSync16(d),
|
|
14152
15712
|
isNonEmptyDir: (d) => {
|
|
14153
15713
|
try {
|
|
14154
|
-
return
|
|
15714
|
+
return readdirSync12(d).length > 0;
|
|
14155
15715
|
} catch {
|
|
14156
15716
|
return false;
|
|
14157
15717
|
}
|
|
@@ -14163,7 +15723,7 @@ async function migrateWorktreesAtStartup() {
|
|
|
14163
15723
|
});
|
|
14164
15724
|
} catch (e) {
|
|
14165
15725
|
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
|
|
15726
|
+
return existsSync16(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
|
|
14167
15727
|
}
|
|
14168
15728
|
}
|
|
14169
15729
|
async function reportWorkspaceAccess(tl, db) {
|
|
@@ -14180,7 +15740,7 @@ async function reportWorkspaceAccess(tl, db) {
|
|
|
14180
15740
|
}
|
|
14181
15741
|
async function runDaemon(opts) {
|
|
14182
15742
|
installCrashGuards();
|
|
14183
|
-
const daemonStart =
|
|
15743
|
+
const daemonStart = captureDaemonStart();
|
|
14184
15744
|
const latestRelease = fetchLatestVersion();
|
|
14185
15745
|
const worktreesDir = await migrateWorktreesAtStartup();
|
|
14186
15746
|
if (!opts.repo) {
|
|
@@ -14233,18 +15793,23 @@ async function runDaemon(opts) {
|
|
|
14233
15793
|
overmindEntry: THIS_FILE,
|
|
14234
15794
|
dbPath: DB_PATH,
|
|
14235
15795
|
runLogsDir: RUN_LOGS_DIR,
|
|
15796
|
+
// Кем исполнять (TASWM-349) — настройка SWARM_EXECUTOR, один резолв на весь демон.
|
|
15797
|
+
executor: EXECUTOR,
|
|
14236
15798
|
droneIsolation: DRONE_ISOLATION,
|
|
14237
15799
|
channel: droneChannelLaunch,
|
|
14238
15800
|
droneCap: () => lastDroneCap
|
|
14239
15801
|
});
|
|
14240
15802
|
let prepareReader;
|
|
15803
|
+
const zoneModels = new ZoneModels({ manualAreas: () => new Map(db.listRuns().map((r) => [r.task_slug, r.area])) });
|
|
15804
|
+
const activeFiles = new ActiveFilesCache((r) => changedPaths(r.worktree_path, { baseBranch: r.base_branch ?? null }));
|
|
14241
15805
|
const baseSpawn = {
|
|
14242
15806
|
db,
|
|
14243
15807
|
repo: opts.repo,
|
|
14244
15808
|
// фолбэк, если у прогона repo пуст
|
|
14245
15809
|
worktreesDir,
|
|
14246
15810
|
launch,
|
|
14247
|
-
prepare: (repo) => prepareReader ? prepareReader(repo) : Promise.resolve(null)
|
|
15811
|
+
prepare: (repo) => prepareReader ? prepareReader(repo) : Promise.resolve(null),
|
|
15812
|
+
conflicts: (run, repo) => zoneConflictFor(db, zoneModels, activeFiles, run, repo, opts.repo)
|
|
14248
15813
|
};
|
|
14249
15814
|
let intake;
|
|
14250
15815
|
if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_MAX_IN_FLIGHT > 0) {
|
|
@@ -14261,7 +15826,7 @@ async function runDaemon(opts) {
|
|
|
14261
15826
|
repoMap = { run: rebuildRepoMap, intervalMs: SWARM_REPO_MAP_MS, lastAt: Date.now() };
|
|
14262
15827
|
}
|
|
14263
15828
|
let daemonVersion;
|
|
14264
|
-
if (
|
|
15829
|
+
if (SWARM_VERSION_CHECK_MS > 0) {
|
|
14265
15830
|
let cached = null;
|
|
14266
15831
|
daemonVersion = {
|
|
14267
15832
|
run: async () => {
|
|
@@ -14278,7 +15843,7 @@ async function runDaemon(opts) {
|
|
|
14278
15843
|
const client = db;
|
|
14279
15844
|
heartbeat = {
|
|
14280
15845
|
push: async () => {
|
|
14281
|
-
const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null);
|
|
15846
|
+
const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastDroneCap);
|
|
14282
15847
|
if (reply.commands.length) onHostCommands(reply.commands);
|
|
14283
15848
|
if (reply.logRequests.length) {
|
|
14284
15849
|
await serveLogRequests(
|
|
@@ -14348,7 +15913,14 @@ async function runDaemon(opts) {
|
|
|
14348
15913
|
autoReviewEnabled: SWARM_AUTO_REVIEW,
|
|
14349
15914
|
outboxPendingCount: () => client.pendingCount(),
|
|
14350
15915
|
notify: (signal) => client.notifyAnomaly(signal),
|
|
14351
|
-
|
|
15916
|
+
// TAS-485: условие ушло → гасим свою же тревогу в инбоксе.
|
|
15917
|
+
resolve: (keys) => client.resolveAnomalies(keys),
|
|
15918
|
+
stopQuiet: () => anomalyState.stopQuiet(),
|
|
15919
|
+
// TAS-485: этот демон отвечает за то, что случилось ПРИ НЁМ. Прогон, закрытый до
|
|
15920
|
+
// старта, он не переоткрывает — иначе каждый перезапуск начинает волну заново
|
|
15921
|
+
// (id прогонов в зеркале свои у каждой жизни процесса, durable-кулдаун по ним не
|
|
15922
|
+
// совпадает — см. closedBeforeStart в anomalies.ts).
|
|
15923
|
+
startedAt: Date.now()
|
|
14352
15924
|
};
|
|
14353
15925
|
}
|
|
14354
15926
|
const watchdog = {
|
|
@@ -14421,6 +15993,9 @@ async function runDaemon(opts) {
|
|
|
14421
15993
|
worktreesDir,
|
|
14422
15994
|
launch,
|
|
14423
15995
|
git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
|
|
15996
|
+
// TASWM-356: ветка ревьюера — фактическая ветка PR. Штатно она уже в поле прогона
|
|
15997
|
+
// (реконсайл синхронит её из headBranch), этот ридер — для прогонов без неё.
|
|
15998
|
+
prHeadBranch,
|
|
14424
15999
|
// TASWM-293: ТОТ ЖЕ живой ридер verifyMode, что у исполнителя (spawnDrone) —
|
|
14425
16000
|
// иначе ревьюер судит проект с verifyMode=local по правилам ci и блокирует PR
|
|
14426
16001
|
// за отсутствие CI, которого у проекта нет и не предполагается.
|
|
@@ -14513,7 +16088,7 @@ async function runDaemon(opts) {
|
|
|
14513
16088
|
if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
|
|
14514
16089
|
if (taskClient) await taskClient.startTask(runToSpawn.task_slug);
|
|
14515
16090
|
},
|
|
14516
|
-
prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state })),
|
|
16091
|
+
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
16092
|
// Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
|
|
14518
16093
|
// (фолбэк — дефолт-репо роя). Реальный gh; логика усыновления — в tickOnce (тестируется).
|
|
14519
16094
|
openPrsForBranch: (branch, repoDir) => openPrsForBranch(branch, repoDir || opts.repo),
|
|
@@ -14537,6 +16112,8 @@ async function runDaemon(opts) {
|
|
|
14537
16112
|
reportComment: (slug, body, label, ws) => tl.forWorkspace(ws).addComment(slug, body, label)
|
|
14538
16113
|
} : {},
|
|
14539
16114
|
prCommitTitles: (prUrl) => prCommitTitles(prUrl),
|
|
16115
|
+
prHeadBranch: (prUrl) => prHeadBranch(prUrl),
|
|
16116
|
+
// TASWM-356: ветка PR на усыновлении, узким запросом
|
|
14540
16117
|
journalForBranch: (repo, branch, slug) => journalForBranch(repo || opts.repo, branch, slug),
|
|
14541
16118
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p),
|
|
14542
16119
|
// TASWM-303: тот же фолбэк repo, что у removeWorktree/journalForBranch — run.repo пуст
|
|
@@ -14546,15 +16123,17 @@ async function runDaemon(opts) {
|
|
|
14546
16123
|
repoHasCi: (run) => repoHasCiWorkflows(run.worktree_path, run.repo || opts.repo),
|
|
14547
16124
|
isAlive: pidAlive,
|
|
14548
16125
|
// Учёт cost/usage (TASWM-188): реальный аккаунтант поверх RUN_LOGS_DIR + db.
|
|
14549
|
-
accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db }),
|
|
16126
|
+
accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR }),
|
|
14550
16127
|
// Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
|
|
14551
16128
|
// (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
|
|
14552
|
-
exitOutcome: makeExitClassifier(RUN_LOGS_DIR),
|
|
16129
|
+
exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
|
|
14553
16130
|
// Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
|
|
14554
16131
|
livePpids,
|
|
14555
16132
|
// Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
|
|
14556
16133
|
// POST /swarm/kill → stopped в проде → overmind гасит живой pid на реконсайле).
|
|
14557
|
-
|
|
16134
|
+
// TASWM-349: свой способ остановки — за адаптером; не задан (оба сегодняшних) → общий
|
|
16135
|
+
// групповой SIGTERM.
|
|
16136
|
+
kill: (pid) => EXECUTOR.stop ? EXECUTOR.stop(pid, "SIGTERM") : killProcessGroup(pid, "SIGTERM"),
|
|
14558
16137
|
// Реклейм worktree паузнутого/возвращаемого дрона (TASWM-228): тот же cwd-скан, что и
|
|
14559
16138
|
// очистка перед сносом worktree, но БЕЗ сноса каталога — worktree остаётся под
|
|
14560
16139
|
// --continue. Гасит тяжёлую работу дрона, ушедшую из его process group (Bash-тул
|
|
@@ -14593,7 +16172,7 @@ async function runDaemon(opts) {
|
|
|
14593
16172
|
lastCommitMs: (p) => worktreeLastCommitMs(p),
|
|
14594
16173
|
// mtime транскрипта claude-сессии как liveness (TASWM-136): точнее коммитов — обновляется
|
|
14595
16174
|
// на каждое действие дрона (Edit/Read/tool-use), не только на коммит.
|
|
14596
|
-
transcriptMtimeMs: (p) =>
|
|
16175
|
+
transcriptMtimeMs: (p) => EXECUTOR.session.mtimeMs(p),
|
|
14597
16176
|
// Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
|
|
14598
16177
|
resourcesOk: hostResourcesOk,
|
|
14599
16178
|
// Ресурс-гейт v2 (TASWM-151): жёсткий порог активной разгрузки — те же сырые показания
|
|
@@ -14624,6 +16203,13 @@ async function runDaemon(opts) {
|
|
|
14624
16203
|
...exclusiveProject ? { exclusiveProject } : {},
|
|
14625
16204
|
repoFallback: opts.repo,
|
|
14626
16205
|
exclusiveLogged: /* @__PURE__ */ new Map(),
|
|
16206
|
+
// Зона задачи из истории репо (TASWM-348) — предлагаем сами, если постановщик
|
|
16207
|
+
// её не проставил. Дедуп событий отдельной мапой, как у гейтов выше.
|
|
16208
|
+
suggestArea: async (run) => {
|
|
16209
|
+
const repo = normalizeRepo(run.repo || opts.repo || "");
|
|
16210
|
+
return repo ? suggestAreaForRun(await zoneModels.forRepo(repo), run) : null;
|
|
16211
|
+
},
|
|
16212
|
+
overlapLogged: /* @__PURE__ */ new Map(),
|
|
14627
16213
|
// Ротация своего лога (TASWM-330): демон пишет в него неделями, а до этого ротация была
|
|
14628
16214
|
// только при старте. Итог ротации печатаем в сам лог — первой строкой нового поколения
|
|
14629
16215
|
// видно, что история не потерялась, а уехала в `.1`.
|
|
@@ -14648,7 +16234,9 @@ async function runDaemon(opts) {
|
|
|
14648
16234
|
repoFallback: opts.repo,
|
|
14649
16235
|
worktreesDir,
|
|
14650
16236
|
launch,
|
|
14651
|
-
git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree }
|
|
16237
|
+
git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree },
|
|
16238
|
+
prHeadBranch
|
|
16239
|
+
// TASWM-356: ветка PR для прогонов без записанного поля branch
|
|
14652
16240
|
},
|
|
14653
16241
|
reviewRun
|
|
14654
16242
|
).then(() => {
|
|
@@ -14679,8 +16267,8 @@ async function runDaemon(opts) {
|
|
|
14679
16267
|
mode,
|
|
14680
16268
|
nudge
|
|
14681
16269
|
),
|
|
14682
|
-
worktreeExists: (p) =>
|
|
14683
|
-
hasTranscript:
|
|
16270
|
+
worktreeExists: (p) => existsSync16(p),
|
|
16271
|
+
hasTranscript: executorHasSession,
|
|
14684
16272
|
// CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
|
|
14685
16273
|
// (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
|
|
14686
16274
|
// строится над failing/failingRunIds ИЗ checks — без повторного `gh pr checks`.
|
|
@@ -14691,7 +16279,7 @@ async function runDaemon(opts) {
|
|
|
14691
16279
|
// Штормовой режим (TASWM-23): health-probe на отбой (реальный claude) + внешний
|
|
14692
16280
|
// сигнал SWARM_STORM_URL (2-й слой, если задан). Логика/пороги — в manageStorm/storm.ts.
|
|
14693
16281
|
storm: {
|
|
14694
|
-
probe: () =>
|
|
16282
|
+
probe: () => executorHealthy({ executor: EXECUTOR, timeoutMs: resolveStormConfig().probeTimeoutMs }),
|
|
14695
16283
|
// externalDown опционален (exactOptionalPropertyTypes): ключ есть только при
|
|
14696
16284
|
// заданном SWARM_STORM_URL, иначе опускаем — не передаём undefined.
|
|
14697
16285
|
...SWARM_STORM_URL ? { externalDown: () => fetchExternalStorm(SWARM_STORM_URL) } : {}
|
|
@@ -14837,11 +16425,33 @@ async function runDaemon(opts) {
|
|
|
14837
16425
|
);
|
|
14838
16426
|
return run ? { id: run.id } : null;
|
|
14839
16427
|
},
|
|
16428
|
+
// TASWM-351: «перезапустить с того же места» из продукта. Тот же вход, что у
|
|
16429
|
+
// `swarm wake <id>` и у POST /wake локальной морды, — сознательно, чтобы у
|
|
16430
|
+
// побудки была ОДНА реализация: она умеет отличать «сессию можно продолжить»
|
|
16431
|
+
// от «продолжать нечем» и не будит служебные прогоны вслепую (см. wakeRun).
|
|
16432
|
+
// Слаг → прогон резолвим тем же activeRunForSlug, что и остановка: у команды
|
|
16433
|
+
// из веба нет и не будет локального id прогона (он живёт только в этом процессе).
|
|
16434
|
+
wakeRun: async (slug) => {
|
|
16435
|
+
const run = db.activeRunForSlug(slug);
|
|
16436
|
+
if (!run) return null;
|
|
16437
|
+
return wakeRun({ db, isAlive: pidAlive, respawn: deps.respawn, narrate: narrator.narrate }, run.id);
|
|
16438
|
+
},
|
|
14840
16439
|
update: async (onStep) => {
|
|
14841
|
-
|
|
14842
|
-
|
|
16440
|
+
const pm = detectGlobalPm(process.argv[1]);
|
|
16441
|
+
if (pm !== "sources") {
|
|
16442
|
+
return runPackageSelfUpdate({
|
|
16443
|
+
currentVersion: PACKAGE_VERSION,
|
|
16444
|
+
installCommand: (spec) => globalInstallCommand(pm, spec),
|
|
16445
|
+
latest: () => fetchLatestVersion(),
|
|
16446
|
+
exec: execLoud,
|
|
16447
|
+
onStep
|
|
16448
|
+
});
|
|
16449
|
+
}
|
|
16450
|
+
const repo = await repoRoot(dirname4(THIS_FILE)).catch(() => null);
|
|
16451
|
+
if (!repo) {
|
|
16452
|
+
return { ok: false, step: "fetch", message: "\u0431\u0435\u0433\u0443 \u0438\u0437 \u0438\u0441\u0445\u043E\u0434\u043D\u0438\u043A\u043E\u0432, \u043D\u043E git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F \u043D\u0430\u0434 \u043D\u0438\u043C\u0438 \u043D\u0435\u0442 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E" };
|
|
14843
16453
|
}
|
|
14844
|
-
return runSelfUpdate(defaultSelfUpdateIO(
|
|
16454
|
+
return runSelfUpdate(defaultSelfUpdateIO(repo, onStep));
|
|
14845
16455
|
},
|
|
14846
16456
|
narrate: narrator.narrate
|
|
14847
16457
|
};
|
|
@@ -15047,7 +16657,9 @@ export {
|
|
|
15047
16657
|
REVIEW_CYCLE_ESCALATION_MARK,
|
|
15048
16658
|
REVIEW_CYCLE_LIMIT,
|
|
15049
16659
|
REVIEW_ESCALATION_MARK,
|
|
16660
|
+
REVIEW_PR_UNKNOWN_MARK,
|
|
15050
16661
|
REVIEW_SPAWN_LIMIT,
|
|
16662
|
+
REVIEW_UNRECOVERABLE_MARK,
|
|
15051
16663
|
SELF_REFERENCE_MARK,
|
|
15052
16664
|
STALL_FLOOR_MS,
|
|
15053
16665
|
STALL_MS,
|
|
@@ -15070,6 +16682,7 @@ export {
|
|
|
15070
16682
|
repoHasCiWorkflows,
|
|
15071
16683
|
resolveConflict,
|
|
15072
16684
|
respawnRun,
|
|
16685
|
+
reviewUnrecoverableMark,
|
|
15073
16686
|
reviveStuck,
|
|
15074
16687
|
runDaemon,
|
|
15075
16688
|
spawnReviewer,
|