@taskless-app/swarm 0.4.10 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -16
- package/dist/cli.js +3016 -806
- package/dist/overmind.js +2334 -573
- package/package.json +1 -1
package/dist/overmind.js
CHANGED
|
@@ -397,9 +397,14 @@ var init_windows = __esm({
|
|
|
397
397
|
});
|
|
398
398
|
|
|
399
399
|
// ../swarm-core/src/slug.ts
|
|
400
|
+
function isTaskSlug(s) {
|
|
401
|
+
return !!s && TASK_SLUG_RE.test(s);
|
|
402
|
+
}
|
|
403
|
+
var TASK_SLUG_RE;
|
|
400
404
|
var init_slug = __esm({
|
|
401
405
|
"../swarm-core/src/slug.ts"() {
|
|
402
406
|
"use strict";
|
|
407
|
+
TASK_SLUG_RE = /^(?:[A-Za-z0-9]{1,8}-\d+|#[A-Za-z0-9]{4,})$/;
|
|
403
408
|
}
|
|
404
409
|
});
|
|
405
410
|
|
|
@@ -432,7 +437,7 @@ var init_text = __esm({
|
|
|
432
437
|
"use strict";
|
|
433
438
|
ANSI_CSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
434
439
|
CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
|
|
435
|
-
EVENT_TEXT_MAX =
|
|
440
|
+
EVENT_TEXT_MAX = 16e3;
|
|
436
441
|
}
|
|
437
442
|
});
|
|
438
443
|
|
|
@@ -715,6 +720,14 @@ var init_hosts = __esm({
|
|
|
715
720
|
}
|
|
716
721
|
});
|
|
717
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
|
+
|
|
718
731
|
// ../swarm-core/src/hostCommands.ts
|
|
719
732
|
function isHostCommandKind(v) {
|
|
720
733
|
return typeof v === "string" && HOST_COMMAND_KINDS.includes(v);
|
|
@@ -724,17 +737,20 @@ function isHostCommandExpired(expiresAt, nowMs) {
|
|
|
724
737
|
const at = Date.parse(expiresAt);
|
|
725
738
|
return Number.isFinite(at) && at <= nowMs;
|
|
726
739
|
}
|
|
740
|
+
function isRunScopedCommand(kind) {
|
|
741
|
+
return RUN_SCOPED.has(kind);
|
|
742
|
+
}
|
|
727
743
|
function hostCommandLabel(cmd) {
|
|
728
744
|
const base3 = HOST_COMMAND_LABELS[cmd.kind];
|
|
729
745
|
if (cmd.kind === "set_max_drones" && cmd.maxDrones != null) return `${base3} \u2192 ${cmd.maxDrones}`;
|
|
730
|
-
if (cmd.kind
|
|
746
|
+
if (isRunScopedCommand(cmd.kind) && cmd.runSlug) return `${base3} ${cmd.runSlug}`;
|
|
731
747
|
return base3;
|
|
732
748
|
}
|
|
733
|
-
var HOST_COMMAND_KINDS, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, UPDATE_STEP_LABELS, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_LABELS, MAX_DRONES_LIMIT;
|
|
749
|
+
var HOST_COMMAND_KINDS, HOST_COMMAND_STATUSES, TERMINAL, HOST_COMMAND_OPEN_STATUSES, UPDATE_STEP_LABELS, HOST_COMMAND_TTL_MS, HOST_COMMAND_KEEPALIVE_MS, HOST_COMMAND_LABELS, RUN_SCOPED_COMMAND_KINDS, RUN_SCOPED, MAX_DRONES_LIMIT;
|
|
734
750
|
var init_hostCommands = __esm({
|
|
735
751
|
"../swarm-core/src/hostCommands.ts"() {
|
|
736
752
|
"use strict";
|
|
737
|
-
HOST_COMMAND_KINDS = ["pause", "resume", "restart", "update", "set_max_drones", "stop_run"];
|
|
753
|
+
HOST_COMMAND_KINDS = ["pause", "resume", "restart", "update", "set_max_drones", "stop_run", "wake_run"];
|
|
738
754
|
HOST_COMMAND_STATUSES = ["pending", "delivered", "running", "done", "failed", "rejected", "expired"];
|
|
739
755
|
TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "rejected", "expired"]);
|
|
740
756
|
HOST_COMMAND_OPEN_STATUSES = HOST_COMMAND_STATUSES.filter((s) => !TERMINAL.has(s));
|
|
@@ -753,8 +769,11 @@ var init_hostCommands = __esm({
|
|
|
753
769
|
restart: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A",
|
|
754
770
|
update: "\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u0434\u043E \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u043A\u043E\u0434\u0430",
|
|
755
771
|
set_max_drones: "\u0441\u043C\u0435\u043D\u0430 \u0447\u0438\u0441\u043B\u0430 \u0434\u0440\u043E\u043D\u043E\u0432",
|
|
756
|
-
stop_run: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430"
|
|
772
|
+
stop_run: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430",
|
|
773
|
+
wake_run: "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0441 \u0442\u043E\u0433\u043E \u0436\u0435 \u043C\u0435\u0441\u0442\u0430"
|
|
757
774
|
};
|
|
775
|
+
RUN_SCOPED_COMMAND_KINDS = ["stop_run", "wake_run"];
|
|
776
|
+
RUN_SCOPED = new Set(RUN_SCOPED_COMMAND_KINDS);
|
|
758
777
|
MAX_DRONES_LIMIT = 8;
|
|
759
778
|
}
|
|
760
779
|
});
|
|
@@ -769,6 +788,436 @@ var init_relief = __esm({
|
|
|
769
788
|
}
|
|
770
789
|
});
|
|
771
790
|
|
|
791
|
+
// ../swarm-core/src/cost.ts
|
|
792
|
+
var EMPTY_TASK_COST;
|
|
793
|
+
var init_cost = __esm({
|
|
794
|
+
"../swarm-core/src/cost.ts"() {
|
|
795
|
+
"use strict";
|
|
796
|
+
EMPTY_TASK_COST = Object.freeze({
|
|
797
|
+
costUsd: 0,
|
|
798
|
+
tokensIn: 0,
|
|
799
|
+
tokensOut: 0,
|
|
800
|
+
durationMs: 0,
|
|
801
|
+
runs: 0,
|
|
802
|
+
respawns: 0
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// ../swarm-core/src/runGuards.ts
|
|
808
|
+
var DEFAULT_RUN_LIMITS;
|
|
809
|
+
var init_runGuards = __esm({
|
|
810
|
+
"../swarm-core/src/runGuards.ts"() {
|
|
811
|
+
"use strict";
|
|
812
|
+
DEFAULT_RUN_LIMITS = {
|
|
813
|
+
maxCostUsd: 20,
|
|
814
|
+
runTimeoutMs: 8 * 60 * 6e4,
|
|
815
|
+
stepTimeoutMs: 90 * 6e4,
|
|
816
|
+
lostMs: 30 * 6e4
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
// ../swarm-core/src/codeZones.ts
|
|
822
|
+
function hasZoneModel(m) {
|
|
823
|
+
return m.tasks > 0 && m.zones.length > 0;
|
|
824
|
+
}
|
|
825
|
+
function testFileFeature(basename5) {
|
|
826
|
+
const head = basename5.replace(/\.(test|spec)\.[a-z]+$/i, "").split(".")[0].toLowerCase();
|
|
827
|
+
return head && !GENERIC_DIRS.has(head) ? head : null;
|
|
828
|
+
}
|
|
829
|
+
function pathZoneCandidate(path) {
|
|
830
|
+
const parts = path.split("/").filter(Boolean);
|
|
831
|
+
if (parts.length <= 1) return "root";
|
|
832
|
+
if (WORKSPACE_DIRS.has(parts[0]) && parts.length >= 3) {
|
|
833
|
+
const pkg = parts[1];
|
|
834
|
+
if (parts[2] === "src" && parts.length >= 5) {
|
|
835
|
+
const feature = parts[3];
|
|
836
|
+
if (!GENERIC_DIRS.has(feature)) return feature;
|
|
837
|
+
}
|
|
838
|
+
if (GENERIC_TEST_DIRS.has(parts[2])) {
|
|
839
|
+
const feature = testFileFeature(parts[parts.length - 1]);
|
|
840
|
+
if (feature) return feature;
|
|
841
|
+
}
|
|
842
|
+
return pkg;
|
|
843
|
+
}
|
|
844
|
+
if (parts[0] === "src" && parts.length >= 3 && !GENERIC_DIRS.has(parts[1])) return parts[1];
|
|
845
|
+
return parts[0];
|
|
846
|
+
}
|
|
847
|
+
function dirOf(path) {
|
|
848
|
+
const i = path.lastIndexOf("/");
|
|
849
|
+
return i < 0 ? "" : path.slice(0, i);
|
|
850
|
+
}
|
|
851
|
+
function tokenize(text) {
|
|
852
|
+
const out = [];
|
|
853
|
+
for (const raw of text.split(/[^\p{L}\p{N}]+/u)) {
|
|
854
|
+
if (!raw) continue;
|
|
855
|
+
for (const piece of raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ")) {
|
|
856
|
+
const t = piece.toLowerCase();
|
|
857
|
+
if (t.length < 3 || STOP_TERMS.has(t) || /^\d+$/.test(t)) continue;
|
|
858
|
+
out.push(t);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return out;
|
|
862
|
+
}
|
|
863
|
+
function normalizeZoneName(raw) {
|
|
864
|
+
if (!raw) return null;
|
|
865
|
+
const s = raw.trim().toLowerCase().replace(/^area:/, "");
|
|
866
|
+
return s || null;
|
|
867
|
+
}
|
|
868
|
+
function bump(m, k, by = 1) {
|
|
869
|
+
m.set(k, (m.get(k) ?? 0) + by);
|
|
870
|
+
}
|
|
871
|
+
function topKey(m) {
|
|
872
|
+
let best = null;
|
|
873
|
+
let bestN = 0;
|
|
874
|
+
let total = 0;
|
|
875
|
+
for (const [k, n] of m) {
|
|
876
|
+
total += n;
|
|
877
|
+
if (n > bestN) {
|
|
878
|
+
best = k;
|
|
879
|
+
bestN = n;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return best === null || total === 0 ? null : { key: best, count: bestN, share: bestN / total };
|
|
883
|
+
}
|
|
884
|
+
function buildZoneModel(entries, opts = {}) {
|
|
885
|
+
const o = { ...DEFAULTS, ...opts };
|
|
886
|
+
const tasks = entries.filter((e) => e.files.length > 0);
|
|
887
|
+
if (tasks.length < o.minTasks) return EMPTY_ZONE_MODEL;
|
|
888
|
+
const fileTasks = /* @__PURE__ */ new Map();
|
|
889
|
+
const candidate = /* @__PURE__ */ new Map();
|
|
890
|
+
for (const t of tasks) {
|
|
891
|
+
for (const f of new Set(t.files)) {
|
|
892
|
+
bump(fileTasks, f);
|
|
893
|
+
if (!candidate.has(f)) candidate.set(f, pathZoneCandidate(f));
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const zoneOf = new Map(candidate);
|
|
897
|
+
for (const [file, seen] of fileTasks) {
|
|
898
|
+
if (seen < 2) continue;
|
|
899
|
+
const votes = /* @__PURE__ */ new Map();
|
|
900
|
+
for (const t of tasks) {
|
|
901
|
+
const files = [...new Set(t.files)];
|
|
902
|
+
if (!files.includes(file)) continue;
|
|
903
|
+
const others = files.filter((f) => f !== file);
|
|
904
|
+
if (!others.length) continue;
|
|
905
|
+
for (const f of others) bump(votes, candidate.get(f), 1 / others.length);
|
|
906
|
+
}
|
|
907
|
+
const top = topKey(votes);
|
|
908
|
+
if (top && top.share >= 0.6 && top.key !== candidate.get(file)) zoneOf.set(file, top.key);
|
|
909
|
+
}
|
|
910
|
+
const voteWeight = (file) => 1 / Math.log2(2 + (fileTasks.get(file) ?? 1));
|
|
911
|
+
const taskZonePre = /* @__PURE__ */ new Map();
|
|
912
|
+
for (const t of tasks) {
|
|
913
|
+
const votes = /* @__PURE__ */ new Map();
|
|
914
|
+
for (const f of new Set(t.files)) bump(votes, zoneOf.get(f), voteWeight(f));
|
|
915
|
+
const top = topKey(votes);
|
|
916
|
+
if (top) taskZonePre.set(t.slug, top.key);
|
|
917
|
+
}
|
|
918
|
+
const fileZones = /* @__PURE__ */ new Map();
|
|
919
|
+
for (const t of tasks) {
|
|
920
|
+
const tz = taskZonePre.get(t.slug);
|
|
921
|
+
if (!tz) continue;
|
|
922
|
+
for (const f of new Set(t.files)) {
|
|
923
|
+
let m = fileZones.get(f);
|
|
924
|
+
if (!m) fileZones.set(f, m = /* @__PURE__ */ new Map());
|
|
925
|
+
bump(m, tz);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
const spineSet = /* @__PURE__ */ new Set();
|
|
929
|
+
for (const [file, seen] of fileTasks) {
|
|
930
|
+
if (seen < o.spineMinTasks) continue;
|
|
931
|
+
const zonesOfFile = fileZones.get(file);
|
|
932
|
+
const dominant = zonesOfFile ? topKey(zonesOfFile) : null;
|
|
933
|
+
if ((zonesOfFile?.size ?? 0) < o.spineMinZones) continue;
|
|
934
|
+
if (dominant && dominant.share >= o.spineDominantShare) continue;
|
|
935
|
+
spineSet.add(file);
|
|
936
|
+
}
|
|
937
|
+
const fileZone = /* @__PURE__ */ new Map();
|
|
938
|
+
for (const [f, z2] of zoneOf) if (!spineSet.has(f)) fileZone.set(f, z2);
|
|
939
|
+
const taskZone = /* @__PURE__ */ new Map();
|
|
940
|
+
for (const t of tasks) {
|
|
941
|
+
const votes = /* @__PURE__ */ new Map();
|
|
942
|
+
for (const f of new Set(t.files)) {
|
|
943
|
+
const z2 = fileZone.get(f);
|
|
944
|
+
if (z2) bump(votes, z2, voteWeight(f));
|
|
945
|
+
}
|
|
946
|
+
const top = topKey(votes);
|
|
947
|
+
taskZone.set(t.slug, top ? top.key : SPINE_ZONE);
|
|
948
|
+
}
|
|
949
|
+
const labelVotes = /* @__PURE__ */ new Map();
|
|
950
|
+
for (const t of tasks) {
|
|
951
|
+
const manual = normalizeZoneName(t.area);
|
|
952
|
+
const z2 = taskZone.get(t.slug);
|
|
953
|
+
if (!manual || !z2 || z2 === SPINE_ZONE || manual === SPINE_ZONE) continue;
|
|
954
|
+
let m = labelVotes.get(z2);
|
|
955
|
+
if (!m) labelVotes.set(z2, m = /* @__PURE__ */ new Map());
|
|
956
|
+
bump(m, manual);
|
|
957
|
+
}
|
|
958
|
+
const rename = /* @__PURE__ */ new Map();
|
|
959
|
+
const takenNames = /* @__PURE__ */ new Set([SPINE_ZONE]);
|
|
960
|
+
const clustersByEvidence = [...labelVotes.entries()].sort((a, b) => {
|
|
961
|
+
const sum = (m) => [...m.values()].reduce((x, y) => x + y, 0);
|
|
962
|
+
return sum(b[1]) - sum(a[1]) || a[0].localeCompare(b[0]);
|
|
963
|
+
});
|
|
964
|
+
for (const [zone, votes] of clustersByEvidence) {
|
|
965
|
+
const top = topKey(votes);
|
|
966
|
+
if (!top || top.share < 0.6 || takenNames.has(top.key) || top.key === zone) continue;
|
|
967
|
+
rename.set(zone, top.key);
|
|
968
|
+
takenNames.add(top.key);
|
|
969
|
+
}
|
|
970
|
+
const renamed = (z2) => rename.get(z2) ?? z2;
|
|
971
|
+
if (rename.size) {
|
|
972
|
+
for (const [f, z2] of fileZone) fileZone.set(f, renamed(z2));
|
|
973
|
+
for (const [s, z2] of taskZone) taskZone.set(s, renamed(z2));
|
|
974
|
+
}
|
|
975
|
+
const zoneTerms = /* @__PURE__ */ new Map();
|
|
976
|
+
const addTerm = (zone, term, w) => {
|
|
977
|
+
let m = zoneTerms.get(zone);
|
|
978
|
+
if (!m) zoneTerms.set(zone, m = /* @__PURE__ */ new Map());
|
|
979
|
+
bump(m, term, w);
|
|
980
|
+
};
|
|
981
|
+
for (const [f, z2] of fileZone) for (const term of tokenize(f)) addTerm(z2, term, 1);
|
|
982
|
+
for (const f of spineSet) for (const term of tokenize(f)) addTerm(SPINE_ZONE, term, 1);
|
|
983
|
+
for (const t of tasks) {
|
|
984
|
+
const z2 = taskZone.get(t.slug);
|
|
985
|
+
if (!z2 || !t.text) continue;
|
|
986
|
+
for (const term of tokenize(t.text)) addTerm(z2, term, 2);
|
|
987
|
+
}
|
|
988
|
+
const termZones = /* @__PURE__ */ new Map();
|
|
989
|
+
for (const terms of zoneTerms.values()) for (const term of terms.keys()) bump(termZones, term);
|
|
990
|
+
const zoneFiles = /* @__PURE__ */ new Map();
|
|
991
|
+
for (const [f, z2] of fileZone) {
|
|
992
|
+
const arr = zoneFiles.get(z2) ?? [];
|
|
993
|
+
arr.push(f);
|
|
994
|
+
zoneFiles.set(z2, arr);
|
|
995
|
+
}
|
|
996
|
+
const zoneTaskCount = /* @__PURE__ */ new Map();
|
|
997
|
+
for (const z2 of taskZone.values()) bump(zoneTaskCount, z2);
|
|
998
|
+
const byFreq = (a, b) => (fileTasks.get(b) ?? 0) - (fileTasks.get(a) ?? 0) || a.localeCompare(b);
|
|
999
|
+
const zones = [...zoneFiles.entries()].map(([zone, files]) => ({ zone, tasks: zoneTaskCount.get(zone) ?? 0, files: files.sort(byFreq) })).sort((a, b) => b.tasks - a.tasks || a.zone.localeCompare(b.zone));
|
|
1000
|
+
const spine = [...spineSet].sort(byFreq);
|
|
1001
|
+
if (spineSet.size) zones.push({ zone: SPINE_ZONE, tasks: zoneTaskCount.get(SPINE_ZONE) ?? 0, files: spine });
|
|
1002
|
+
const entryTerms = tasks.map((t) => new Set(t.text ? tokenize(t.text) : []));
|
|
1003
|
+
return { tasks: tasks.length, zones, fileZone, spine, taskZone, zoneTerms, termZones, entries: tasks, entryTerms };
|
|
1004
|
+
}
|
|
1005
|
+
function zoneForFiles(model, files) {
|
|
1006
|
+
if (!hasZoneModel(model) || !files.length) return null;
|
|
1007
|
+
const spineSet = new Set(model.spine);
|
|
1008
|
+
const touchedSpine = files.filter((f) => spineSet.has(f));
|
|
1009
|
+
const votes = /* @__PURE__ */ new Map();
|
|
1010
|
+
for (const f of new Set(files)) {
|
|
1011
|
+
const z2 = model.fileZone.get(f) ?? (spineSet.has(f) ? null : pathZoneCandidate(f));
|
|
1012
|
+
if (z2) bump(votes, z2);
|
|
1013
|
+
}
|
|
1014
|
+
const top = topKey(votes);
|
|
1015
|
+
if (!top) {
|
|
1016
|
+
return {
|
|
1017
|
+
zone: SPINE_ZONE,
|
|
1018
|
+
confidence: 1,
|
|
1019
|
+
why: `\u0432\u0441\u0435 \u0444\u0430\u0439\u043B\u044B \u2014 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${touchedSpine.slice(0, 3).join(", ")})`,
|
|
1020
|
+
spine: true
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
zone: top.key,
|
|
1025
|
+
confidence: top.share,
|
|
1026
|
+
why: `${top.count} \u0438\u0437 ${[...votes.values()].reduce((a, b) => a + b, 0)} \u0444\u0430\u0439\u043B\u043E\u0432 \u2014 \u0437\u043E\u043D\u0430 ${top.key}`,
|
|
1027
|
+
spine: touchedSpine.length > 0
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function suggestZonesFromText(model, text, limit = 3) {
|
|
1031
|
+
if (!hasZoneModel(model)) return [];
|
|
1032
|
+
const terms = tokenize(text);
|
|
1033
|
+
if (!terms.length) return [];
|
|
1034
|
+
const zoneCount = model.zoneTerms.size || 1;
|
|
1035
|
+
const idf = (term) => Math.log(1 + zoneCount / (model.termZones.get(term) ?? 1));
|
|
1036
|
+
const scores = /* @__PURE__ */ new Map();
|
|
1037
|
+
const hits = /* @__PURE__ */ new Map();
|
|
1038
|
+
const tasksOfZone = new Map(model.zones.map((z2) => [z2.zone, z2.tasks]));
|
|
1039
|
+
for (const [zone, dict] of model.zoneTerms) {
|
|
1040
|
+
if ((tasksOfZone.get(zone) ?? 0) < 2) continue;
|
|
1041
|
+
let dot = 0;
|
|
1042
|
+
let norm = 0;
|
|
1043
|
+
const matched = [];
|
|
1044
|
+
for (const [term, w] of dict) {
|
|
1045
|
+
const v = Math.log(1 + w) * idf(term);
|
|
1046
|
+
norm += v * v;
|
|
1047
|
+
}
|
|
1048
|
+
if (!norm) continue;
|
|
1049
|
+
for (const term of new Set(terms)) {
|
|
1050
|
+
const w = dict.get(term);
|
|
1051
|
+
if (!w) continue;
|
|
1052
|
+
dot += Math.log(1 + w) * idf(term) * idf(term);
|
|
1053
|
+
matched.push(term);
|
|
1054
|
+
}
|
|
1055
|
+
const score = dot / Math.sqrt(norm) * Math.log(1 + (tasksOfZone.get(zone) ?? 1));
|
|
1056
|
+
if (score > 0) {
|
|
1057
|
+
scores.set(zone, score);
|
|
1058
|
+
hits.set(zone, matched);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const total = [...scores.values()].reduce((a, b) => a + b, 0);
|
|
1062
|
+
if (!total) return [];
|
|
1063
|
+
return [...scores.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([zone, s]) => ({
|
|
1064
|
+
zone,
|
|
1065
|
+
confidence: s / total,
|
|
1066
|
+
why: `\u0441\u043E\u0432\u043F\u0430\u043B\u043E \u043F\u043E \u0441\u043B\u043E\u0432\u0430\u043C: ${(hits.get(zone) ?? []).slice(0, 5).join(", ")}`,
|
|
1067
|
+
spine: zone === SPINE_ZONE
|
|
1068
|
+
}));
|
|
1069
|
+
}
|
|
1070
|
+
function similarTasks(model, text, limit = 5) {
|
|
1071
|
+
const terms = new Set(tokenize(text));
|
|
1072
|
+
if (!terms.size) return [];
|
|
1073
|
+
const scored = [];
|
|
1074
|
+
for (const [i, e] of model.entries.entries()) {
|
|
1075
|
+
const other = model.entryTerms[i];
|
|
1076
|
+
if (!other?.size) continue;
|
|
1077
|
+
let inter = 0;
|
|
1078
|
+
for (const t of terms) if (other.has(t)) inter += Math.log(1 + (model.zoneTerms.size || 1) / (model.termZones.get(t) ?? 1));
|
|
1079
|
+
if (inter <= 0) continue;
|
|
1080
|
+
scored.push({ entry: e, score: inter / Math.sqrt(terms.size * other.size) });
|
|
1081
|
+
}
|
|
1082
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1083
|
+
}
|
|
1084
|
+
function predictFiles(model, opts) {
|
|
1085
|
+
if (!hasZoneModel(model)) return [];
|
|
1086
|
+
const limit = opts.limit ?? 40;
|
|
1087
|
+
const best = /* @__PURE__ */ new Map();
|
|
1088
|
+
const put = (path, weight, why) => {
|
|
1089
|
+
const cur = best.get(path);
|
|
1090
|
+
if (!cur || cur.weight < weight) best.set(path, { path, weight, why });
|
|
1091
|
+
};
|
|
1092
|
+
if (opts.text) {
|
|
1093
|
+
const sim = similarTasks(model, opts.text, opts.similar ?? 5);
|
|
1094
|
+
const topScore = sim[0]?.score ?? 0;
|
|
1095
|
+
for (const { entry, score } of sim) {
|
|
1096
|
+
const w = topScore > 0 ? Math.min(1, score / topScore) : 0;
|
|
1097
|
+
for (const f of new Set(entry.files)) put(f, Math.max(0.35, w * 0.9), `\u043F\u043E\u0445\u043E\u0436\u0430\u044F \u0437\u0430\u0434\u0430\u0447\u0430 ${entry.slug}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
const zone = normalizeZoneName(opts.zone) ?? (opts.text ? suggestZonesFromText(model, opts.text)[0]?.zone ?? null : null);
|
|
1101
|
+
if (zone) {
|
|
1102
|
+
const stat = model.zones.find((z2) => z2.zone === zone);
|
|
1103
|
+
if (stat) stat.files.slice(0, limit).forEach((f, i) => put(f, Math.max(0.12, 0.45 - i * 0.02), `\u0437\u043E\u043D\u0430 ${zone}`));
|
|
1104
|
+
}
|
|
1105
|
+
return [...best.values()].sort((a, b) => b.weight - a.weight || a.path.localeCompare(b.path)).slice(0, limit);
|
|
1106
|
+
}
|
|
1107
|
+
function overlapRisk(predicted, actual, opts = {}) {
|
|
1108
|
+
const o = { ...OVERLAP_DEFAULTS, ...opts };
|
|
1109
|
+
const actualSet = new Set(actual);
|
|
1110
|
+
const actualDirs = new Set([...actualSet].map(dirOf).filter(Boolean));
|
|
1111
|
+
const totalWeight = predicted.reduce((a, p) => a + p.weight, 0);
|
|
1112
|
+
if (!totalWeight) return { overlap: false, score: 0, sharedFiles: [], sharedDirs: [] };
|
|
1113
|
+
let hit = 0;
|
|
1114
|
+
let strong = false;
|
|
1115
|
+
const sharedFiles = [];
|
|
1116
|
+
const sharedDirs = /* @__PURE__ */ new Set();
|
|
1117
|
+
for (const p of predicted) {
|
|
1118
|
+
if (actualSet.has(p.path)) {
|
|
1119
|
+
hit += p.weight;
|
|
1120
|
+
sharedFiles.push(p.path);
|
|
1121
|
+
if (p.weight >= o.strongFile) strong = true;
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
const d = dirOf(p.path);
|
|
1125
|
+
if (d && actualDirs.has(d)) {
|
|
1126
|
+
hit += p.weight * 0.25;
|
|
1127
|
+
sharedDirs.add(d);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const score = Math.min(1, hit / totalWeight);
|
|
1131
|
+
return {
|
|
1132
|
+
overlap: strong || score >= o.threshold,
|
|
1133
|
+
score,
|
|
1134
|
+
sharedFiles: sharedFiles.slice(0, 10),
|
|
1135
|
+
sharedDirs: [...sharedDirs].slice(0, 5)
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
var SPINE_ZONE, DEFAULTS, EMPTY_ZONE_MODEL, GENERIC_DIRS, WORKSPACE_DIRS, GENERIC_TEST_DIRS, STOP_TERMS, OVERLAP_DEFAULTS;
|
|
1139
|
+
var init_codeZones = __esm({
|
|
1140
|
+
"../swarm-core/src/codeZones.ts"() {
|
|
1141
|
+
"use strict";
|
|
1142
|
+
SPINE_ZONE = "core";
|
|
1143
|
+
DEFAULTS = {
|
|
1144
|
+
minTasks: 5,
|
|
1145
|
+
spineMinTasks: 3,
|
|
1146
|
+
spineMinZones: 3,
|
|
1147
|
+
spineDominantShare: 0.6
|
|
1148
|
+
};
|
|
1149
|
+
EMPTY_ZONE_MODEL = {
|
|
1150
|
+
tasks: 0,
|
|
1151
|
+
zones: [],
|
|
1152
|
+
fileZone: /* @__PURE__ */ new Map(),
|
|
1153
|
+
spine: [],
|
|
1154
|
+
taskZone: /* @__PURE__ */ new Map(),
|
|
1155
|
+
zoneTerms: /* @__PURE__ */ new Map(),
|
|
1156
|
+
termZones: /* @__PURE__ */ new Map(),
|
|
1157
|
+
entries: [],
|
|
1158
|
+
entryTerms: []
|
|
1159
|
+
};
|
|
1160
|
+
GENERIC_DIRS = /* @__PURE__ */ new Set([
|
|
1161
|
+
"src",
|
|
1162
|
+
"source",
|
|
1163
|
+
"lib",
|
|
1164
|
+
"libs",
|
|
1165
|
+
"util",
|
|
1166
|
+
"utils",
|
|
1167
|
+
"common",
|
|
1168
|
+
"internal",
|
|
1169
|
+
"app",
|
|
1170
|
+
"test",
|
|
1171
|
+
"tests",
|
|
1172
|
+
"__tests__",
|
|
1173
|
+
"spec",
|
|
1174
|
+
"specs",
|
|
1175
|
+
"index"
|
|
1176
|
+
]);
|
|
1177
|
+
WORKSPACE_DIRS = /* @__PURE__ */ new Set(["packages", "apps", "services", "libs", "modules"]);
|
|
1178
|
+
GENERIC_TEST_DIRS = /* @__PURE__ */ new Set(["tests", "test", "__tests__", "spec"]);
|
|
1179
|
+
STOP_TERMS = /* @__PURE__ */ new Set([
|
|
1180
|
+
"the",
|
|
1181
|
+
"and",
|
|
1182
|
+
"for",
|
|
1183
|
+
"with",
|
|
1184
|
+
"from",
|
|
1185
|
+
"into",
|
|
1186
|
+
"src",
|
|
1187
|
+
"test",
|
|
1188
|
+
"tests",
|
|
1189
|
+
"index",
|
|
1190
|
+
"packages",
|
|
1191
|
+
"package",
|
|
1192
|
+
"json",
|
|
1193
|
+
"yml",
|
|
1194
|
+
"yaml",
|
|
1195
|
+
"tsx",
|
|
1196
|
+
"mjs",
|
|
1197
|
+
"cjs",
|
|
1198
|
+
"\u0447\u0442\u043E",
|
|
1199
|
+
"\u043A\u0430\u043A",
|
|
1200
|
+
"\u0434\u043B\u044F",
|
|
1201
|
+
"\u043F\u0440\u0438",
|
|
1202
|
+
"\u044D\u0442\u043E",
|
|
1203
|
+
"\u0438\u043B\u0438",
|
|
1204
|
+
"\u0435\u0441\u043B\u0438",
|
|
1205
|
+
"\u0447\u0442\u043E\u0431\u044B",
|
|
1206
|
+
"\u0442\u043E\u0436\u0435",
|
|
1207
|
+
"\u0435\u0433\u043E",
|
|
1208
|
+
"\u0432\u0441\u0435",
|
|
1209
|
+
"feat",
|
|
1210
|
+
"fix",
|
|
1211
|
+
"chore",
|
|
1212
|
+
"docs",
|
|
1213
|
+
"refactor",
|
|
1214
|
+
"\u043F\u0435\u0440\u0444",
|
|
1215
|
+
"wip"
|
|
1216
|
+
]);
|
|
1217
|
+
OVERLAP_DEFAULTS = { threshold: 0.2, strongFile: 0.35 };
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
772
1221
|
// ../swarm-core/src/index.ts
|
|
773
1222
|
var init_src = __esm({
|
|
774
1223
|
"../swarm-core/src/index.ts"() {
|
|
@@ -785,14 +1234,492 @@ var init_src = __esm({
|
|
|
785
1234
|
init_names();
|
|
786
1235
|
init_baseBranch();
|
|
787
1236
|
init_hosts();
|
|
1237
|
+
init_hostPool();
|
|
788
1238
|
init_hostCommands();
|
|
789
1239
|
init_relief();
|
|
1240
|
+
init_cost();
|
|
1241
|
+
init_runGuards();
|
|
1242
|
+
init_codeZones();
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
// src/claudeBin.ts
|
|
1247
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
1248
|
+
import { win32 as winPath } from "path";
|
|
1249
|
+
function compareVersionDirs(a, b) {
|
|
1250
|
+
const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
|
|
1251
|
+
const pa = parts(a);
|
|
1252
|
+
const pb = parts(b);
|
|
1253
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1254
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1255
|
+
if (d !== 0) return d;
|
|
1256
|
+
}
|
|
1257
|
+
return 0;
|
|
1258
|
+
}
|
|
1259
|
+
function realListDir(dir) {
|
|
1260
|
+
try {
|
|
1261
|
+
return readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1262
|
+
} catch {
|
|
1263
|
+
return [];
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function winVersionRoots(env, listDir) {
|
|
1267
|
+
const roots = [];
|
|
1268
|
+
const appData = env.APPDATA;
|
|
1269
|
+
if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
|
|
1270
|
+
const localAppData = env.LOCALAPPDATA;
|
|
1271
|
+
if (localAppData) {
|
|
1272
|
+
const packages = winPath.join(localAppData, "Packages");
|
|
1273
|
+
for (const name of listDir(packages)) {
|
|
1274
|
+
if (!name.startsWith("Claude")) continue;
|
|
1275
|
+
roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return roots;
|
|
1279
|
+
}
|
|
1280
|
+
function findWindowsClaude(opts = {}) {
|
|
1281
|
+
const env = opts.env ?? process.env;
|
|
1282
|
+
const listDir = opts.listDir ?? realListDir;
|
|
1283
|
+
const fileExists = opts.fileExists ?? existsSync2;
|
|
1284
|
+
let best = null;
|
|
1285
|
+
for (const root of winVersionRoots(env, listDir)) {
|
|
1286
|
+
for (const version of listDir(root)) {
|
|
1287
|
+
const candidate = winPath.join(root, version, "claude.exe");
|
|
1288
|
+
if (!fileExists(candidate)) continue;
|
|
1289
|
+
if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return best?.path ?? null;
|
|
1293
|
+
}
|
|
1294
|
+
function resolveClaudeBin(opts = {}) {
|
|
1295
|
+
const env = opts.env ?? process.env;
|
|
1296
|
+
const platform = opts.platform ?? process.platform;
|
|
1297
|
+
const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
|
|
1298
|
+
if (explicit) return explicit;
|
|
1299
|
+
if (platform !== "win32") return CLAUDE_FALLBACK;
|
|
1300
|
+
if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
|
|
1301
|
+
windowsScanDone = true;
|
|
1302
|
+
const found = findWindowsClaude({ ...opts, env });
|
|
1303
|
+
cachedWindowsBin = found;
|
|
1304
|
+
if (found) return found;
|
|
1305
|
+
if (!warnedNoWindowsClaude) {
|
|
1306
|
+
warnedNoWindowsClaude = true;
|
|
1307
|
+
console.warn(
|
|
1308
|
+
"[overmind] claude.exe \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u043D\u0438 \u0432 %APPDATA%\\Claude\\claude-code, \u043D\u0438 \u0432 MSIX-\u043A\u043E\u043F\u0438\u0438 \u043F\u043E\u0434 %LOCALAPPDATA%\\Packages\\Claude* \u2014 \u0441\u043F\u0430\u0432\u043D\u044E \u0433\u043E\u043B\u043E\u0435 \xABclaude\xBB (\u043F\u043E\u0438\u0441\u043A \u043F\u043E PATH+PATHEXT). \u0423\u043F\u0430\u0434\u0451\u0442 ENOENT (bash-\u043E\u0431\u0451\u0440\u0442\u043A\u0430 \u0431\u0435\u0437 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F \u0432 PATH \u043D\u0435 \u0432\u0438\u0434\u043D\u0430 spawn'\u0443) \u2014 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 SWARM_CLAUDE_BIN \u043F\u0443\u0442\u0451\u043C \u043A \u043D\u0430\u0441\u0442\u043E\u044F\u0449\u0435\u043C\u0443 claude.exe."
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
return CLAUDE_FALLBACK;
|
|
1312
|
+
}
|
|
1313
|
+
var CLAUDE_FALLBACK, warnedNoWindowsClaude, cachedWindowsBin, windowsScanDone;
|
|
1314
|
+
var init_claudeBin = __esm({
|
|
1315
|
+
"src/claudeBin.ts"() {
|
|
1316
|
+
"use strict";
|
|
1317
|
+
CLAUDE_FALLBACK = "claude";
|
|
1318
|
+
warnedNoWindowsClaude = false;
|
|
1319
|
+
cachedWindowsBin = null;
|
|
1320
|
+
windowsScanDone = false;
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
// src/pricing.ts
|
|
1325
|
+
function costFromUsage(usage, model, table = MODEL_PRICING) {
|
|
1326
|
+
const pricing = model ? table[model] : void 0;
|
|
1327
|
+
if (!pricing) {
|
|
1328
|
+
return { costUsd: 0, warning: `\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043C\u043E\u0434\u0435\u043B\u044C \u0434\u043B\u044F \u043F\u0440\u0430\u0439\u0441\u0438\u043D\u0433\u0430: "${model ?? ""}" \u2014 cost \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D` };
|
|
1329
|
+
}
|
|
1330
|
+
const input = usage.input_tokens ?? 0;
|
|
1331
|
+
const output = usage.output_tokens ?? 0;
|
|
1332
|
+
const cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
1333
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
1334
|
+
const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
|
|
1335
|
+
return { costUsd };
|
|
1336
|
+
}
|
|
1337
|
+
var MODEL_PRICING, CACHE_READ_FACTOR, CACHE_WRITE_5M_FACTOR;
|
|
1338
|
+
var init_pricing = __esm({
|
|
1339
|
+
"src/pricing.ts"() {
|
|
1340
|
+
"use strict";
|
|
1341
|
+
MODEL_PRICING = {
|
|
1342
|
+
// Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
|
|
1343
|
+
// Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
|
|
1344
|
+
// ветвление по времени внутри расчёта стоимости.
|
|
1345
|
+
"claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
|
|
1346
|
+
"claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1347
|
+
// Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
|
|
1348
|
+
"claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
1349
|
+
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
|
|
1350
|
+
};
|
|
1351
|
+
CACHE_READ_FACTOR = 0.1;
|
|
1352
|
+
CACHE_WRITE_5M_FACTOR = 1.25;
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
// src/claudeResult.ts
|
|
1357
|
+
function parseClaudeResultJson(raw) {
|
|
1358
|
+
const trimmed = raw.trim();
|
|
1359
|
+
if (!trimmed) return null;
|
|
1360
|
+
try {
|
|
1361
|
+
const parsed = JSON.parse(trimmed);
|
|
1362
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
1363
|
+
} catch {
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function launchCostUsd(result, model) {
|
|
1368
|
+
if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
|
|
1369
|
+
return { costUsd: result.total_cost_usd };
|
|
1370
|
+
}
|
|
1371
|
+
if (!result.usage) return { costUsd: 0 };
|
|
1372
|
+
return costFromUsage(result.usage, model);
|
|
1373
|
+
}
|
|
1374
|
+
function launchSpend(result) {
|
|
1375
|
+
const u = result.usage ?? {};
|
|
1376
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
1377
|
+
return {
|
|
1378
|
+
tokensIn: num(u.input_tokens) + num(u.cache_creation_input_tokens) + num(u.cache_read_input_tokens),
|
|
1379
|
+
tokensOut: num(u.output_tokens),
|
|
1380
|
+
durationMs: num(result.duration_ms)
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
var init_claudeResult = __esm({
|
|
1384
|
+
"src/claudeResult.ts"() {
|
|
1385
|
+
"use strict";
|
|
1386
|
+
init_pricing();
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
// src/executors/claudeCode.ts
|
|
1391
|
+
import { readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1392
|
+
import { homedir } from "os";
|
|
1393
|
+
import { join as join2 } from "path";
|
|
1394
|
+
function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
|
|
1395
|
+
const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
|
|
1396
|
+
const base3 = [
|
|
1397
|
+
"--dangerously-skip-permissions",
|
|
1398
|
+
...ISOLATION_FLAGS,
|
|
1399
|
+
"--mcp-config",
|
|
1400
|
+
cfgPath,
|
|
1401
|
+
...settings ? ["--settings", settings] : [],
|
|
1402
|
+
...pluginFlags,
|
|
1403
|
+
"--output-format",
|
|
1404
|
+
"json",
|
|
1405
|
+
"-p",
|
|
1406
|
+
prompt
|
|
1407
|
+
];
|
|
1408
|
+
const withModel = model ? ["--model", model, ...base3] : base3;
|
|
1409
|
+
return continueSession ? ["--continue", ...withModel] : withModel;
|
|
1410
|
+
}
|
|
1411
|
+
function droneDenyRules(paths) {
|
|
1412
|
+
const abs = (p) => `//${p.replace(/^\/+/, "")}`;
|
|
1413
|
+
return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
|
|
1414
|
+
}
|
|
1415
|
+
function droneSettingsJson(paths) {
|
|
1416
|
+
return JSON.stringify({ permissions: { deny: droneDenyRules(paths) } });
|
|
1417
|
+
}
|
|
1418
|
+
function transcriptDir(worktree, projectsDir) {
|
|
1419
|
+
return join2(projectsDir, worktree.replace(/[/.]/g, "-"));
|
|
1420
|
+
}
|
|
1421
|
+
function sessionTranscriptExists(worktree, projectsDir = join2(homedir(), ".claude", "projects")) {
|
|
1422
|
+
try {
|
|
1423
|
+
return readdirSync2(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
|
|
1424
|
+
} catch {
|
|
1425
|
+
return false;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
function transcriptMtimeMs(worktree, projectsDir = join2(homedir(), ".claude", "projects")) {
|
|
1429
|
+
const dir = transcriptDir(worktree, projectsDir);
|
|
1430
|
+
let files;
|
|
1431
|
+
try {
|
|
1432
|
+
files = readdirSync2(dir);
|
|
1433
|
+
} catch {
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
let latest = null;
|
|
1437
|
+
for (const f of files) {
|
|
1438
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
1439
|
+
try {
|
|
1440
|
+
const mtime2 = statSync(join2(dir, f)).mtimeMs;
|
|
1441
|
+
if (latest === null || mtime2 > latest) latest = mtime2;
|
|
1442
|
+
} catch {
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return latest;
|
|
1446
|
+
}
|
|
1447
|
+
function writeMcpConfig(spec) {
|
|
1448
|
+
const cfg = {
|
|
1449
|
+
mcpServers: {
|
|
1450
|
+
[spec.mcp.name]: { command: spec.mcp.command, args: spec.mcp.args, env: spec.mcp.env }
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
const path = join2(spec.worktree, MCP_CONFIG_FILE);
|
|
1454
|
+
writeFileSync2(path, JSON.stringify(cfg, null, 2));
|
|
1455
|
+
return path;
|
|
1456
|
+
}
|
|
1457
|
+
var ISOLATION_FLAGS, MCP_CONFIG_FILE, claudeCodeExecutor;
|
|
1458
|
+
var init_claudeCode = __esm({
|
|
1459
|
+
"src/executors/claudeCode.ts"() {
|
|
1460
|
+
"use strict";
|
|
1461
|
+
init_claudeBin();
|
|
1462
|
+
init_claudeResult();
|
|
1463
|
+
ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
|
|
1464
|
+
MCP_CONFIG_FILE = ".swarm-mcp.json";
|
|
1465
|
+
claudeCodeExecutor = {
|
|
1466
|
+
id: "claude-code",
|
|
1467
|
+
title: "Claude Code (claude)",
|
|
1468
|
+
binEnv: "SWARM_CLAUDE_BIN",
|
|
1469
|
+
install: { what: "Claude Code CLI \u2014 \u0438\u043C \u0434\u0440\u043E\u043D \u043F\u0438\u0448\u0435\u0442 \u043A\u043E\u0434", pkg: "@anthropic-ai/claude-code" },
|
|
1470
|
+
resolveBin: () => resolveClaudeBin(),
|
|
1471
|
+
// Стрелка, а не метод: `plan` могут вызвать в отрыве от объекта (деструктуризация в тестах,
|
|
1472
|
+
// проброс колбэком), и `this` тогда уедет в undefined.
|
|
1473
|
+
plan: (spec) => {
|
|
1474
|
+
const cfgPath = writeMcpConfig(spec);
|
|
1475
|
+
const settings = spec.denyPaths.length > 0 ? droneSettingsJson(spec.denyPaths) : void 0;
|
|
1476
|
+
return {
|
|
1477
|
+
bin: resolveClaudeBin(),
|
|
1478
|
+
args: claudeArgs(cfgPath, spec.prompt, spec.continueSession, spec.model, spec.skillDirs, settings)
|
|
1479
|
+
};
|
|
1480
|
+
},
|
|
1481
|
+
parseResult(raw, model) {
|
|
1482
|
+
const result = parseClaudeResultJson(raw);
|
|
1483
|
+
if (!result) return null;
|
|
1484
|
+
const est = launchCostUsd(result, model);
|
|
1485
|
+
const spend = launchSpend(result);
|
|
1486
|
+
return {
|
|
1487
|
+
costUsd: est.costUsd,
|
|
1488
|
+
...est.warning ? { warning: est.warning } : {},
|
|
1489
|
+
tokensIn: spend.tokensIn,
|
|
1490
|
+
tokensOut: spend.tokensOut,
|
|
1491
|
+
durationMs: spend.durationMs
|
|
1492
|
+
};
|
|
1493
|
+
},
|
|
1494
|
+
session: {
|
|
1495
|
+
exists: (worktree) => sessionTranscriptExists(worktree),
|
|
1496
|
+
mtimeMs: (worktree) => transcriptMtimeMs(worktree)
|
|
1497
|
+
},
|
|
1498
|
+
// `claude -p "ok" --max-turns 1` (TASWM-23) — прямее, чем HEAD на статус-хост: дёргает ТОТ
|
|
1499
|
+
// ЖЕ путь (auth + реальный ответ модели), что и дрон. ISOLATION_FLAGS те же, что у дрона
|
|
1500
|
+
// (TASWM-90): SessionStart-хуки юзера могли бы подвесить пробу и дать ложный шторм.
|
|
1501
|
+
probe: {
|
|
1502
|
+
args: [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"],
|
|
1503
|
+
// TASWM-214: исчерпанный лимит подписки — НЕ шторм провайдера, и путать их нельзя.
|
|
1504
|
+
limitPattern: /(weekly limit|usage limit|hit your .*limit)/i
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
// src/swarmHome.ts
|
|
1511
|
+
import { homedir as homedir2 } from "os";
|
|
1512
|
+
import { join as join3 } from "path";
|
|
1513
|
+
import { mkdirSync } from "fs";
|
|
1514
|
+
var SWARM_HOME;
|
|
1515
|
+
var init_swarmHome = __esm({
|
|
1516
|
+
"src/swarmHome.ts"() {
|
|
1517
|
+
"use strict";
|
|
1518
|
+
SWARM_HOME = process.env.SWARM_HOME || join3(homedir2(), ".taskless-swarm");
|
|
1519
|
+
mkdirSync(SWARM_HOME, { recursive: true });
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
// src/executors/codex.ts
|
|
1524
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync3, statSync as statSync2, symlinkSync } from "fs";
|
|
1525
|
+
import { join as join4 } from "path";
|
|
1526
|
+
function resolveCodexBin(env = process.env) {
|
|
1527
|
+
return (env.SWARM_CODEX_BIN ?? "").trim() || "codex";
|
|
1528
|
+
}
|
|
1529
|
+
function codexHomePath(worktree, swarmHome = SWARM_HOME) {
|
|
1530
|
+
return join4(swarmHome, "executor-state", "codex", worktree.replace(/[/.]/g, "-"));
|
|
1531
|
+
}
|
|
1532
|
+
function ensureCodexHome(worktree, realHome, env = process.env, swarmHome = SWARM_HOME) {
|
|
1533
|
+
const home = codexHomePath(worktree, swarmHome);
|
|
1534
|
+
mkdirSync2(home, { recursive: true });
|
|
1535
|
+
const real = (env.CODEX_HOME ?? "").trim() || join4(realHome, ".codex");
|
|
1536
|
+
for (const file of ["auth.json", "config.toml"]) {
|
|
1537
|
+
const target = join4(real, file);
|
|
1538
|
+
const link = join4(home, file);
|
|
1539
|
+
if (!existsSync3(target) || existsSync3(link)) continue;
|
|
1540
|
+
try {
|
|
1541
|
+
symlinkSync(target, link);
|
|
1542
|
+
} catch {
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return home;
|
|
1546
|
+
}
|
|
1547
|
+
function tomlValue(v) {
|
|
1548
|
+
if (typeof v === "string") {
|
|
1549
|
+
const escaped = v.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
1550
|
+
return `"${escaped}"`;
|
|
1551
|
+
}
|
|
1552
|
+
if (Array.isArray(v)) return `[${v.map((x) => tomlValue(x)).join(", ")}]`;
|
|
1553
|
+
return `{${Object.entries(v).map(([k, val]) => `${k} = ${tomlValue(val)}`).join(", ")}}`;
|
|
1554
|
+
}
|
|
1555
|
+
function mcpConfigArgs(spec) {
|
|
1556
|
+
const inline = `{command = ${tomlValue(spec.mcp.command)}, args = ${tomlValue(spec.mcp.args)}, env = ${tomlValue(spec.mcp.env)}}`;
|
|
1557
|
+
return ["-c", `mcp_servers.${spec.mcp.name}=${inline}`];
|
|
1558
|
+
}
|
|
1559
|
+
function skillsNotice(skillDirs) {
|
|
1560
|
+
if (skillDirs.length === 0) return "";
|
|
1561
|
+
return "\u0421\u043A\u0438\u043B\u043B\u044B \u0440\u043E\u043B\u0438 \u043B\u0435\u0436\u0430\u0442 \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C\u0438 \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 (\u044D\u0442\u043E\u0442 \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C \u043D\u0435 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0438\u0445 \u0441\u0430\u043C): " + skillDirs.join(", ") + ". \u041F\u0440\u043E\u0447\u0438\u0442\u0430\u0439 \u0438\u0445 SKILL.md \u041F\u0415\u0420\u0415\u0414 \u0440\u0430\u0431\u043E\u0442\u043E\u0439 \u2014 \u0442\u0430\u043C \u0434\u0438\u0441\u0446\u0438\u043F\u043B\u0438\u043D\u0430 \u0440\u043E\u044F \u0438 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u043F\u0440\u043E\u0435\u043A\u0442\u0430.";
|
|
1562
|
+
}
|
|
1563
|
+
function readUsage(node) {
|
|
1564
|
+
if (typeof node !== "object" || node === null) return null;
|
|
1565
|
+
const rec = node;
|
|
1566
|
+
const direct = rec.total_token_usage ?? rec.usage ?? rec.token_usage;
|
|
1567
|
+
if (direct && typeof direct === "object") return direct;
|
|
1568
|
+
const info = rec.info;
|
|
1569
|
+
if (info && typeof info === "object") return readUsage(info);
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
function parseCodexStream(raw) {
|
|
1573
|
+
let usage = null;
|
|
1574
|
+
let completed = false;
|
|
1575
|
+
for (const line of raw.split("\n")) {
|
|
1576
|
+
const trimmed = line.trim();
|
|
1577
|
+
if (!trimmed) continue;
|
|
1578
|
+
let parsed;
|
|
1579
|
+
try {
|
|
1580
|
+
parsed = JSON.parse(trimmed);
|
|
1581
|
+
} catch {
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
if (typeof parsed !== "object" || parsed === null) continue;
|
|
1585
|
+
const rec = parsed;
|
|
1586
|
+
const msg = typeof rec.msg === "object" && rec.msg !== null ? rec.msg : rec;
|
|
1587
|
+
const type = typeof msg.type === "string" ? msg.type : null;
|
|
1588
|
+
const found = readUsage(msg);
|
|
1589
|
+
if (found) usage = found;
|
|
1590
|
+
if (type && TERMINAL_EVENTS.has(type)) completed = true;
|
|
1591
|
+
}
|
|
1592
|
+
return { usage, completed };
|
|
1593
|
+
}
|
|
1594
|
+
function newestRolloutMtime(dir, depth = 4) {
|
|
1595
|
+
let entries;
|
|
1596
|
+
try {
|
|
1597
|
+
entries = readdirSync3(dir);
|
|
1598
|
+
} catch {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
let latest = null;
|
|
1602
|
+
for (const name of entries) {
|
|
1603
|
+
const full = join4(dir, name);
|
|
1604
|
+
let st;
|
|
1605
|
+
try {
|
|
1606
|
+
st = statSync2(full);
|
|
1607
|
+
} catch {
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
if (st.isDirectory()) {
|
|
1611
|
+
if (depth <= 0) continue;
|
|
1612
|
+
const nested = newestRolloutMtime(full, depth - 1);
|
|
1613
|
+
if (nested !== null && (latest === null || nested > latest)) latest = nested;
|
|
1614
|
+
} else if (name.endsWith(".jsonl")) {
|
|
1615
|
+
if (latest === null || st.mtimeMs > latest) latest = st.mtimeMs;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return latest;
|
|
1619
|
+
}
|
|
1620
|
+
var BYPASS_FLAG, SKIP_GIT_CHECK_FLAG, CODEX_MODEL_PRICING, TERMINAL_EVENTS, codexExecutor;
|
|
1621
|
+
var init_codex = __esm({
|
|
1622
|
+
"src/executors/codex.ts"() {
|
|
1623
|
+
"use strict";
|
|
1624
|
+
init_swarmHome();
|
|
1625
|
+
init_pricing();
|
|
1626
|
+
BYPASS_FLAG = "--dangerously-bypass-approvals-and-sandbox";
|
|
1627
|
+
SKIP_GIT_CHECK_FLAG = "--skip-git-repo-check";
|
|
1628
|
+
CODEX_MODEL_PRICING = {};
|
|
1629
|
+
TERMINAL_EVENTS = /* @__PURE__ */ new Set(["task_complete", "turn.completed", "thread.completed", "session.completed"]);
|
|
1630
|
+
codexExecutor = {
|
|
1631
|
+
id: "codex",
|
|
1632
|
+
title: "Codex CLI (codex)",
|
|
1633
|
+
binEnv: "SWARM_CODEX_BIN",
|
|
1634
|
+
install: { what: "Codex CLI \u2014 \u0430\u043B\u044C\u0442\u0435\u0440\u043D\u0430\u0442\u0438\u0432\u043D\u044B\u0439 \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C \u0434\u0440\u043E\u043D\u0430", pkg: "@openai/codex" },
|
|
1635
|
+
resolveBin: () => resolveCodexBin(),
|
|
1636
|
+
// Стрелка, а не метод — см. тот же комментарий в claudeCode.ts.
|
|
1637
|
+
plan: (spec) => {
|
|
1638
|
+
const notice = skillsNotice(spec.skillDirs);
|
|
1639
|
+
const prompt = notice ? `${spec.prompt}
|
|
1640
|
+
|
|
1641
|
+
${notice}` : spec.prompt;
|
|
1642
|
+
const args = [
|
|
1643
|
+
"exec",
|
|
1644
|
+
// Продолжение оборванной сессии — ПОДКОМАНДА, а не флаг: `exec resume --last` берёт
|
|
1645
|
+
// последнюю сессию этого CODEX_HOME (то есть этого worktree, см. codexHome).
|
|
1646
|
+
...spec.continueSession ? ["resume", "--last"] : [],
|
|
1647
|
+
"--json",
|
|
1648
|
+
SKIP_GIT_CHECK_FLAG,
|
|
1649
|
+
BYPASS_FLAG,
|
|
1650
|
+
...spec.model ? ["-m", spec.model] : [],
|
|
1651
|
+
...mcpConfigArgs(spec),
|
|
1652
|
+
prompt
|
|
1653
|
+
];
|
|
1654
|
+
return { bin: resolveCodexBin(), args, env: { CODEX_HOME: ensureCodexHome(spec.worktree, spec.realHome) } };
|
|
1655
|
+
},
|
|
1656
|
+
parseResult(raw, model) {
|
|
1657
|
+
const { usage, completed } = parseCodexStream(raw);
|
|
1658
|
+
if (!completed) return null;
|
|
1659
|
+
if (!usage) return { costUsd: 0, warning: "codex: \u043F\u043E\u0442\u043E\u043A \u0431\u0435\u0437 usage \u2014 cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0435 \u043F\u043E\u0441\u0447\u0438\u0442\u0430\u043D" };
|
|
1660
|
+
const cached = usage.cached_input_tokens ?? 0;
|
|
1661
|
+
const common = {
|
|
1662
|
+
input_tokens: Math.max(0, (usage.input_tokens ?? 0) - cached),
|
|
1663
|
+
output_tokens: usage.output_tokens ?? 0,
|
|
1664
|
+
cache_read_input_tokens: cached
|
|
1665
|
+
};
|
|
1666
|
+
const est = costFromUsage(common, model, CODEX_MODEL_PRICING);
|
|
1667
|
+
return est.warning ? { costUsd: est.costUsd, warning: est.warning } : { costUsd: est.costUsd };
|
|
1668
|
+
},
|
|
1669
|
+
session: {
|
|
1670
|
+
exists: (worktree) => newestRolloutMtime(join4(codexHomePath(worktree), "sessions")) !== null,
|
|
1671
|
+
mtimeMs: (worktree) => newestRolloutMtime(join4(codexHomePath(worktree), "sessions"))
|
|
1672
|
+
},
|
|
1673
|
+
probe: {
|
|
1674
|
+
// Тот же принцип, что у claude: дешёвый реальный прогон — он проверяет и auth, и живость
|
|
1675
|
+
// провайдера разом. Один короткий ответ, без тулов и без сети сверх модели.
|
|
1676
|
+
args: ["exec", "--json", SKIP_GIT_CHECK_FLAG, BYPASS_FLAG, "ok"],
|
|
1677
|
+
limitPattern: /(usage limit|rate limit|quota exceeded|insufficient_quota)/i
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
// src/executors/types.ts
|
|
1684
|
+
var init_types2 = __esm({
|
|
1685
|
+
"src/executors/types.ts"() {
|
|
1686
|
+
"use strict";
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
// src/executors/index.ts
|
|
1691
|
+
function resolveExecutor(id) {
|
|
1692
|
+
const key = (id ?? "").trim();
|
|
1693
|
+
if (!key) return EXECUTORS[DEFAULT_EXECUTOR_ID];
|
|
1694
|
+
const found = EXECUTORS[key];
|
|
1695
|
+
if (!found) {
|
|
1696
|
+
throw new Error(`\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u044B\u0439 \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C "${key}" (SWARM_EXECUTOR). \u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B: ${Object.keys(EXECUTORS).join(", ")}`);
|
|
1697
|
+
}
|
|
1698
|
+
return found;
|
|
1699
|
+
}
|
|
1700
|
+
var DEFAULT_EXECUTOR_ID, EXECUTORS;
|
|
1701
|
+
var init_executors = __esm({
|
|
1702
|
+
"src/executors/index.ts"() {
|
|
1703
|
+
"use strict";
|
|
1704
|
+
init_claudeCode();
|
|
1705
|
+
init_codex();
|
|
1706
|
+
init_types2();
|
|
1707
|
+
init_claudeCode();
|
|
1708
|
+
init_codex();
|
|
1709
|
+
DEFAULT_EXECUTOR_ID = "claude-code";
|
|
1710
|
+
EXECUTORS = {
|
|
1711
|
+
[claudeCodeExecutor.id]: claudeCodeExecutor,
|
|
1712
|
+
[codexExecutor.id]: codexExecutor
|
|
1713
|
+
};
|
|
790
1714
|
}
|
|
791
1715
|
});
|
|
792
1716
|
|
|
793
1717
|
// src/config.ts
|
|
794
1718
|
var config_exports = {};
|
|
795
1719
|
__export(config_exports, {
|
|
1720
|
+
DAEMON_LOG_CHECK_MS: () => DAEMON_LOG_CHECK_MS,
|
|
1721
|
+
DAEMON_LOG_KEEP: () => DAEMON_LOG_KEEP,
|
|
1722
|
+
DAEMON_LOG_MAX_BYTES: () => DAEMON_LOG_MAX_BYTES,
|
|
796
1723
|
DAEMON_LOG_PATH: () => DAEMON_LOG_PATH,
|
|
797
1724
|
DB_PATH: () => DB_PATH,
|
|
798
1725
|
DRONE_CHANNEL_ENABLED: () => DRONE_CHANNEL_ENABLED,
|
|
@@ -803,8 +1730,10 @@ __export(config_exports, {
|
|
|
803
1730
|
DRONE_HOME: () => DRONE_HOME,
|
|
804
1731
|
DRONE_HOME_ISOLATION: () => DRONE_HOME_ISOLATION,
|
|
805
1732
|
DRONE_ISOLATION: () => DRONE_ISOLATION,
|
|
1733
|
+
EXECUTOR: () => EXECUTOR,
|
|
806
1734
|
HEARTBEAT_PATH: () => HEARTBEAT_PATH,
|
|
807
1735
|
HEARTBEAT_STALE_MS: () => HEARTBEAT_STALE_MS,
|
|
1736
|
+
REPO_MAP_PATH: () => REPO_MAP_PATH,
|
|
808
1737
|
REVIEW_CUTOFF_PATH: () => REVIEW_CUTOFF_PATH,
|
|
809
1738
|
RUN_LOGS_DIR: () => RUN_LOGS_DIR,
|
|
810
1739
|
SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
|
|
@@ -842,9 +1771,11 @@ __export(config_exports, {
|
|
|
842
1771
|
SWARM_RELIEF_MAX_FD_LOAD_FACTOR: () => SWARM_RELIEF_MAX_FD_LOAD_FACTOR,
|
|
843
1772
|
SWARM_RELIEF_MIN_FREE_GB: () => SWARM_RELIEF_MIN_FREE_GB,
|
|
844
1773
|
SWARM_REPO: () => SWARM_REPO,
|
|
1774
|
+
SWARM_REPO_MAP_MS: () => SWARM_REPO_MAP_MS,
|
|
845
1775
|
SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
|
|
846
1776
|
SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
|
|
847
1777
|
SWARM_SCAN_DEPTH: () => SWARM_SCAN_DEPTH,
|
|
1778
|
+
SWARM_SCAN_ROOTS_RAW: () => SWARM_SCAN_ROOTS_RAW,
|
|
848
1779
|
SWARM_SHOT_TIMEOUT_MS: () => SWARM_SHOT_TIMEOUT_MS,
|
|
849
1780
|
SWARM_STORM_URL: () => SWARM_STORM_URL,
|
|
850
1781
|
SWARM_SWEEP_INTERVAL_MS: () => SWARM_SWEEP_INTERVAL_MS,
|
|
@@ -869,9 +1800,8 @@ __export(config_exports, {
|
|
|
869
1800
|
resolveEffectiveWorktreesDir: () => resolveEffectiveWorktreesDir,
|
|
870
1801
|
reviewCutoffAt: () => reviewCutoffAt
|
|
871
1802
|
});
|
|
872
|
-
import {
|
|
873
|
-
import {
|
|
874
|
-
import { mkdirSync, readFileSync as readFileSync2, realpathSync, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1803
|
+
import { basename, join as join5, resolve } from "path";
|
|
1804
|
+
import { readFileSync as readFileSync2, realpathSync, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
875
1805
|
function normalizeRepo(repo) {
|
|
876
1806
|
const r = (repo ?? "").trim();
|
|
877
1807
|
if (!r) return "";
|
|
@@ -882,7 +1812,7 @@ function normalizeRepo(repo) {
|
|
|
882
1812
|
}
|
|
883
1813
|
}
|
|
884
1814
|
function droneWorktreePath(worktreesDir, repo, slug) {
|
|
885
|
-
return
|
|
1815
|
+
return join5(worktreesDir, basename(normalizeRepo(repo)), slug);
|
|
886
1816
|
}
|
|
887
1817
|
function resolveEffectiveWorktreesDir(deps) {
|
|
888
1818
|
if (deps.envOverride) return deps.envOverride;
|
|
@@ -924,7 +1854,7 @@ function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
|
|
|
924
1854
|
}
|
|
925
1855
|
function tokenFromFile() {
|
|
926
1856
|
try {
|
|
927
|
-
return readFileSync2(
|
|
1857
|
+
return readFileSync2(join5(SWARM_HOME, "token"), "utf8").trim();
|
|
928
1858
|
} catch {
|
|
929
1859
|
return "";
|
|
930
1860
|
}
|
|
@@ -938,31 +1868,36 @@ function reviewCutoffAt() {
|
|
|
938
1868
|
const at = stamp();
|
|
939
1869
|
try {
|
|
940
1870
|
const tmp = `${REVIEW_CUTOFF_PATH}.tmp`;
|
|
941
|
-
|
|
1871
|
+
writeFileSync3(tmp, JSON.stringify({ at }));
|
|
942
1872
|
renameSync2(tmp, REVIEW_CUTOFF_PATH);
|
|
943
1873
|
} catch {
|
|
944
1874
|
}
|
|
945
1875
|
return at;
|
|
946
1876
|
}
|
|
947
|
-
var
|
|
1877
|
+
var DB_PATH, EXECUTOR, DRONE_HOME, DRONE_HOME_ISOLATION, DRONE_ISOLATION, WORKTREES_NOINDEX_DIR, WORKTREES_LEGACY_DIR, WORKTREES_DIR, RUN_LOGS_DIR, DAEMON_LOG_PATH, DAEMON_LOG_MAX_BYTES, DAEMON_LOG_KEEP, DAEMON_LOG_CHECK_MS, DIST_BUILD, SWARM_BACKEND, SWARM_OUTBOX, SWARM_MAX_IN_FLIGHT, SWARM_CLAIM_INTERVAL_MS, SWARM_MAX_LOAD_FACTOR, SWARM_MIN_FREE_GB, SWARM_MAX_FD_LOAD_FACTOR, SWARM_MAX_CPU_BUSY, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_RELIEF_CPU_BUSY, SWARM_RELIEF_COST_WINDOW_MS, SWARM_RELIEF_ALERT_PAUSES, SWARM_RELIEF_ALERT_LOST_MS, SWARM_RELIEF_ALERT_MIN_STEPS, SWARM_RELIEF_ALERT_SUSTAIN_MS, SWARM_LOW_DISK_GB, SWARM_REHYDRATE_MS, SWARM_DRONE_BRIDGE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_VERSION_CHECK_MS, SWARM_SWEEP_INTERVAL_MS, DRONE_CHANNEL_ENABLED, DRONE_CHANNEL_PORT, DRONE_CHANNEL_URL, DRONE_CHANNEL_SECRET_PATH, DRONE_CHANNEL_GRACE_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_SCAN_DEPTH, SWARM_REPO_PATHS, SWARM_SCAN_ROOTS_RAW, REPO_MAP_PATH, SWARM_REPO_MAP_MS, TASKLESS_API, SWARM_SHOT_TIMEOUT_MS, TASKLESS_MCP_PATH, TASKLESS_TOKEN, TASKLESS_WORKSPACE, TASKLESS_WORKSPACE_SLUG, TASKLESS_TASK_URL_BASE, SWARM_VERIFY, SWARM_AUTO_REVIEW, REVIEW_CUTOFF_PATH, SWARM_REVIEW_CAP, SWARM_STORM_URL, WEB_PORT, WEB_REDIRECT_URI, SWARM_ANOMALY_RENOTIFY_MS, SWARM_ANOMALY_MAX_REPEATS, SWARM_ANOMALY_RUN_DOWN_WINDOW_MS, SWARM_ANOMALY_FAILED_SPIKE_COUNT, SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS, SWARM_ANOMALY_QUEUE_STALLED_MS, SWARM_ANOMALY_PR_STUCK_MERGE_MS, SWARM_ANOMALY_PR_NO_REVIEWER_MS, SWARM_ANOMALY_QUESTION_UNANSWERED_MS, SWARM_ANOMALY_OUTBOX_STUCK_MS, SWARM_ANOMALY_RESOURCE_GATE_STALL_MS, SWARM_WATCHDOG_STALL_MS;
|
|
948
1878
|
var init_config = __esm({
|
|
949
1879
|
"src/config.ts"() {
|
|
950
1880
|
"use strict";
|
|
951
1881
|
init_src();
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1882
|
+
init_executors();
|
|
1883
|
+
init_swarmHome();
|
|
1884
|
+
init_swarmHome();
|
|
1885
|
+
DB_PATH = process.env.SWARM_DB || join5(SWARM_HOME, "swarm.db");
|
|
1886
|
+
EXECUTOR = resolveExecutor(process.env.SWARM_EXECUTOR);
|
|
1887
|
+
DRONE_HOME = process.env.SWARM_DRONE_HOME || join5(SWARM_HOME, "drone-home");
|
|
956
1888
|
DRONE_HOME_ISOLATION = process.env.SWARM_DRONE_ISOLATE_HOME !== "0" && process.platform !== "win32";
|
|
957
1889
|
DRONE_ISOLATION = DRONE_HOME_ISOLATION ? { home: DRONE_HOME, swarmHome: SWARM_HOME } : void 0;
|
|
958
|
-
WORKTREES_NOINDEX_DIR =
|
|
959
|
-
WORKTREES_LEGACY_DIR =
|
|
1890
|
+
WORKTREES_NOINDEX_DIR = join5(SWARM_HOME, "worktrees.noindex");
|
|
1891
|
+
WORKTREES_LEGACY_DIR = join5(SWARM_HOME, "worktrees");
|
|
960
1892
|
WORKTREES_DIR = process.env.SWARM_WORKTREES || WORKTREES_NOINDEX_DIR;
|
|
961
|
-
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS ||
|
|
962
|
-
DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG ||
|
|
1893
|
+
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join5(SWARM_HOME, "run-logs");
|
|
1894
|
+
DAEMON_LOG_PATH = process.env.SWARM_DAEMON_LOG || join5(SWARM_HOME, "overmind.log");
|
|
1895
|
+
DAEMON_LOG_MAX_BYTES = envNum("SWARM_DAEMON_LOG_MAX_BYTES", 16 * 1024 * 1024);
|
|
1896
|
+
DAEMON_LOG_KEEP = envNum("SWARM_DAEMON_LOG_KEEP", 2);
|
|
1897
|
+
DAEMON_LOG_CHECK_MS = envNum("SWARM_DAEMON_LOG_CHECK_MS", 6e4);
|
|
963
1898
|
DIST_BUILD = true;
|
|
964
1899
|
SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
|
|
965
|
-
SWARM_OUTBOX = process.env.SWARM_OUTBOX ||
|
|
1900
|
+
SWARM_OUTBOX = process.env.SWARM_OUTBOX || join5(SWARM_HOME, "outbox.jsonl");
|
|
966
1901
|
SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
|
|
967
1902
|
SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
|
|
968
1903
|
SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
|
|
@@ -987,13 +1922,16 @@ var init_config = __esm({
|
|
|
987
1922
|
DRONE_CHANNEL_ENABLED = process.env.SWARM_DRONE_CHANNEL !== "0";
|
|
988
1923
|
DRONE_CHANNEL_PORT = Number(process.env.SWARM_DRONE_CHANNEL_PORT) || 7421;
|
|
989
1924
|
DRONE_CHANNEL_URL = process.env.SWARM_DRONE_CHANNEL_URL || `http://127.0.0.1:${DRONE_CHANNEL_PORT}`;
|
|
990
|
-
DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET ||
|
|
1925
|
+
DRONE_CHANNEL_SECRET_PATH = process.env.SWARM_DRONE_CHANNEL_SECRET || join5(SWARM_HOME, "drone-channel.key");
|
|
991
1926
|
DRONE_CHANNEL_GRACE_MS = envNum("SWARM_DRONE_CHANNEL_GRACE_MS", 12e4);
|
|
992
|
-
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT ||
|
|
1927
|
+
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join5(SWARM_HOME, "overmind.heartbeat");
|
|
993
1928
|
HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
994
1929
|
SWARM_REPO = process.env.SWARM_REPO || "";
|
|
995
1930
|
SWARM_SCAN_DEPTH = Number(process.env.SWARM_SCAN_DEPTH) || 3;
|
|
996
1931
|
SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
|
|
1932
|
+
SWARM_SCAN_ROOTS_RAW = process.env.SWARM_SCAN_ROOTS || "";
|
|
1933
|
+
REPO_MAP_PATH = process.env.SWARM_REPO_MAP || join5(SWARM_HOME, "repo-map.json");
|
|
1934
|
+
SWARM_REPO_MAP_MS = envNum("SWARM_REPO_MAP_MS", 15 * 6e4);
|
|
997
1935
|
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
998
1936
|
SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
999
1937
|
TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
|
|
@@ -1003,7 +1941,7 @@ var init_config = __esm({
|
|
|
1003
1941
|
TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
|
|
1004
1942
|
SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
|
|
1005
1943
|
SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
|
|
1006
|
-
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF ||
|
|
1944
|
+
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join5(SWARM_HOME, "review-cutoff.json");
|
|
1007
1945
|
SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
|
|
1008
1946
|
SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
|
|
1009
1947
|
WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
|
|
@@ -1360,10 +2298,10 @@ var init_storm = __esm({
|
|
|
1360
2298
|
});
|
|
1361
2299
|
|
|
1362
2300
|
// src/heartbeat.ts
|
|
1363
|
-
import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as
|
|
2301
|
+
import { readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
1364
2302
|
function writeHeartbeat(path, now) {
|
|
1365
2303
|
const tmp = `${path}.tmp`;
|
|
1366
|
-
|
|
2304
|
+
writeFileSync5(tmp, String(now));
|
|
1367
2305
|
renameSync3(tmp, path);
|
|
1368
2306
|
}
|
|
1369
2307
|
function readHeartbeat(path) {
|
|
@@ -1486,7 +2424,7 @@ var init_procList = __esm({
|
|
|
1486
2424
|
|
|
1487
2425
|
// src/procCleanup.ts
|
|
1488
2426
|
function sleep(ms) {
|
|
1489
|
-
return new Promise((
|
|
2427
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1490
2428
|
}
|
|
1491
2429
|
function parseLsofCwd(output) {
|
|
1492
2430
|
const rows = [];
|
|
@@ -1704,16 +2642,21 @@ var init_procCleanup = __esm({
|
|
|
1704
2642
|
});
|
|
1705
2643
|
|
|
1706
2644
|
// src/git.ts
|
|
1707
|
-
import { existsSync as
|
|
2645
|
+
import { existsSync as existsSync9, readdirSync as readdirSync10, statSync as statSync7 } from "fs";
|
|
1708
2646
|
import { appendFile, mkdir, readFile, rm } from "fs/promises";
|
|
1709
|
-
import { isAbsolute, join as
|
|
2647
|
+
import { isAbsolute as isAbsolute2, join as join15 } from "path";
|
|
1710
2648
|
import { $ as $2 } from "execa";
|
|
2649
|
+
function isUnrecoverableRefError(e) {
|
|
2650
|
+
if (e instanceof MissingRefError) return true;
|
|
2651
|
+
const msg = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
|
2652
|
+
return MISSING_REF_RE.test(msg);
|
|
2653
|
+
}
|
|
1711
2654
|
async function reclaimStaleWorktree(repo, worktree) {
|
|
1712
2655
|
await detachWorktree(repo, worktree);
|
|
1713
2656
|
}
|
|
1714
2657
|
function isLinkedWorktree(dir) {
|
|
1715
2658
|
try {
|
|
1716
|
-
return
|
|
2659
|
+
return statSync7(join15(dir, ".git")).isFile();
|
|
1717
2660
|
} catch {
|
|
1718
2661
|
return false;
|
|
1719
2662
|
}
|
|
@@ -1725,7 +2668,7 @@ async function detachWorktree(repo, worktree) {
|
|
|
1725
2668
|
}
|
|
1726
2669
|
}
|
|
1727
2670
|
async function ensureWorktreeDirGone(worktree) {
|
|
1728
|
-
if (!
|
|
2671
|
+
if (!existsSync9(worktree)) return false;
|
|
1729
2672
|
if (!isLinkedWorktree(worktree)) {
|
|
1730
2673
|
console.error(
|
|
1731
2674
|
`[git] ${worktree} \u2014 \u043D\u0435 \u043F\u0440\u0438\u0441\u0442\u0451\u0433\u043D\u0443\u0442\u044B\u0439 worktree (.git \u043D\u0435 \u0444\u0430\u0439\u043B): \u043A\u0430\u0442\u0430\u043B\u043E\u0433 \u041D\u0415 \u0442\u0440\u043E\u0433\u0430\u044E. \u0415\u0441\u043B\u0438 \u044D\u0442\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E worktree \u0434\u0440\u043E\u043D\u0430 \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043E \u0440\u0443\u043A\u0430\u043C\u0438; \u0435\u0441\u043B\u0438 \u044D\u0442\u043E \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u0447\u0435\u043A\u0430\u0443\u0442, \u0437\u043D\u0430\u0447\u0438\u0442 \u0432\u0435\u0442\u043A\u0430 \u0441\u043B\u0430\u0433\u0430 \u0447\u0435\u043A\u043D\u0443\u0442\u0430 \u0432 \u043D\u0451\u043C (git checkout <slug>) \u2014 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0435\u0433\u043E \u043D\u0430 \u0434\u0440\u0443\u0433\u0443\u044E \u0432\u0435\u0442\u043A\u0443.`
|
|
@@ -1740,7 +2683,7 @@ async function ensureWorktreeDirGone(worktree) {
|
|
|
1740
2683
|
);
|
|
1741
2684
|
return true;
|
|
1742
2685
|
}
|
|
1743
|
-
if (
|
|
2686
|
+
if (existsSync9(worktree)) {
|
|
1744
2687
|
console.error(`[git] \u043A\u0430\u0442\u0430\u043B\u043E\u0433 worktree ${worktree} \u043F\u0435\u0440\u0435\u0436\u0438\u043B rm \u2014 \u0443\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u043A\u0430\u043C\u0438, \u0438\u043D\u0430\u0447\u0435 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0441\u043F\u0430\u0432\u043D \u0441\u043B\u0430\u0433\u0430 \u0432\u0441\u0442\u0430\u043D\u0435\u0442.`);
|
|
1745
2688
|
}
|
|
1746
2689
|
return true;
|
|
@@ -1783,7 +2726,7 @@ async function addWorktree(repo, path, branch, opts = {}) {
|
|
|
1783
2726
|
}
|
|
1784
2727
|
const busyAt = await worktreeForBranch(repo, branch);
|
|
1785
2728
|
if (busyAt) {
|
|
1786
|
-
const exists =
|
|
2729
|
+
const exists = existsSync9(busyAt);
|
|
1787
2730
|
const live = exists && (opts.isWorktreeHeldLive ? await opts.isWorktreeHeldLive(busyAt) : true);
|
|
1788
2731
|
if (live) throw new WorktreeBusyError(branch, busyAt);
|
|
1789
2732
|
if (exists && await worktreeDirty(busyAt)) throw new WorktreeBusyError(branch, busyAt);
|
|
@@ -1819,9 +2762,9 @@ async function ensureGitExcluded(worktree, pattern) {
|
|
|
1819
2762
|
const out = await $2({ reject: false })`git -C ${worktree} rev-parse --git-common-dir`;
|
|
1820
2763
|
const raw = out.stdout.trim();
|
|
1821
2764
|
if (out.exitCode !== 0 || !raw) return false;
|
|
1822
|
-
const commonDir =
|
|
1823
|
-
const infoDir =
|
|
1824
|
-
const excludeFile =
|
|
2765
|
+
const commonDir = isAbsolute2(raw) ? raw : join15(worktree, raw);
|
|
2766
|
+
const infoDir = join15(commonDir, "info");
|
|
2767
|
+
const excludeFile = join15(infoDir, "exclude");
|
|
1825
2768
|
let current = "";
|
|
1826
2769
|
try {
|
|
1827
2770
|
current = await readFile(excludeFile, "utf8");
|
|
@@ -1854,7 +2797,7 @@ async function worktreeDirty(path) {
|
|
|
1854
2797
|
async function repairMovedWorktrees(targetDir) {
|
|
1855
2798
|
const subdirs = (dir) => {
|
|
1856
2799
|
try {
|
|
1857
|
-
return
|
|
2800
|
+
return readdirSync10(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => join15(dir, d.name));
|
|
1858
2801
|
} catch {
|
|
1859
2802
|
return [];
|
|
1860
2803
|
}
|
|
@@ -1898,7 +2841,9 @@ async function journalForBranch(repo, branch, slug) {
|
|
|
1898
2841
|
}
|
|
1899
2842
|
async function prepareDetachedWorktree(repo, branch, worktree) {
|
|
1900
2843
|
await $2`git -C ${repo} fetch --quiet origin`;
|
|
1901
|
-
|
|
2844
|
+
const ref = `origin/${branch}`;
|
|
2845
|
+
if (!await revParse(repo, `${ref}^{commit}`)) throw new MissingRefError(ref, repo);
|
|
2846
|
+
await $2`git -C ${repo} worktree add --detach --force ${worktree} ${ref}`;
|
|
1902
2847
|
}
|
|
1903
2848
|
async function mergeRef(worktree, ref) {
|
|
1904
2849
|
const before = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
|
|
@@ -1931,7 +2876,7 @@ async function worktreeLastCommitMs(worktree) {
|
|
|
1931
2876
|
const sec = Number(out);
|
|
1932
2877
|
return Number.isInteger(sec) && sec > 0 ? sec * 1e3 : null;
|
|
1933
2878
|
}
|
|
1934
|
-
var WorktreeBusyError, GITHUB_REMOTE_RE;
|
|
2879
|
+
var WorktreeBusyError, MissingRefError, MISSING_REF_RE, GITHUB_REMOTE_RE;
|
|
1935
2880
|
var init_git = __esm({
|
|
1936
2881
|
"src/git.ts"() {
|
|
1937
2882
|
"use strict";
|
|
@@ -1946,6 +2891,17 @@ var init_git = __esm({
|
|
|
1946
2891
|
branch;
|
|
1947
2892
|
worktreePath;
|
|
1948
2893
|
};
|
|
2894
|
+
MissingRefError = class extends Error {
|
|
2895
|
+
constructor(ref, repo) {
|
|
2896
|
+
super(`ref ${ref} \u043D\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 ${repo} \u2014 \u0432\u0435\u0442\u043A\u0443 \u043D\u0435\u043B\u044C\u0437\u044F \u0432\u044B\u0447\u0435\u043A\u0430\u0442\u044C (\u043F\u0440\u043E\u0432\u0435\u0440\u044C, \u0438\u0437 \u043A\u0430\u043A\u043E\u0439 \u0432\u0435\u0442\u043A\u0438 \u043E\u0442\u043A\u0440\u044B\u0442 PR)`);
|
|
2897
|
+
this.ref = ref;
|
|
2898
|
+
this.repo = repo;
|
|
2899
|
+
this.name = "MissingRefError";
|
|
2900
|
+
}
|
|
2901
|
+
ref;
|
|
2902
|
+
repo;
|
|
2903
|
+
};
|
|
2904
|
+
MISSING_REF_RE = /invalid reference|unknown revision|not a valid object name|couldn't find remote ref/i;
|
|
1949
2905
|
GITHUB_REMOTE_RE = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/i;
|
|
1950
2906
|
}
|
|
1951
2907
|
});
|
|
@@ -1966,18 +2922,18 @@ __export(droneChannel_exports, {
|
|
|
1966
2922
|
verifyLocalCliToken: () => verifyLocalCliToken
|
|
1967
2923
|
});
|
|
1968
2924
|
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
1969
|
-
import { chmodSync, mkdirSync as
|
|
2925
|
+
import { chmodSync, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
|
|
1970
2926
|
import { dirname as dirname3 } from "path";
|
|
1971
2927
|
import { serve } from "@hono/node-server";
|
|
1972
2928
|
function loadChannelSecret(path) {
|
|
1973
2929
|
try {
|
|
1974
|
-
const s =
|
|
2930
|
+
const s = readFileSync13(path, "utf8").trim();
|
|
1975
2931
|
if (s.length >= 32) return s;
|
|
1976
2932
|
} catch {
|
|
1977
2933
|
}
|
|
1978
2934
|
const secret = randomBytes(32).toString("hex");
|
|
1979
|
-
|
|
1980
|
-
|
|
2935
|
+
mkdirSync7(dirname3(path), { recursive: true });
|
|
2936
|
+
writeFileSync9(path, secret, { mode: 384 });
|
|
1981
2937
|
try {
|
|
1982
2938
|
chmodSync(path, 384);
|
|
1983
2939
|
} catch {
|
|
@@ -2184,8 +3140,8 @@ function startDroneChannel(deps, port) {
|
|
|
2184
3140
|
return { port, server };
|
|
2185
3141
|
}
|
|
2186
3142
|
function closeDroneChannel(server) {
|
|
2187
|
-
return new Promise((
|
|
2188
|
-
server.close(() =>
|
|
3143
|
+
return new Promise((resolve4) => {
|
|
3144
|
+
server.close(() => resolve4());
|
|
2189
3145
|
server.closeAllConnections?.();
|
|
2190
3146
|
});
|
|
2191
3147
|
}
|
|
@@ -2307,7 +3263,7 @@ var init_droneChannel = __esm({
|
|
|
2307
3263
|
|
|
2308
3264
|
// src/attachments.ts
|
|
2309
3265
|
import { mkdir as mkdir2, rm as rm2, writeFile } from "fs/promises";
|
|
2310
|
-
import { join as
|
|
3266
|
+
import { join as join17 } from "path";
|
|
2311
3267
|
function attachmentsFromDoc(doc, origin) {
|
|
2312
3268
|
const out = [];
|
|
2313
3269
|
const walk2 = (node) => {
|
|
@@ -2398,7 +3354,7 @@ async function materializeAttachments(opts) {
|
|
|
2398
3354
|
};
|
|
2399
3355
|
const result = { files: [], skipped: [] };
|
|
2400
3356
|
if (opts.refs.length === 0) return result;
|
|
2401
|
-
const dir =
|
|
3357
|
+
const dir = join17(opts.worktree, ATTACHMENTS_DIR);
|
|
2402
3358
|
await io.removeDir(dir);
|
|
2403
3359
|
let dirReady = false;
|
|
2404
3360
|
let total = 0;
|
|
@@ -2444,7 +3400,7 @@ async function materializeAttachments(opts) {
|
|
|
2444
3400
|
await io.mkdir(dir);
|
|
2445
3401
|
dirReady = true;
|
|
2446
3402
|
}
|
|
2447
|
-
await io.writeFile(
|
|
3403
|
+
await io.writeFile(join17(dir, name), file.bytes);
|
|
2448
3404
|
} catch (e) {
|
|
2449
3405
|
result.skipped.push(`${label} \u2014 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u043E\u0441\u044C \u043D\u0430 \u0434\u0438\u0441\u043A (${errText(e)})`);
|
|
2450
3406
|
continue;
|
|
@@ -2597,7 +3553,7 @@ var init_attachments = __esm({
|
|
|
2597
3553
|
|
|
2598
3554
|
// src/shotUpload.ts
|
|
2599
3555
|
import { readFile as fsReadFile } from "fs/promises";
|
|
2600
|
-
import { basename as basename2, isAbsolute as
|
|
3556
|
+
import { basename as basename2, isAbsolute as isAbsolute3, join as join18 } from "path";
|
|
2601
3557
|
function shotMimetype(path) {
|
|
2602
3558
|
const dot = path.lastIndexOf(".");
|
|
2603
3559
|
if (dot < 0) return null;
|
|
@@ -2623,7 +3579,7 @@ async function readShots(paths, opts) {
|
|
|
2623
3579
|
}
|
|
2624
3580
|
let bytes;
|
|
2625
3581
|
try {
|
|
2626
|
-
bytes = await read(
|
|
3582
|
+
bytes = await read(isAbsolute3(path) ? path : join18(opts.worktree, path));
|
|
2627
3583
|
} catch (e) {
|
|
2628
3584
|
skipped.push({ path, reason: `\u043D\u0435 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D: ${e instanceof Error ? e.message : String(e)}` });
|
|
2629
3585
|
continue;
|
|
@@ -2701,8 +3657,8 @@ var init_shotUpload = __esm({
|
|
|
2701
3657
|
|
|
2702
3658
|
// src/oauth.ts
|
|
2703
3659
|
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
2704
|
-
import { readFileSync as
|
|
2705
|
-
import { join as
|
|
3660
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
|
|
3661
|
+
import { join as join19 } from "path";
|
|
2706
3662
|
function makePkce() {
|
|
2707
3663
|
const verifier = randomBytes2(48).toString("base64url");
|
|
2708
3664
|
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
@@ -2786,23 +3742,23 @@ function toStore(clientId, t, now) {
|
|
|
2786
3742
|
}
|
|
2787
3743
|
function loadStore() {
|
|
2788
3744
|
try {
|
|
2789
|
-
return JSON.parse(
|
|
3745
|
+
return JSON.parse(readFileSync14(STORE_PATH, "utf8"));
|
|
2790
3746
|
} catch {
|
|
2791
3747
|
return null;
|
|
2792
3748
|
}
|
|
2793
3749
|
}
|
|
2794
3750
|
function saveStore(s) {
|
|
2795
|
-
|
|
3751
|
+
writeFileSync10(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
|
|
2796
3752
|
}
|
|
2797
3753
|
function clearStore() {
|
|
2798
3754
|
try {
|
|
2799
|
-
|
|
3755
|
+
unlinkSync3(STORE_PATH);
|
|
2800
3756
|
} catch {
|
|
2801
3757
|
}
|
|
2802
3758
|
}
|
|
2803
3759
|
function loadClientId() {
|
|
2804
3760
|
try {
|
|
2805
|
-
return JSON.parse(
|
|
3761
|
+
return JSON.parse(readFileSync14(CLIENT_PATH, "utf8")).clientId;
|
|
2806
3762
|
} catch {
|
|
2807
3763
|
return null;
|
|
2808
3764
|
}
|
|
@@ -2811,7 +3767,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
|
|
|
2811
3767
|
const existing = loadClientId();
|
|
2812
3768
|
if (existing) return existing;
|
|
2813
3769
|
const clientId = await registerClient({ api, redirectUri }, fetchFn);
|
|
2814
|
-
|
|
3770
|
+
writeFileSync10(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
|
|
2815
3771
|
return clientId;
|
|
2816
3772
|
}
|
|
2817
3773
|
async function ensureAccess(store, opts = {}) {
|
|
@@ -2837,18 +3793,18 @@ var init_oauth = __esm({
|
|
|
2837
3793
|
AUTHORIZE = "/api/oauth/authorize";
|
|
2838
3794
|
TOKEN = "/api/oauth/token";
|
|
2839
3795
|
SCOPE = "openid profile email taskless:full";
|
|
2840
|
-
STORE_PATH =
|
|
2841
|
-
CLIENT_PATH =
|
|
3796
|
+
STORE_PATH = join19(SWARM_HOME, "oauth.json");
|
|
3797
|
+
CLIENT_PATH = join19(SWARM_HOME, "oauth-client.json");
|
|
2842
3798
|
base = (api) => api.replace(/\/$/, "");
|
|
2843
3799
|
}
|
|
2844
3800
|
});
|
|
2845
3801
|
|
|
2846
3802
|
// src/agentToken.ts
|
|
2847
|
-
import { readFileSync as
|
|
2848
|
-
import { join as
|
|
3803
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync4 } from "fs";
|
|
3804
|
+
import { join as join20 } from "path";
|
|
2849
3805
|
function loadAgentToken() {
|
|
2850
3806
|
try {
|
|
2851
|
-
const s = JSON.parse(
|
|
3807
|
+
const s = JSON.parse(readFileSync15(STORE_PATH2, "utf8"));
|
|
2852
3808
|
return s.token ? s : null;
|
|
2853
3809
|
} catch {
|
|
2854
3810
|
return null;
|
|
@@ -2885,17 +3841,17 @@ var init_agentToken = __esm({
|
|
|
2885
3841
|
"src/agentToken.ts"() {
|
|
2886
3842
|
"use strict";
|
|
2887
3843
|
init_config();
|
|
2888
|
-
STORE_PATH2 =
|
|
3844
|
+
STORE_PATH2 = join20(SWARM_HOME, "agent.json");
|
|
2889
3845
|
base2 = (api) => api.replace(/\/$/, "");
|
|
2890
3846
|
}
|
|
2891
3847
|
});
|
|
2892
3848
|
|
|
2893
3849
|
// src/workspace.ts
|
|
2894
|
-
import { readFileSync as
|
|
2895
|
-
import { join as
|
|
3850
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync5 } from "fs";
|
|
3851
|
+
import { join as join21 } from "path";
|
|
2896
3852
|
function loadWorkspace() {
|
|
2897
3853
|
try {
|
|
2898
|
-
const w = JSON.parse(
|
|
3854
|
+
const w = JSON.parse(readFileSync16(WORKSPACE_PATH, "utf8"));
|
|
2899
3855
|
return w && typeof w.id === "string" && w.id ? w : null;
|
|
2900
3856
|
} catch {
|
|
2901
3857
|
return null;
|
|
@@ -2909,7 +3865,7 @@ var init_workspace = __esm({
|
|
|
2909
3865
|
"src/workspace.ts"() {
|
|
2910
3866
|
"use strict";
|
|
2911
3867
|
init_config();
|
|
2912
|
-
WORKSPACE_PATH =
|
|
3868
|
+
WORKSPACE_PATH = join21(SWARM_HOME, "workspace.json");
|
|
2913
3869
|
}
|
|
2914
3870
|
});
|
|
2915
3871
|
|
|
@@ -2992,6 +3948,11 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
|
|
|
2992
3948
|
if (!doc) return null;
|
|
2993
3949
|
return { title: doc.title ?? "", body: extractText(doc.body) };
|
|
2994
3950
|
}
|
|
3951
|
+
async function getTaskContext(idOrSlug) {
|
|
3952
|
+
const res = await safeCall("get_task_context", { id: idOrSlug });
|
|
3953
|
+
const text = res?.content?.find((c) => c.type === "text")?.text;
|
|
3954
|
+
return typeof text === "string" && text.trim() ? text : null;
|
|
3955
|
+
}
|
|
2995
3956
|
async function resolveId(idOrSlug) {
|
|
2996
3957
|
const cached = idCache.get(idOrSlug);
|
|
2997
3958
|
if (cached) return cached;
|
|
@@ -3059,6 +4020,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase, fetchFile, fetchImpl
|
|
|
3059
4020
|
},
|
|
3060
4021
|
backlogStatusId,
|
|
3061
4022
|
getDocument,
|
|
4023
|
+
getTaskContext,
|
|
3062
4024
|
createTask: async (input) => {
|
|
3063
4025
|
if (!input.projectId) {
|
|
3064
4026
|
throw new Error("createTask: projectId \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u2014 \u0430\u0432\u0442\u043E-\u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0440\u043E\u0435\u043C \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u043E (TASWM-204)");
|
|
@@ -3098,7 +4060,19 @@ function requireTaskBody(task, slug, workspaceId) {
|
|
|
3098
4060
|
if (!task) throw new TaskBodyUnavailableError(slug, workspaceId);
|
|
3099
4061
|
return task;
|
|
3100
4062
|
}
|
|
4063
|
+
function wrapUntrusted(parts) {
|
|
4064
|
+
if (parts.length === 0) return "";
|
|
4065
|
+
return [
|
|
4066
|
+
UNTRUSTED_TASK_INPUT_NOTICE,
|
|
4067
|
+
"",
|
|
4068
|
+
"\u2500\u2500\u2500\u2500\u2500 \u041D\u0415\u0414\u041E\u0412\u0415\u0420\u0415\u041D\u041D\u042B\u0419 \u0412\u0412\u041E\u0414: \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438/\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B \u0437\u0430\u0434\u0430\u0447\u0438 \u2500\u2500\u2500\u2500\u2500",
|
|
4069
|
+
parts.join("\n\n"),
|
|
4070
|
+
"\u2500\u2500\u2500\u2500\u2500 \u041A\u041E\u041D\u0415\u0426 \u041D\u0415\u0414\u041E\u0412\u0415\u0420\u0415\u041D\u041D\u041E\u0413\u041E \u0412\u0412\u041E\u0414\u0410 \u2500\u2500\u2500\u2500\u2500"
|
|
4071
|
+
].join("\n");
|
|
4072
|
+
}
|
|
3101
4073
|
async function fetchTaskContext(client, task) {
|
|
4074
|
+
const brief = await client.getTaskContext(task.id);
|
|
4075
|
+
if (brief) return wrapUntrusted([brief]);
|
|
3102
4076
|
const parts = [];
|
|
3103
4077
|
const comments = task.comments ?? [];
|
|
3104
4078
|
if (comments.length > 0) {
|
|
@@ -3134,14 +4108,7 @@ ${text || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
|
|
3134
4108
|
)
|
|
3135
4109
|
);
|
|
3136
4110
|
}
|
|
3137
|
-
|
|
3138
|
-
return [
|
|
3139
|
-
UNTRUSTED_TASK_INPUT_NOTICE,
|
|
3140
|
-
"",
|
|
3141
|
-
"\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",
|
|
3142
|
-
parts.join("\n\n"),
|
|
3143
|
-
"\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"
|
|
3144
|
-
].join("\n");
|
|
4111
|
+
return wrapUntrusted(parts);
|
|
3145
4112
|
}
|
|
3146
4113
|
function commentIdOf(res) {
|
|
3147
4114
|
const comment = res?.structuredContent?.comment;
|
|
@@ -3336,8 +4303,8 @@ __export(db_exports, {
|
|
|
3336
4303
|
SwarmDb: () => SwarmDb
|
|
3337
4304
|
});
|
|
3338
4305
|
import { createRequire as createRequire2 } from "module";
|
|
3339
|
-
import { existsSync as
|
|
3340
|
-
import { resolve as
|
|
4306
|
+
import { existsSync as existsSync11 } from "fs";
|
|
4307
|
+
import { resolve as resolve3 } from "path";
|
|
3341
4308
|
function rowToQuestion(row) {
|
|
3342
4309
|
return {
|
|
3343
4310
|
id: row.id,
|
|
@@ -3370,6 +4337,10 @@ function rowToRun(row) {
|
|
|
3370
4337
|
depends_on: row.depends_on ? JSON.parse(row.depends_on) : [],
|
|
3371
4338
|
model: row.model ?? null,
|
|
3372
4339
|
cost_usd: row.cost_usd ?? 0,
|
|
4340
|
+
// Токены/время работы (TASWM-353) — старые прогоны без колонок → 0.
|
|
4341
|
+
tokens_in: row.tokens_in ?? 0,
|
|
4342
|
+
tokens_out: row.tokens_out ?? 0,
|
|
4343
|
+
duration_ms: row.duration_ms ?? 0,
|
|
3373
4344
|
pr_url: row.pr_url ?? null,
|
|
3374
4345
|
superseded_by: row.superseded_by ?? null,
|
|
3375
4346
|
decision: row.decision ? JSON.parse(row.decision) : null,
|
|
@@ -3435,6 +4406,9 @@ CREATE TABLE IF NOT EXISTS runs (
|
|
|
3435
4406
|
depends_on TEXT,
|
|
3436
4407
|
model TEXT,
|
|
3437
4408
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
4409
|
+
tokens_in INTEGER NOT NULL DEFAULT 0,
|
|
4410
|
+
tokens_out INTEGER NOT NULL DEFAULT 0,
|
|
4411
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
3438
4412
|
pr_url TEXT,
|
|
3439
4413
|
superseded_by INTEGER,
|
|
3440
4414
|
decision TEXT,
|
|
@@ -3528,7 +4502,11 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
3528
4502
|
// Базовая ветка PR (TASWM-285): куда дрон открывает PR и от чего ответвляется.
|
|
3529
4503
|
"ALTER TABLE runs ADD COLUMN base_branch TEXT",
|
|
3530
4504
|
// Воркспейс задачи прогона (TASWM-288): им пинятся задачные MCP-вызовы прогона.
|
|
3531
|
-
"ALTER TABLE runs ADD COLUMN workspace_id TEXT"
|
|
4505
|
+
"ALTER TABLE runs ADD COLUMN workspace_id TEXT",
|
|
4506
|
+
// Чем обошёлся прогон, кроме денег (TASWM-353): токены вход/выход и время работы.
|
|
4507
|
+
"ALTER TABLE runs ADD COLUMN tokens_in INTEGER NOT NULL DEFAULT 0",
|
|
4508
|
+
"ALTER TABLE runs ADD COLUMN tokens_out INTEGER NOT NULL DEFAULT 0",
|
|
4509
|
+
"ALTER TABLE runs ADD COLUMN duration_ms INTEGER NOT NULL DEFAULT 0"
|
|
3532
4510
|
]) {
|
|
3533
4511
|
try {
|
|
3534
4512
|
this.db.exec(alter);
|
|
@@ -3817,11 +4795,11 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
3817
4795
|
* транзакции: прервётся — маркер не встанет, повтор безопасен.
|
|
3818
4796
|
*/
|
|
3819
4797
|
importDb(sourcePath, repo) {
|
|
3820
|
-
const abs =
|
|
4798
|
+
const abs = resolve3(sourcePath);
|
|
3821
4799
|
const repoN = normalizeRepo(repo);
|
|
3822
4800
|
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
3823
4801
|
if (already) return { imported: false, runs: already.run_count };
|
|
3824
|
-
if (!
|
|
4802
|
+
if (!existsSync11(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
3825
4803
|
const src = new this.databaseCtor(abs);
|
|
3826
4804
|
try {
|
|
3827
4805
|
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
@@ -4189,9 +5167,9 @@ var init_droneStore = __esm({
|
|
|
4189
5167
|
});
|
|
4190
5168
|
|
|
4191
5169
|
// src/overmind.ts
|
|
4192
|
-
import { homedir as
|
|
4193
|
-
import { existsSync as
|
|
4194
|
-
import { basename as basename4, dirname as dirname5, join as
|
|
5170
|
+
import { homedir as homedir5 } from "os";
|
|
5171
|
+
import { existsSync as existsSync16, readFileSync as readFileSync20, readdirSync as readdirSync12, renameSync as renameSync10 } from "fs";
|
|
5172
|
+
import { basename as basename4, dirname as dirname5, join as join28 } from "path";
|
|
4195
5173
|
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "url";
|
|
4196
5174
|
import { spawn as spawn3 } from "child_process";
|
|
4197
5175
|
|
|
@@ -4271,13 +5249,13 @@ init_config();
|
|
|
4271
5249
|
init_resources();
|
|
4272
5250
|
init_names2();
|
|
4273
5251
|
init_storm();
|
|
4274
|
-
import { appendFileSync, existsSync as
|
|
5252
|
+
import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
4275
5253
|
import { dirname } from "path";
|
|
4276
5254
|
import { randomUUID } from "crypto";
|
|
4277
5255
|
|
|
4278
5256
|
// src/logTail.ts
|
|
4279
|
-
import { closeSync, existsSync as
|
|
4280
|
-
import { join as
|
|
5257
|
+
import { closeSync, existsSync as existsSync4, openSync, readSync, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
|
|
5258
|
+
import { join as join6 } from "path";
|
|
4281
5259
|
var TAIL_LINES_FALLBACK = 200;
|
|
4282
5260
|
var TAIL_MAX_BYTES_FALLBACK = 64 * 1024;
|
|
4283
5261
|
function fileHeader(name, size, shown) {
|
|
@@ -4287,7 +5265,7 @@ function fileHeader(name, size, shown) {
|
|
|
4287
5265
|
function readTail(path, opts) {
|
|
4288
5266
|
let size;
|
|
4289
5267
|
try {
|
|
4290
|
-
size =
|
|
5268
|
+
size = statSync3(path).size;
|
|
4291
5269
|
} catch {
|
|
4292
5270
|
return null;
|
|
4293
5271
|
}
|
|
@@ -4318,10 +5296,10 @@ function readTail(path, opts) {
|
|
|
4318
5296
|
return { text, size, shown: Buffer.byteLength(text, "utf8") };
|
|
4319
5297
|
}
|
|
4320
5298
|
function latestRunLaunch(runLogsDir, localRunId) {
|
|
4321
|
-
const dir =
|
|
5299
|
+
const dir = join6(runLogsDir, String(localRunId));
|
|
4322
5300
|
let files;
|
|
4323
5301
|
try {
|
|
4324
|
-
files =
|
|
5302
|
+
files = readdirSync4(dir);
|
|
4325
5303
|
} catch {
|
|
4326
5304
|
return null;
|
|
4327
5305
|
}
|
|
@@ -4332,13 +5310,13 @@ function latestRunLaunch(runLogsDir, localRunId) {
|
|
|
4332
5310
|
}
|
|
4333
5311
|
if (indexes.size === 0) return null;
|
|
4334
5312
|
const index = Math.max(...indexes);
|
|
4335
|
-
const out = [
|
|
4336
|
-
const err =
|
|
5313
|
+
const out = [join6(dir, `${index}.json`), join6(dir, `${index}.json.done`)].find((p) => existsSync4(p)) ?? null;
|
|
5314
|
+
const err = existsSync4(join6(dir, `${index}.err.log`)) ? join6(dir, `${index}.err.log`) : null;
|
|
4337
5315
|
return { index, total: indexes.size, out, err };
|
|
4338
5316
|
}
|
|
4339
5317
|
function runLogTail(runLogsDir, localRunId, opts) {
|
|
4340
5318
|
const launch = latestRunLaunch(runLogsDir, localRunId);
|
|
4341
|
-
if (!launch) return { error: `\u043B\u043E\u0433\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 (${
|
|
5319
|
+
if (!launch) return { error: `\u043B\u043E\u0433\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u043D\u0435\u0442 (${join6(runLogsDir, String(localRunId))} \u043F\u0443\u0441\u0442 \u0438\u043B\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442)` };
|
|
4342
5320
|
const parts = [];
|
|
4343
5321
|
let budget = opts.maxBytes;
|
|
4344
5322
|
if (launch.err) {
|
|
@@ -4471,6 +5449,15 @@ var SwarmClient = class _SwarmClient {
|
|
|
4471
5449
|
nextRunId = 1;
|
|
4472
5450
|
nextEventId = 1;
|
|
4473
5451
|
nextQuestionId = 1;
|
|
5452
|
+
// TASWM-350: ключ НЕЗАКРЫТОЙ попытки взять работу. Живёт от первого вызова claim до
|
|
5453
|
+
// первого настоящего ответа прода; всё это время ретраи идут с ним, и повтор после
|
|
5454
|
+
// обрыва связи не превращается во второе взятие. См. claim().
|
|
5455
|
+
pendingClaimKey = null;
|
|
5456
|
+
// TASWM-357: последнее НАПЕЧАТАННОЕ про пропущенные claim'ом задачи (родители и
|
|
5457
|
+
// заблокированные). Дедуп по тексту: claim идёт раз в ~60 c, а набор пропущенных меняется
|
|
5458
|
+
// редко — без этого лог был бы простынёй из одной и той же строки, и в ней потонуло бы
|
|
5459
|
+
// всё остальное. Меняется набор (эпик закрыли / блокер прошли) → печатаем снова.
|
|
5460
|
+
lastClaimSkipLog = null;
|
|
4474
5461
|
// Зависимости, подтверждённые ПРОДОМ (TASWM-300): слаг → «прогон этой задачи уже done».
|
|
4475
5462
|
// Монотонно (done не откатывается) → положительный ответ не протухает; отрицательный
|
|
4476
5463
|
// переспрашивается по TTL. Живёт в памяти: рестарт демона просто переспросит прод.
|
|
@@ -4481,6 +5468,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
4481
5468
|
// ровно один раз (первой отвергнутой нотификацией) и держится всю жизнь процесса — иначе
|
|
4482
5469
|
// каждый сигнал платил бы за это лишним отвергнутым вызовом. См. notifyAnomaly.
|
|
4483
5470
|
notifyLevelUnsupported = false;
|
|
5471
|
+
// TAS-485: то же для гашения разрешившихся тревог (swarm_notify_resolve) — прод старее
|
|
5472
|
+
// демона не знает тула; ретраить это каждый тик бессмысленно, см. resolveAnomalies.
|
|
5473
|
+
notifyResolveUnsupported = false;
|
|
4484
5474
|
// Корреляция с продом.
|
|
4485
5475
|
keyByLocal = /* @__PURE__ */ new Map();
|
|
4486
5476
|
// local run id → runKey
|
|
@@ -4523,24 +5513,24 @@ var SwarmClient = class _SwarmClient {
|
|
|
4523
5513
|
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
4524
5514
|
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
4525
5515
|
this.host = opts.host ?? null;
|
|
4526
|
-
this.repoExists = opts.repoExists ??
|
|
5516
|
+
this.repoExists = opts.repoExists ?? existsSync5;
|
|
4527
5517
|
this.questionSyncMs = opts.questionSyncMs ?? 3e3;
|
|
4528
5518
|
this.outboxPath = opts.outboxPath;
|
|
4529
5519
|
this.mapPath = `${opts.outboxPath}.prodmap.json`;
|
|
4530
5520
|
this.stormPath = `${opts.outboxPath}.storm.jsonl`;
|
|
4531
|
-
|
|
5521
|
+
mkdirSync4(dirname(this.outboxPath), { recursive: true });
|
|
4532
5522
|
this.recover();
|
|
4533
5523
|
}
|
|
4534
5524
|
// ── рестарт: поднять outbox (незаписанные операции) и карту runKey→prodId ─────
|
|
4535
5525
|
recover() {
|
|
4536
|
-
if (
|
|
5526
|
+
if (existsSync5(this.mapPath)) {
|
|
4537
5527
|
try {
|
|
4538
5528
|
const obj = JSON.parse(readFileSync4(this.mapPath, "utf8"));
|
|
4539
5529
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
4540
5530
|
} catch {
|
|
4541
5531
|
}
|
|
4542
5532
|
}
|
|
4543
|
-
if (
|
|
5533
|
+
if (existsSync5(this.outboxPath)) {
|
|
4544
5534
|
const txt = readFileSync4(this.outboxPath, "utf8");
|
|
4545
5535
|
for (const line of txt.split("\n")) {
|
|
4546
5536
|
const s = line.trim();
|
|
@@ -4561,7 +5551,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
4561
5551
|
// eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
|
|
4562
5552
|
// переприсваиваем локально (id событий — только порядок, не корреляция с продом).
|
|
4563
5553
|
replayGlobalEvents() {
|
|
4564
|
-
if (!
|
|
5554
|
+
if (!existsSync5(this.stormPath)) return;
|
|
4565
5555
|
let txt;
|
|
4566
5556
|
try {
|
|
4567
5557
|
txt = readFileSync4(this.stormPath, "utf8");
|
|
@@ -4601,6 +5591,18 @@ var SwarmClient = class _SwarmClient {
|
|
|
4601
5591
|
runKeyFor(localId) {
|
|
4602
5592
|
return this.keyByLocal.get(localId);
|
|
4603
5593
|
}
|
|
5594
|
+
/**
|
|
5595
|
+
* TASWM-336: заменить репо-реестр на лету. До этой задачи реестр собирался ОДИН РАЗ на
|
|
5596
|
+
* старте из окружения и жил до рестарта демона; теперь машина сама находит клоны своих
|
|
5597
|
+
* репозиториев и переспрашивает прод по TTL — у владельца появился новый swarm-проект,
|
|
5598
|
+
* и машина обязана подхватить его без рестарта. Меняем обе стороны разом: и список для
|
|
5599
|
+
* claim/heartbeat, и обратную карту `owner/repo` → путь (иначе прогон нового репо
|
|
5600
|
+
* пришёл бы именем, которое не во что перевести).
|
|
5601
|
+
*/
|
|
5602
|
+
setRepoRegistry(registry) {
|
|
5603
|
+
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
5604
|
+
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
5605
|
+
}
|
|
4604
5606
|
// cuid прода → локальный id (по runKey гидрированного прогона `p:<cuid>`).
|
|
4605
5607
|
// Для маппинга прод heroId → локальный hero_run_id при гидрации. Резолвится, когда
|
|
4606
5608
|
// прогон-герой присутствует в зеркале; иначе null (уточнится следующим рефрешем).
|
|
@@ -4618,8 +5620,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
4618
5620
|
this.scheduleFlush();
|
|
4619
5621
|
}
|
|
4620
5622
|
// Прод-патч прогона: локальные поля Run → поля swarm_run_upsert (см. TASWM-74).
|
|
4621
|
-
// pid/worktree_path (рантайм),
|
|
4622
|
-
//
|
|
5623
|
+
// pid/worktree_path (рантайм), снимок задачи (task_*, прод берёт join'ом из Task)
|
|
5624
|
+
// и priority (локальная очередь ЭТОГО демона, TASWM-269)
|
|
4623
5625
|
// в прод НЕ уходят. kind → role (плоский enum). Self-ref
|
|
4624
5626
|
// прогонов hero_run_id/resolves_run_id/superseded_by → heroId/resolvesRunId/
|
|
4625
5627
|
// supersededById как ОТЛОЖЕННЫЕ ссылки (RunRef) — cuid резолвится на флаше.
|
|
@@ -4630,6 +5632,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
4630
5632
|
if ("model" in patch) out.model = patch.model ?? null;
|
|
4631
5633
|
if ("name" in patch) out.droneName = patch.name ?? null;
|
|
4632
5634
|
if ("cost_usd" in patch && patch.cost_usd !== void 0) out.costUsd = patch.cost_usd;
|
|
5635
|
+
if ("tokens_in" in patch && patch.tokens_in !== void 0) out.tokensIn = patch.tokens_in;
|
|
5636
|
+
if ("tokens_out" in patch && patch.tokens_out !== void 0) out.tokensOut = patch.tokens_out;
|
|
5637
|
+
if ("duration_ms" in patch && patch.duration_ms !== void 0) out.durationMs = patch.duration_ms;
|
|
5638
|
+
if ("respawns" in patch && patch.respawns !== void 0) out.respawns = patch.respawns;
|
|
4633
5639
|
if ("pr_url" in patch) out.prUrl = patch.pr_url ?? null;
|
|
4634
5640
|
if ("branch" in patch) out.branch = patch.branch ?? null;
|
|
4635
5641
|
if ("repo" in patch) out.repo = patch.repo ?? null;
|
|
@@ -4695,6 +5701,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
4695
5701
|
depends_on: input.depends_on && input.depends_on.length ? input.depends_on : [],
|
|
4696
5702
|
model: input.model ?? null,
|
|
4697
5703
|
cost_usd: 0,
|
|
5704
|
+
tokens_in: 0,
|
|
5705
|
+
// копятся учётом запусков claude (cost.ts), как cost_usd — TASWM-353
|
|
5706
|
+
tokens_out: 0,
|
|
5707
|
+
duration_ms: 0,
|
|
4698
5708
|
pr_url: null,
|
|
4699
5709
|
superseded_by: null,
|
|
4700
5710
|
decision: input.decision ?? null,
|
|
@@ -4817,6 +5827,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
4817
5827
|
if (!d) return 0;
|
|
4818
5828
|
d.respawns += 1;
|
|
4819
5829
|
d.updated_at = stamp();
|
|
5830
|
+
this.enqueueRun(d, this.runFields({ respawns: d.respawns }));
|
|
4820
5831
|
return d.respawns;
|
|
4821
5832
|
}
|
|
4822
5833
|
cancel(id, reason) {
|
|
@@ -5232,7 +6243,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
5232
6243
|
* транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
|
|
5233
6244
|
*/
|
|
5234
6245
|
reconcileRuntime(opts) {
|
|
5235
|
-
const wtExists = opts.worktreeExists ??
|
|
6246
|
+
const wtExists = opts.worktreeExists ?? existsSync5;
|
|
5236
6247
|
const pidByWorktree = /* @__PURE__ */ new Map();
|
|
5237
6248
|
try {
|
|
5238
6249
|
for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
|
|
@@ -5320,23 +6331,69 @@ var SwarmClient = class _SwarmClient {
|
|
|
5320
6331
|
* шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
|
|
5321
6332
|
*/
|
|
5322
6333
|
async claim() {
|
|
6334
|
+
this.pendingClaimKey ??= randomUUID();
|
|
5323
6335
|
let res;
|
|
5324
6336
|
try {
|
|
5325
6337
|
res = await this.call("swarm_claim_work", {
|
|
5326
6338
|
...this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {},
|
|
5327
6339
|
// TASWM-281: взятая из общей очереди задача сразу принадлежит ЭТОЙ машине —
|
|
5328
6340
|
// прод помечает созданный прогон hostId, чужой демон его не увидит.
|
|
5329
|
-
...this.host ? { hostId: this.host.hostId } : {}
|
|
6341
|
+
...this.host ? { hostId: this.host.hostId } : {},
|
|
6342
|
+
idempotencyKey: this.pendingClaimKey
|
|
5330
6343
|
});
|
|
5331
6344
|
} catch {
|
|
5332
6345
|
return null;
|
|
5333
6346
|
}
|
|
5334
6347
|
if (res?.isError) return null;
|
|
6348
|
+
this.pendingClaimKey = null;
|
|
5335
6349
|
const sc = res.structuredContent ?? {};
|
|
6350
|
+
this.logClaimSkips(sc);
|
|
5336
6351
|
if (!sc.claimed || typeof sc.run !== "object" || sc.run === null) return null;
|
|
5337
6352
|
const local = this.hydrateRun(sc.run);
|
|
5338
6353
|
return local ? this.mat(local) : null;
|
|
5339
6354
|
}
|
|
6355
|
+
/**
|
|
6356
|
+
* TASWM-357: назвать в логе задачи, которые прод НЕ отдал в работу осознанно — родителей с
|
|
6357
|
+
* незавершёнными подзадачами (работа всегда в листьях) и листья с незакрытым блокером
|
|
6358
|
+
* (явный порядок, связь задач «блокирует»). Печатаем ЗДЕСЬ, а не на проде: оба фильтра
|
|
6359
|
+
* живут SQL-условиями и молчат по природе, а `display` claim'а демон не логирует — без
|
|
6360
|
+
* этой строки «почему эпик стоит нетронутым» не видно ниоткуда, и оператор ровно так же
|
|
6361
|
+
* гадает, как гадал, пока дрон вставал на эпике с вопросом «меня спавнили на родителя».
|
|
6362
|
+
*
|
|
6363
|
+
* Формат прода — `data.skippedParents: [{slug,title,openChildren}]` и
|
|
6364
|
+
* `data.skippedBlocked: [{slug,title,blockers[]}]`. Читаем защищённо: у старого прода
|
|
6365
|
+
* полей нет вовсе (тогда молчим), а разбирать чужую строку `display` вместо структуры —
|
|
6366
|
+
* тот же класс хрупкости, что уже кусал нас в notifyAnomaly.
|
|
6367
|
+
*/
|
|
6368
|
+
logClaimSkips(sc) {
|
|
6369
|
+
const rows = (raw) => (Array.isArray(raw) ? raw : []).map((p) => typeof p === "object" && p !== null ? p : null).filter((p) => p !== null && typeof p.slug === "string");
|
|
6370
|
+
const others = (flag) => flag === true ? " \u0438 \u0434\u0440\u0443\u0433\u0438\u0435" : "";
|
|
6371
|
+
const lines = [];
|
|
6372
|
+
const parents = rows(sc.skippedParents).map(
|
|
6373
|
+
(p) => `${p.slug} (${typeof p.openChildren === "number" ? p.openChildren : "?"} \u043D\u0435\u0437\u0430\u043A\u0440\u044B\u0442\u044B\u0445 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447)`
|
|
6374
|
+
);
|
|
6375
|
+
if (parents.length > 0) {
|
|
6376
|
+
lines.push(
|
|
6377
|
+
`[swarm-client] claim \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438\u043B \u0437\u0430\u0434\u0430\u0447\u0438-\u0440\u043E\u0434\u0438\u0442\u0435\u043B\u0438 \u2014 \u0440\u0430\u0431\u043E\u0442\u0430 \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0432 \u043B\u0438\u0441\u0442\u044C\u044F\u0445, \u0434\u0440\u043E\u043D\u0430 \u043D\u0430 \u043D\u0438\u0445 \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u0438\u043C: ${parents.join(", ")}${others(sc.skippedParentsMore)}`
|
|
6378
|
+
);
|
|
6379
|
+
}
|
|
6380
|
+
const blocked = rows(sc.skippedBlocked).map(
|
|
6381
|
+
(b) => `${b.slug} \u0436\u0434\u0451\u0442 ${Array.isArray(b.blockers) && b.blockers.length > 0 ? b.blockers.join(", ") : "\u0431\u043B\u043E\u043A\u0435\u0440\u0430"}`
|
|
6382
|
+
);
|
|
6383
|
+
if (blocked.length > 0) {
|
|
6384
|
+
lines.push(
|
|
6385
|
+
`[swarm-client] claim \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438\u043B \u0437\u0430\u0434\u0430\u0447\u0438 \u0441 \u043D\u0435\u0437\u0430\u043A\u0440\u044B\u0442\u044B\u043C \u0431\u043B\u043E\u043A\u0435\u0440\u043E\u043C \u2014 \u043F\u043E\u0440\u044F\u0434\u043E\u043A \u0437\u0430\u0434\u0430\u043D \u0441\u0432\u044F\u0437\u044C\u044E \xAB\u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442\xBB, \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u0438\u0442\u044C \u043D\u0435 \u0431\u0443\u0434\u0443: ${blocked.join("; ")}${others(sc.skippedBlockedMore)}`
|
|
6386
|
+
);
|
|
6387
|
+
}
|
|
6388
|
+
const text = lines.join("\n");
|
|
6389
|
+
if (text.length === 0) {
|
|
6390
|
+
this.lastClaimSkipLog = null;
|
|
6391
|
+
return;
|
|
6392
|
+
}
|
|
6393
|
+
if (text === this.lastClaimSkipLog) return;
|
|
6394
|
+
this.lastClaimSkipLog = text;
|
|
6395
|
+
for (const line of lines) console.log(line);
|
|
6396
|
+
}
|
|
5340
6397
|
/**
|
|
5341
6398
|
* TASWM-281: пометить своими прогоны, под которыми на ЭТОЙ машине нашёлся ЖИВОЙ
|
|
5342
6399
|
* процесс (после reconcileRuntime на старте). Это ровно те прогоны, где сомнений в
|
|
@@ -5610,12 +6667,20 @@ var SwarmClient = class _SwarmClient {
|
|
|
5610
6667
|
* Шлём ВСЕГДА (в т.ч. `paused:false`): это состояние, а не команда, и молчание тут
|
|
5611
6668
|
* означало бы «прод помнит вчерашнюю паузу», когда рой давно работает.
|
|
5612
6669
|
*
|
|
6670
|
+
* TASWM-352: тем же пульсом уходит ЁМКОСТЬ этой машины (`capacity` — живой потолок
|
|
6671
|
+
* дронов ЭТОГО демона: машинный оверрайд, иначе общая настройка владельца). По ней
|
|
6672
|
+
* планировщик роя раскладывает очередь между машинами: до этого у прода было ОДНО
|
|
6673
|
+
* число на владельца, и ноутбук с рабочей станцией делили его поровну, хотя тянут
|
|
6674
|
+
* разное. Не знаем своего потолка (ещё не читали / sqlite) → ключ не шлём, прод не
|
|
6675
|
+
* трогает прошлое значение — та же логика, что у repoFullNames.
|
|
6676
|
+
*
|
|
5613
6677
|
* Прод недоступен / ответ без поручений → пустые списки (демон работает дальше).
|
|
5614
6678
|
*/
|
|
5615
|
-
async heartbeat(daemonVersion, pause) {
|
|
6679
|
+
async heartbeat(daemonVersion, pause, capacity) {
|
|
5616
6680
|
try {
|
|
5617
6681
|
const args = this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {};
|
|
5618
6682
|
if (daemonVersion) args.daemonVersion = daemonVersion;
|
|
6683
|
+
if (capacity != null && Number.isFinite(capacity) && capacity > 0) args.capacity = Math.trunc(capacity);
|
|
5619
6684
|
if (pause !== void 0) {
|
|
5620
6685
|
args.paused = !!pause?.paused;
|
|
5621
6686
|
if (pause?.reason) args.pauseReason = pause.reason;
|
|
@@ -5721,6 +6786,35 @@ var SwarmClient = class _SwarmClient {
|
|
|
5721
6786
|
const res = await this.call("swarm_notify", base3);
|
|
5722
6787
|
if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
|
|
5723
6788
|
}
|
|
6789
|
+
/**
|
|
6790
|
+
* TAS-485: погасить в инбоксе владельца тревоги, чьё условие ушло. Ключи — те же, с
|
|
6791
|
+
* которыми уходил notifyAnomaly (прод сам ставит им префикс `swarm:`).
|
|
6792
|
+
*
|
|
6793
|
+
* Бросает при сбое — СОЗНАТЕЛЬНО, как notifyAnomaly: вызывающий (tickOnce) вычёркивает
|
|
6794
|
+
* ключи из durable-очереди только после успеха, иначе «погашено» разошлось бы с тем,
|
|
6795
|
+
* что человек видит в инбоксе, и разошлось бы навсегда — повторить будет некому.
|
|
6796
|
+
*
|
|
6797
|
+
* ДЕПЛОЙ-ОКНО. Демон из свежего чекаута против ещё не задеплоенного прода получит
|
|
6798
|
+
* «неизвестный тул». Это не ошибка доставки, а отсутствие фичи на той стороне: ретраить
|
|
6799
|
+
* её каждые ~4 секунды бессмысленно, поэтому первую такую осечку запоминаем на жизнь
|
|
6800
|
+
* процесса и дальше молча пропускаем гашение (надзор в остальном работает как раньше).
|
|
6801
|
+
* Тот же приём, что у notifyAnomaly с аргументом `level`.
|
|
6802
|
+
*/
|
|
6803
|
+
async resolveAnomalies(keys) {
|
|
6804
|
+
const tags = [...new Set(keys.filter((k) => k.length > 0 && k.length <= 120))];
|
|
6805
|
+
if (tags.length === 0 || this.notifyResolveUnsupported) return;
|
|
6806
|
+
const res = await this.call("swarm_notify_resolve", { tags });
|
|
6807
|
+
if (!res?.isError) return;
|
|
6808
|
+
const text = JSON.stringify(res);
|
|
6809
|
+
if (text.includes("swarm_notify_resolve") && (text.includes("Unknown tool") || text.includes("\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D"))) {
|
|
6810
|
+
this.notifyResolveUnsupported = true;
|
|
6811
|
+
console.warn(
|
|
6812
|
+
"[swarm-client] \u043F\u0440\u043E\u0434 \u0441\u0442\u0430\u0440\u0435\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 (\u043D\u0435\u0442 swarm_notify_resolve) \u2014 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0432\u0448\u0438\u0435\u0441\u044F \u0442\u0440\u0435\u0432\u043E\u0433\u0438 \u0440\u043E\u044F \u043F\u0440\u0438\u0434\u0451\u0442\u0441\u044F \u0433\u0430\u0441\u0438\u0442\u044C \u0432 \u0438\u043D\u0431\u043E\u043A\u0441\u0435 \u0440\u0443\u043A\u0430\u043C\u0438. \u041E\u0431\u043D\u043E\u0432\u0438 \u043F\u0440\u043E\u0434 \u0438 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438 \u0434\u0435\u043C\u043E\u043D."
|
|
6813
|
+
);
|
|
6814
|
+
return;
|
|
6815
|
+
}
|
|
6816
|
+
throw new Error(`swarm_notify_resolve \u043E\u0442\u0432\u0435\u0440\u0433 ${tags.length} \u043A\u043B\u044E\u0447(\u0435\u0439)`);
|
|
6817
|
+
}
|
|
5724
6818
|
/**
|
|
5725
6819
|
* TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
|
|
5726
6820
|
* через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
|
|
@@ -5785,6 +6879,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
5785
6879
|
depends_on: [],
|
|
5786
6880
|
model: null,
|
|
5787
6881
|
cost_usd: 0,
|
|
6882
|
+
tokens_in: 0,
|
|
6883
|
+
// токены и время работы (TASWM-353) — прод-поля, заполняются ниже
|
|
6884
|
+
tokens_out: 0,
|
|
6885
|
+
duration_ms: 0,
|
|
5788
6886
|
pr_url: null,
|
|
5789
6887
|
superseded_by: null,
|
|
5790
6888
|
decision: null,
|
|
@@ -5795,9 +6893,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
5795
6893
|
review_verdict: null,
|
|
5796
6894
|
review_phase: null,
|
|
5797
6895
|
// вычисляемая фаза ревью (TASWM-230) — прод хранит и раздаёт
|
|
5798
|
-
// Локальная бухгалтерия (в прод-схему не
|
|
5799
|
-
//
|
|
5800
|
-
//
|
|
6896
|
+
// Локальная бухгалтерия (в прод-схему не уходит) — на гидрации из прода дефолт 0;
|
|
6897
|
+
// существующая локальная запись НЕ перетирается (поле не в field-copy ниже),
|
|
6898
|
+
// в отличие от respawns, — авто-ревью v2, TASWM-162.
|
|
5801
6899
|
review_cycle: 0,
|
|
5802
6900
|
task_title: null,
|
|
5803
6901
|
task_desc: null,
|
|
@@ -5820,8 +6918,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
5820
6918
|
this.runs.set(id, local);
|
|
5821
6919
|
this.keyByLocal.set(id, runKey);
|
|
5822
6920
|
}
|
|
5823
|
-
const
|
|
5824
|
-
local.task_slug =
|
|
6921
|
+
const prodSlug = pr.taskSlug ?? pr.task?.slug ?? null;
|
|
6922
|
+
if (prodSlug) local.task_slug = prodSlug;
|
|
6923
|
+
else if (!isTaskSlug(local.task_slug)) local.task_slug = pr.taskId || local.task_slug;
|
|
5825
6924
|
local.task_title = pr.taskTitle ?? local.task_title;
|
|
5826
6925
|
local.task_desc = pr.taskDesc ?? local.task_desc;
|
|
5827
6926
|
local.name = pr.droneName ?? local.name;
|
|
@@ -5848,6 +6947,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
5848
6947
|
local.depends_on = Array.isArray(pr.dependsOn) ? pr.dependsOn : [];
|
|
5849
6948
|
local.model = pr.model ?? null;
|
|
5850
6949
|
local.cost_usd = typeof pr.costUsd === "number" ? pr.costUsd : 0;
|
|
6950
|
+
local.tokens_in = typeof pr.tokensIn === "number" ? pr.tokensIn : 0;
|
|
6951
|
+
local.tokens_out = typeof pr.tokensOut === "number" ? pr.tokensOut : 0;
|
|
6952
|
+
local.duration_ms = typeof pr.durationMs === "number" ? pr.durationMs : 0;
|
|
5851
6953
|
local.pr_url = pr.prUrl ?? null;
|
|
5852
6954
|
local.stop_reason = pr.stopReason ?? null;
|
|
5853
6955
|
local.respawns = typeof pr.respawns === "number" ? pr.respawns : 0;
|
|
@@ -6105,13 +7207,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
6105
7207
|
}
|
|
6106
7208
|
persistPending() {
|
|
6107
7209
|
try {
|
|
6108
|
-
|
|
7210
|
+
writeFileSync4(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
|
|
6109
7211
|
} catch {
|
|
6110
7212
|
}
|
|
6111
7213
|
}
|
|
6112
7214
|
persistMap() {
|
|
6113
7215
|
try {
|
|
6114
|
-
|
|
7216
|
+
writeFileSync4(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
|
|
6115
7217
|
} catch {
|
|
6116
7218
|
}
|
|
6117
7219
|
}
|
|
@@ -6140,6 +7242,66 @@ function parseDecision(v) {
|
|
|
6140
7242
|
init_config();
|
|
6141
7243
|
init_heartbeat();
|
|
6142
7244
|
|
|
7245
|
+
// src/logRotate.ts
|
|
7246
|
+
import { copyFileSync, renameSync as renameSync4, statSync as statSync4, fstatSync, truncateSync, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
|
|
7247
|
+
var realIo = {
|
|
7248
|
+
size: (path) => {
|
|
7249
|
+
try {
|
|
7250
|
+
return statSync4(path).size;
|
|
7251
|
+
} catch {
|
|
7252
|
+
return null;
|
|
7253
|
+
}
|
|
7254
|
+
},
|
|
7255
|
+
copy: copyFileSync,
|
|
7256
|
+
rename: renameSync4,
|
|
7257
|
+
truncate: (path) => truncateSync(path, 0),
|
|
7258
|
+
unlink: unlinkSync2,
|
|
7259
|
+
exists: existsSync6
|
|
7260
|
+
};
|
|
7261
|
+
var realOwnerIo = {
|
|
7262
|
+
statFd: (fd) => {
|
|
7263
|
+
try {
|
|
7264
|
+
const s = fstatSync(fd);
|
|
7265
|
+
return { dev: s.dev, ino: s.ino, isFile: s.isFile() };
|
|
7266
|
+
} catch {
|
|
7267
|
+
return null;
|
|
7268
|
+
}
|
|
7269
|
+
},
|
|
7270
|
+
statPath: (path) => {
|
|
7271
|
+
try {
|
|
7272
|
+
const s = statSync4(path);
|
|
7273
|
+
return { dev: s.dev, ino: s.ino };
|
|
7274
|
+
} catch {
|
|
7275
|
+
return null;
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
};
|
|
7279
|
+
function stdoutGoesTo(path, io = realOwnerIo) {
|
|
7280
|
+
const out = io.statFd(1);
|
|
7281
|
+
if (!out || !out.isFile) return false;
|
|
7282
|
+
const file = io.statPath(path);
|
|
7283
|
+
return file != null && file.dev === out.dev && file.ino === out.ino;
|
|
7284
|
+
}
|
|
7285
|
+
var STRAY_GENERATIONS = 8;
|
|
7286
|
+
function rotateDaemonLog(path, opts, io = realIo) {
|
|
7287
|
+
const size = io.size(path);
|
|
7288
|
+
if (size == null) return "missing";
|
|
7289
|
+
if (size <= opts.maxBytes) return "kept";
|
|
7290
|
+
const keep = Math.max(0, Math.trunc(opts.keep));
|
|
7291
|
+
const gen = (n) => `${path}.${n}`;
|
|
7292
|
+
for (let i = keep; i >= 1; i--) {
|
|
7293
|
+
if (!io.exists(gen(i))) continue;
|
|
7294
|
+
if (i === keep) io.unlink(gen(i));
|
|
7295
|
+
else io.rename(gen(i), gen(i + 1));
|
|
7296
|
+
}
|
|
7297
|
+
for (let i = keep + 1; i <= keep + STRAY_GENERATIONS; i++) {
|
|
7298
|
+
if (io.exists(gen(i))) io.unlink(gen(i));
|
|
7299
|
+
}
|
|
7300
|
+
if (keep >= 1) io.copy(path, gen(1));
|
|
7301
|
+
io.truncate(path);
|
|
7302
|
+
return "rotated";
|
|
7303
|
+
}
|
|
7304
|
+
|
|
6143
7305
|
// src/repoRegistry.ts
|
|
6144
7306
|
init_config();
|
|
6145
7307
|
import { $ } from "execa";
|
|
@@ -6178,13 +7340,13 @@ async function buildRepoRegistry(paths) {
|
|
|
6178
7340
|
}
|
|
6179
7341
|
|
|
6180
7342
|
// src/repoMemory.ts
|
|
6181
|
-
import { readFileSync as readFileSync6, writeFileSync as
|
|
6182
|
-
import { join as
|
|
7343
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
7344
|
+
import { join as join7 } from "path";
|
|
6183
7345
|
var FILE = "repos.json";
|
|
6184
7346
|
function loadRememberedRepos(home) {
|
|
6185
7347
|
let raw;
|
|
6186
7348
|
try {
|
|
6187
|
-
raw = readFileSync6(
|
|
7349
|
+
raw = readFileSync6(join7(home, FILE), "utf8");
|
|
6188
7350
|
} catch {
|
|
6189
7351
|
return [];
|
|
6190
7352
|
}
|
|
@@ -6199,7 +7361,7 @@ function loadRememberedRepos(home) {
|
|
|
6199
7361
|
function rememberRepos(home, paths, now = () => /* @__PURE__ */ new Date()) {
|
|
6200
7362
|
const clean = paths.filter((p) => p.trim().length > 0);
|
|
6201
7363
|
if (clean.length === 0) return;
|
|
6202
|
-
|
|
7364
|
+
writeFileSync6(join7(home, FILE), `${JSON.stringify({ paths: clean, at: now().toISOString() }, null, 2)}
|
|
6203
7365
|
`);
|
|
6204
7366
|
}
|
|
6205
7367
|
function pickRepoPaths(args) {
|
|
@@ -6209,9 +7371,16 @@ function pickRepoPaths(args) {
|
|
|
6209
7371
|
return { paths: [], source: "none" };
|
|
6210
7372
|
}
|
|
6211
7373
|
|
|
7374
|
+
// src/repoDiscovery.ts
|
|
7375
|
+
init_src();
|
|
7376
|
+
init_config();
|
|
7377
|
+
import { homedir as homedir3 } from "os";
|
|
7378
|
+
import { readFileSync as readFileSync8, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
7379
|
+
import { isAbsolute, join as join9, resolve as resolve2 } from "path";
|
|
7380
|
+
|
|
6212
7381
|
// src/repoScan.ts
|
|
6213
|
-
import { readdirSync as
|
|
6214
|
-
import { join as
|
|
7382
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync5 } from "fs";
|
|
7383
|
+
import { join as join8 } from "path";
|
|
6215
7384
|
var SKIP = /* @__PURE__ */ new Set([
|
|
6216
7385
|
"node_modules",
|
|
6217
7386
|
"vendor",
|
|
@@ -6241,12 +7410,16 @@ function parseOriginFromGitConfig(text) {
|
|
|
6241
7410
|
}
|
|
6242
7411
|
function originOf(dir) {
|
|
6243
7412
|
try {
|
|
6244
|
-
return parseOriginFromGitConfig(readFileSync7(
|
|
7413
|
+
return parseOriginFromGitConfig(readFileSync7(join8(dir, ".git", "config"), "utf8"));
|
|
6245
7414
|
} catch {
|
|
6246
7415
|
return null;
|
|
6247
7416
|
}
|
|
6248
7417
|
}
|
|
6249
|
-
function
|
|
7418
|
+
function repoFullNameAt(dir) {
|
|
7419
|
+
const origin = originOf(dir);
|
|
7420
|
+
return origin ? parseRepoFullName(origin) : null;
|
|
7421
|
+
}
|
|
7422
|
+
function scanRootsAll(roots, opts = {}) {
|
|
6250
7423
|
const maxDepth = opts.maxDepth ?? 3;
|
|
6251
7424
|
const found = /* @__PURE__ */ new Map();
|
|
6252
7425
|
for (const root of roots) walk(root, 0, maxDepth, found);
|
|
@@ -6257,20 +7430,24 @@ function walk(dir, depth, maxDepth, found) {
|
|
|
6257
7430
|
const origin = originOf(dir);
|
|
6258
7431
|
if (origin) {
|
|
6259
7432
|
const full = parseRepoFullName(origin);
|
|
6260
|
-
if (full
|
|
7433
|
+
if (full) {
|
|
7434
|
+
const paths = found.get(full) ?? [];
|
|
7435
|
+
if (!paths.includes(dir)) paths.push(dir);
|
|
7436
|
+
found.set(full, paths);
|
|
7437
|
+
}
|
|
6261
7438
|
return;
|
|
6262
7439
|
}
|
|
6263
7440
|
let entries;
|
|
6264
7441
|
try {
|
|
6265
|
-
entries =
|
|
7442
|
+
entries = readdirSync5(dir);
|
|
6266
7443
|
} catch {
|
|
6267
7444
|
return;
|
|
6268
7445
|
}
|
|
6269
7446
|
for (const name of entries) {
|
|
6270
7447
|
if (name.startsWith(".") || SKIP.has(name)) continue;
|
|
6271
|
-
const child =
|
|
7448
|
+
const child = join8(dir, name);
|
|
6272
7449
|
try {
|
|
6273
|
-
if (!
|
|
7450
|
+
if (!statSync5(child).isDirectory()) continue;
|
|
6274
7451
|
} catch {
|
|
6275
7452
|
continue;
|
|
6276
7453
|
}
|
|
@@ -6290,25 +7467,178 @@ function defaultScanRoots(home, knownPaths = []) {
|
|
|
6290
7467
|
if (parent && parent !== p) add(parent);
|
|
6291
7468
|
}
|
|
6292
7469
|
for (const name of ["projects", "code", "src", "dev", "work", "repos", "git", "Projects", "Code", "Developer", "Documents/GitHub", "go/src"]) {
|
|
6293
|
-
add(
|
|
7470
|
+
add(join8(home, name));
|
|
6294
7471
|
}
|
|
6295
7472
|
return out;
|
|
6296
7473
|
}
|
|
6297
7474
|
|
|
7475
|
+
// src/repoDiscovery.ts
|
|
7476
|
+
function resolveScanRoots(raw, home = homedir3(), knownPaths = []) {
|
|
7477
|
+
const v = (raw ?? "").trim();
|
|
7478
|
+
if (!v) return defaultScanRoots(home, knownPaths);
|
|
7479
|
+
if (["off", "none", "0"].includes(v.toLowerCase())) return [];
|
|
7480
|
+
const out = [];
|
|
7481
|
+
for (const part of v.split(",")) {
|
|
7482
|
+
const p = part.trim();
|
|
7483
|
+
if (!p) continue;
|
|
7484
|
+
const abs = p === "~" ? home : p.startsWith("~/") ? join9(home, p.slice(2)) : isAbsolute(p) ? p : resolve2(home, p);
|
|
7485
|
+
if (!out.includes(abs)) out.push(abs);
|
|
7486
|
+
}
|
|
7487
|
+
return out;
|
|
7488
|
+
}
|
|
7489
|
+
function normalizeFullNames(list) {
|
|
7490
|
+
const out = [];
|
|
7491
|
+
for (const raw of list) {
|
|
7492
|
+
const v = (raw ?? "").trim().toLowerCase();
|
|
7493
|
+
if (v && !out.includes(v)) out.push(v);
|
|
7494
|
+
}
|
|
7495
|
+
return out;
|
|
7496
|
+
}
|
|
7497
|
+
async function fetchSwarmRepos(call) {
|
|
7498
|
+
let res;
|
|
7499
|
+
try {
|
|
7500
|
+
res = await call("swarm_my_repos", {});
|
|
7501
|
+
} catch {
|
|
7502
|
+
return null;
|
|
7503
|
+
}
|
|
7504
|
+
if (!res || res.isError) return null;
|
|
7505
|
+
const raw = res.structuredContent?.repos;
|
|
7506
|
+
if (!Array.isArray(raw)) return null;
|
|
7507
|
+
const out = [];
|
|
7508
|
+
for (const item of raw) {
|
|
7509
|
+
if (!item || typeof item !== "object") continue;
|
|
7510
|
+
const r = item;
|
|
7511
|
+
const repoFullName = typeof r.repoFullName === "string" ? r.repoFullName.trim().toLowerCase() : "";
|
|
7512
|
+
if (!repoFullName) continue;
|
|
7513
|
+
out.push({
|
|
7514
|
+
repoFullName,
|
|
7515
|
+
taskPrefix: typeof r.taskPrefix === "string" ? r.taskPrefix : null,
|
|
7516
|
+
workspaceSlug: typeof r.workspaceSlug === "string" ? r.workspaceSlug : ""
|
|
7517
|
+
});
|
|
7518
|
+
}
|
|
7519
|
+
return out;
|
|
7520
|
+
}
|
|
7521
|
+
function emptyRepoMapState(roots) {
|
|
7522
|
+
return { clones: [], missing: [], roots: [...roots], at: null, unknown: true };
|
|
7523
|
+
}
|
|
7524
|
+
async function refreshRepoMap(opts) {
|
|
7525
|
+
const log = opts.log ?? (() => {
|
|
7526
|
+
});
|
|
7527
|
+
const scan = opts.scan ?? scanRootsAll;
|
|
7528
|
+
const repoAt = opts.repoAt ?? repoFullNameAt;
|
|
7529
|
+
const wanted = await fetchSwarmRepos(opts.call);
|
|
7530
|
+
const clones = /* @__PURE__ */ new Map();
|
|
7531
|
+
const take = (repoFullName, path, source, alternatives = []) => {
|
|
7532
|
+
if (clones.has(repoFullName)) return;
|
|
7533
|
+
clones.set(repoFullName, { repoFullName, path, source, alternatives });
|
|
7534
|
+
};
|
|
7535
|
+
for (const e of opts.configured) take(e.repoFullName.toLowerCase(), e.path, "configured");
|
|
7536
|
+
for (const e of readRepoMapCache(opts.cachePath)) {
|
|
7537
|
+
const name = e.repoFullName.toLowerCase();
|
|
7538
|
+
if (clones.has(name)) continue;
|
|
7539
|
+
if (repoAt(e.path) === name) take(name, e.path, "cache");
|
|
7540
|
+
}
|
|
7541
|
+
const wantedNames = wanted ? normalizeFullNames(wanted.map((r) => r.repoFullName)) : [];
|
|
7542
|
+
const needScan = wanted ? wantedNames.some((n) => !clones.has(n)) : clones.size === 0;
|
|
7543
|
+
if (needScan) {
|
|
7544
|
+
const startedAt = Date.now();
|
|
7545
|
+
const scanned = scan(opts.roots, opts.maxDepth !== void 0 ? { maxDepth: opts.maxDepth } : {});
|
|
7546
|
+
let added = 0;
|
|
7547
|
+
for (const [full, paths] of scanned) {
|
|
7548
|
+
if (clones.has(full) || !paths.length) continue;
|
|
7549
|
+
take(full, paths[0], "scan", paths.slice(1));
|
|
7550
|
+
added++;
|
|
7551
|
+
}
|
|
7552
|
+
if (added > 0) {
|
|
7553
|
+
log(`[overmind] \u043D\u0430\u0448\u0451\u043B \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 ${added} \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439(\u0435\u0432) \u0441\u043A\u0430\u043D\u043E\u043C \u0437\u0430 ${Date.now() - startedAt}\u043C\u0441`);
|
|
7554
|
+
}
|
|
7555
|
+
}
|
|
7556
|
+
const registry = [...clones.values()].map((c) => ({
|
|
7557
|
+
repoFullName: c.repoFullName,
|
|
7558
|
+
path: normalizeRepo(c.path)
|
|
7559
|
+
}));
|
|
7560
|
+
writeRepoMapCache(
|
|
7561
|
+
opts.cachePath,
|
|
7562
|
+
[...clones.values()].map((c) => ({ repoFullName: c.repoFullName, path: c.path }))
|
|
7563
|
+
);
|
|
7564
|
+
if (!wanted) {
|
|
7565
|
+
log(
|
|
7566
|
+
"[overmind] \u043F\u0440\u043E\u0434 \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B, \u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443 \u0440\u043E\u044F (swarm_my_repos) \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u043F\u043E \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u043C\u0443 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435. \u0421\u0435\u0440\u0432\u0435\u0440 \u0441\u0442\u0430\u0440\u0435\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u044D\u0442\u043E \u043E\u0436\u0438\u0434\u0430\u0435\u043C\u043E \u0438 \u043B\u0435\u0447\u0438\u0442\u0441\u044F \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435\u043C \u043F\u0440\u043E\u0434\u0430."
|
|
7567
|
+
);
|
|
7568
|
+
return registry;
|
|
7569
|
+
}
|
|
7570
|
+
opts.state.unknown = false;
|
|
7571
|
+
opts.state.clones = wantedNames.map((n) => clones.get(n)).filter((c) => !!c);
|
|
7572
|
+
opts.state.missing = wantedNames.filter((n) => !clones.has(n));
|
|
7573
|
+
opts.state.roots = [...opts.roots];
|
|
7574
|
+
opts.state.at = stamp();
|
|
7575
|
+
if (wantedNames.length === 0) {
|
|
7576
|
+
log("[overmind] \u0443 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043D\u0435\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 \u0441 \u0432\u043A\u043B\u044E\u0447\u0451\u043D\u043D\u044B\u043C \u0440\u043E\u0435\u043C \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u043F\u043E \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u043C\u0443 \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435");
|
|
7577
|
+
return registry;
|
|
7578
|
+
}
|
|
7579
|
+
log(`[overmind] \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0440\u043E\u044F (${wantedNames.length}), \u043A\u0430\u0440\u0442\u0430 \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435:`);
|
|
7580
|
+
for (const c of opts.state.clones) log(`[overmind] ${describeClone(c)}`);
|
|
7581
|
+
const prefixOf = new Map(wanted.map((r) => [r.repoFullName, r.taskPrefix]));
|
|
7582
|
+
for (const name of opts.state.missing) {
|
|
7583
|
+
const prefix = prefixOf.get(name);
|
|
7584
|
+
log(`[overmind] ${describeMissing(prefix ? `${name} (\u043F\u0440\u043E\u0435\u043A\u0442 ${prefix})` : name, opts.roots)}`);
|
|
7585
|
+
}
|
|
7586
|
+
return registry;
|
|
7587
|
+
}
|
|
7588
|
+
function describeClone(c) {
|
|
7589
|
+
const how = c.source === "configured" ? "\u043F\u0443\u0442\u044C \u0437\u0430\u0434\u0430\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C" : c.source === "cache" ? "\u0438\u0437 \u043A\u044D\u0448\u0430 \u043C\u0430\u0448\u0438\u043D\u044B" : "\u043D\u0430\u0439\u0434\u0435\u043D \u0441\u043A\u0430\u043D\u043E\u043C";
|
|
7590
|
+
const alt = c.alternatives.length ? ` \u26A0 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u0435\u0449\u0451 ${c.alternatives.length}: ${c.alternatives.join(", ")} \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u044E \u0432 \u043F\u0435\u0440\u0432\u043E\u043C \u043F\u043E \u043F\u043E\u0440\u044F\u0434\u043A\u0443 \u043A\u043E\u0440\u043D\u0435\u0439` : "";
|
|
7591
|
+
return `${c.repoFullName} \u2192 ${c.path} (${how})${alt}`;
|
|
7592
|
+
}
|
|
7593
|
+
function describeMissing(repoFullName, roots) {
|
|
7594
|
+
const where = roots.length ? `\u0438\u0441\u043A\u0430\u043B \u0432: ${roots.slice(0, 6).join(", ")}` : "\u043F\u043E\u0438\u0441\u043A \u043F\u043E \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430\u043C \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D (SWARM_SCAN_ROOTS)";
|
|
7595
|
+
return `${repoFullName} \u2014 \u041A\u041B\u041E\u041D\u0410 \u041D\u0410 \u042D\u0422\u041E\u0419 \u041C\u0410\u0428\u0418\u041D\u0415 \u041D\u0415\u0422 (${where}). \u0421\u0430\u043C \u043D\u0435 \u043A\u043B\u043E\u043D\u0438\u0440\u0443\u044E: \u0441\u043A\u043B\u043E\u043D\u0438\u0440\u0443\u0439 \u0440\u0443\u043A\u0430\u043C\u0438 \u043B\u0438\u0431\u043E \u0443\u043A\u0430\u0436\u0438 \u043F\u0443\u0442\u044C \`swarm start --repo <\u043F\u0443\u0442\u044C>\`.`;
|
|
7596
|
+
}
|
|
7597
|
+
function repoMapRows(state) {
|
|
7598
|
+
return [
|
|
7599
|
+
...state.clones.map((c) => ({
|
|
7600
|
+
repoFullName: c.repoFullName,
|
|
7601
|
+
path: c.path,
|
|
7602
|
+
source: c.source,
|
|
7603
|
+
alternatives: c.alternatives
|
|
7604
|
+
})),
|
|
7605
|
+
...state.missing.map((repoFullName) => ({ repoFullName, path: null, source: null, alternatives: [] }))
|
|
7606
|
+
];
|
|
7607
|
+
}
|
|
7608
|
+
function readRepoMapCache(path) {
|
|
7609
|
+
try {
|
|
7610
|
+
const raw = JSON.parse(readFileSync8(path, "utf8"));
|
|
7611
|
+
if (!Array.isArray(raw?.entries)) return [];
|
|
7612
|
+
return raw.entries.filter(
|
|
7613
|
+
(e) => !!e && typeof e.repoFullName === "string" && typeof e.path === "string" && !!e.repoFullName && !!e.path
|
|
7614
|
+
);
|
|
7615
|
+
} catch {
|
|
7616
|
+
return [];
|
|
7617
|
+
}
|
|
7618
|
+
}
|
|
7619
|
+
function writeRepoMapCache(path, entries) {
|
|
7620
|
+
try {
|
|
7621
|
+
const tmp = `${path}.tmp`;
|
|
7622
|
+
writeFileSync7(tmp, JSON.stringify({ at: stamp(), entries }, null, 2));
|
|
7623
|
+
renameSync5(tmp, path);
|
|
7624
|
+
} catch {
|
|
7625
|
+
}
|
|
7626
|
+
}
|
|
7627
|
+
|
|
6298
7628
|
// src/hostIdentity.ts
|
|
6299
7629
|
import { hostname } from "os";
|
|
6300
|
-
import { existsSync as
|
|
6301
|
-
import { join as
|
|
7630
|
+
import { existsSync as existsSync7, readFileSync as readFileSync9, renameSync as renameSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
7631
|
+
import { join as join10 } from "path";
|
|
6302
7632
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
6303
7633
|
function hostIdentityPath(home) {
|
|
6304
|
-
return
|
|
7634
|
+
return join10(home, "host.json");
|
|
6305
7635
|
}
|
|
6306
7636
|
function loadHostIdentity(deps) {
|
|
6307
|
-
const exists = deps.exists ??
|
|
6308
|
-
const readFile2 = deps.readFile ?? ((p) =>
|
|
7637
|
+
const exists = deps.exists ?? existsSync7;
|
|
7638
|
+
const readFile2 = deps.readFile ?? ((p) => readFileSync9(p, "utf8"));
|
|
6309
7639
|
const writeFile2 = deps.writeFile ?? ((p, data) => {
|
|
6310
|
-
|
|
6311
|
-
|
|
7640
|
+
writeFileSync8(`${p}.tmp`, data);
|
|
7641
|
+
renameSync6(`${p}.tmp`, p);
|
|
6312
7642
|
});
|
|
6313
7643
|
const host = deps.hostname ?? hostname;
|
|
6314
7644
|
const newId = deps.newId ?? randomUUID2;
|
|
@@ -6481,96 +7811,24 @@ function reliefAlertBody(cost, pressure, advice, windowMs, minSteps) {
|
|
|
6481
7811
|
return body.length <= MAX_ALERT_BODY_CHARS ? body : `${body.slice(0, MAX_ALERT_BODY_CHARS - 1)}\u2026`;
|
|
6482
7812
|
}
|
|
6483
7813
|
function reliefAlertTitle(cost, windowMs) {
|
|
6484
|
-
return `\u041C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u0442\u044F\u043D\u0435\u0442 \u0440\u043E\u0439: ${cost.pauses} \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043E\u043A \u0437\u0430 ${humanMinutes(windowMs)}, \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E ${humanMinutes(cost.lostMs)}`;
|
|
6485
|
-
}
|
|
6486
|
-
function buildReliefCostCandidates(cost, t, sustainMs, build) {
|
|
6487
|
-
if (!reliefAlertDue(cost, t)) return [];
|
|
6488
|
-
return [{ key: "relief_cost", minDurationMs: sustainMs, build }];
|
|
6489
|
-
}
|
|
6490
|
-
|
|
6491
|
-
// src/launcher.ts
|
|
6492
|
-
import { spawn } from "child_process";
|
|
6493
|
-
import { closeSync as closeSync2, existsSync as existsSync6, mkdirSync as mkdirSync4, openSync as openSync2, readdirSync as readdirSync5, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
6494
|
-
import { createRequire } from "module";
|
|
6495
|
-
import { homedir as homedir2 } from "os";
|
|
6496
|
-
import { dirname as dirname2, join as join8 } from "path";
|
|
6497
|
-
import { fileURLToPath } from "url";
|
|
6498
|
-
|
|
6499
|
-
// src/claudeBin.ts
|
|
6500
|
-
import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
|
|
6501
|
-
import { win32 as winPath } from "path";
|
|
6502
|
-
var CLAUDE_FALLBACK = "claude";
|
|
6503
|
-
function compareVersionDirs(a, b) {
|
|
6504
|
-
const parts = (s) => s.split(".").map((p) => Number.isFinite(Number(p)) ? Number(p) : -1);
|
|
6505
|
-
const pa = parts(a);
|
|
6506
|
-
const pb = parts(b);
|
|
6507
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
6508
|
-
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
6509
|
-
if (d !== 0) return d;
|
|
6510
|
-
}
|
|
6511
|
-
return 0;
|
|
6512
|
-
}
|
|
6513
|
-
function realListDir(dir) {
|
|
6514
|
-
try {
|
|
6515
|
-
return readdirSync3(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
6516
|
-
} catch {
|
|
6517
|
-
return [];
|
|
6518
|
-
}
|
|
6519
|
-
}
|
|
6520
|
-
function winVersionRoots(env, listDir) {
|
|
6521
|
-
const roots = [];
|
|
6522
|
-
const appData = env.APPDATA;
|
|
6523
|
-
if (appData) roots.push(winPath.join(appData, "Claude", "claude-code"));
|
|
6524
|
-
const localAppData = env.LOCALAPPDATA;
|
|
6525
|
-
if (localAppData) {
|
|
6526
|
-
const packages = winPath.join(localAppData, "Packages");
|
|
6527
|
-
for (const name of listDir(packages)) {
|
|
6528
|
-
if (!name.startsWith("Claude")) continue;
|
|
6529
|
-
roots.push(winPath.join(packages, name, "LocalCache", "Roaming", "Claude", "claude-code"));
|
|
6530
|
-
}
|
|
6531
|
-
}
|
|
6532
|
-
return roots;
|
|
6533
|
-
}
|
|
6534
|
-
function findWindowsClaude(opts = {}) {
|
|
6535
|
-
const env = opts.env ?? process.env;
|
|
6536
|
-
const listDir = opts.listDir ?? realListDir;
|
|
6537
|
-
const fileExists = opts.fileExists ?? existsSync5;
|
|
6538
|
-
let best = null;
|
|
6539
|
-
for (const root of winVersionRoots(env, listDir)) {
|
|
6540
|
-
for (const version of listDir(root)) {
|
|
6541
|
-
const candidate = winPath.join(root, version, "claude.exe");
|
|
6542
|
-
if (!fileExists(candidate)) continue;
|
|
6543
|
-
if (!best || compareVersionDirs(version, best.version) > 0) best = { version, path: candidate };
|
|
6544
|
-
}
|
|
6545
|
-
}
|
|
6546
|
-
return best?.path ?? null;
|
|
6547
|
-
}
|
|
6548
|
-
var warnedNoWindowsClaude = false;
|
|
6549
|
-
var cachedWindowsBin = null;
|
|
6550
|
-
var windowsScanDone = false;
|
|
6551
|
-
function resolveClaudeBin(opts = {}) {
|
|
6552
|
-
const env = opts.env ?? process.env;
|
|
6553
|
-
const platform = opts.platform ?? process.platform;
|
|
6554
|
-
const explicit = (env.SWARM_CLAUDE_BIN ?? "").trim();
|
|
6555
|
-
if (explicit) return explicit;
|
|
6556
|
-
if (platform !== "win32") return CLAUDE_FALLBACK;
|
|
6557
|
-
if (windowsScanDone) return cachedWindowsBin ?? CLAUDE_FALLBACK;
|
|
6558
|
-
windowsScanDone = true;
|
|
6559
|
-
const found = findWindowsClaude({ ...opts, env });
|
|
6560
|
-
cachedWindowsBin = found;
|
|
6561
|
-
if (found) return found;
|
|
6562
|
-
if (!warnedNoWindowsClaude) {
|
|
6563
|
-
warnedNoWindowsClaude = true;
|
|
6564
|
-
console.warn(
|
|
6565
|
-
"[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."
|
|
6566
|
-
);
|
|
6567
|
-
}
|
|
6568
|
-
return CLAUDE_FALLBACK;
|
|
7814
|
+
return `\u041C\u0430\u0448\u0438\u043D\u0430 \u043D\u0435 \u0442\u044F\u043D\u0435\u0442 \u0440\u043E\u0439: ${cost.pauses} \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043E\u043A \u0437\u0430 ${humanMinutes(windowMs)}, \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u043E ${humanMinutes(cost.lostMs)}`;
|
|
7815
|
+
}
|
|
7816
|
+
function buildReliefCostCandidates(cost, t, sustainMs, build) {
|
|
7817
|
+
if (!reliefAlertDue(cost, t)) return [];
|
|
7818
|
+
return [{ key: "relief_cost", minDurationMs: sustainMs, build }];
|
|
6569
7819
|
}
|
|
6570
7820
|
|
|
7821
|
+
// src/launcher.ts
|
|
7822
|
+
import { spawn } from "child_process";
|
|
7823
|
+
import { closeSync as closeSync2, existsSync as existsSync8, mkdirSync as mkdirSync6, openSync as openSync2, readdirSync as readdirSync7 } from "fs";
|
|
7824
|
+
import { createRequire } from "module";
|
|
7825
|
+
import { homedir as homedir4 } from "os";
|
|
7826
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
7827
|
+
import { fileURLToPath } from "url";
|
|
7828
|
+
|
|
6571
7829
|
// src/droneHome.ts
|
|
6572
|
-
import { lstatSync, mkdirSync as
|
|
6573
|
-
import { join as
|
|
7830
|
+
import { lstatSync, mkdirSync as mkdirSync5, readdirSync as readdirSync6, rmSync, statSync as statSync6, symlinkSync as symlinkSync2 } from "fs";
|
|
7831
|
+
import { join as join11, relative, sep } from "path";
|
|
6574
7832
|
var DRONE_HOME_DENY_DEFAULT = [".taskless-swarm", ".bash_history", ".zsh_history"];
|
|
6575
7833
|
function droneHomeDeny(env = process.env) {
|
|
6576
7834
|
const extra = (env.SWARM_DRONE_HOME_DENY || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -6588,34 +7846,34 @@ function buildDroneHome(opts) {
|
|
|
6588
7846
|
const { realHome, droneHome } = opts;
|
|
6589
7847
|
const deny = opts.deny ?? droneHomeDeny();
|
|
6590
7848
|
const platform = opts.platform ?? process.platform;
|
|
6591
|
-
|
|
6592
|
-
const wanted = new Set(mirroredEntries(
|
|
7849
|
+
mkdirSync5(droneHome, { recursive: true, mode: 448 });
|
|
7850
|
+
const wanted = new Set(mirroredEntries(readdirSync6(realHome), deny, platform));
|
|
6593
7851
|
const rel = relative(realHome, droneHome);
|
|
6594
7852
|
if (rel && !rel.startsWith("..") && !rel.includes(sep)) wanted.delete(rel);
|
|
6595
|
-
for (const existing of
|
|
7853
|
+
for (const existing of readdirSync6(droneHome)) {
|
|
6596
7854
|
if (wanted.has(existing)) continue;
|
|
6597
|
-
const p =
|
|
7855
|
+
const p = join11(droneHome, existing);
|
|
6598
7856
|
try {
|
|
6599
7857
|
if (lstatSync(p).isSymbolicLink()) rmSync(p);
|
|
6600
7858
|
} catch {
|
|
6601
7859
|
}
|
|
6602
7860
|
}
|
|
6603
7861
|
for (const name of wanted) {
|
|
6604
|
-
const link =
|
|
7862
|
+
const link = join11(droneHome, name);
|
|
6605
7863
|
try {
|
|
6606
7864
|
lstatSync(link);
|
|
6607
7865
|
continue;
|
|
6608
7866
|
} catch {
|
|
6609
7867
|
}
|
|
6610
|
-
const target =
|
|
7868
|
+
const target = join11(realHome, name);
|
|
6611
7869
|
let isDir;
|
|
6612
7870
|
try {
|
|
6613
|
-
isDir =
|
|
7871
|
+
isDir = statSync6(target).isDirectory();
|
|
6614
7872
|
} catch {
|
|
6615
7873
|
continue;
|
|
6616
7874
|
}
|
|
6617
7875
|
try {
|
|
6618
|
-
|
|
7876
|
+
symlinkSync2(target, link, isDir ? "junction" : "file");
|
|
6619
7877
|
} catch (e) {
|
|
6620
7878
|
if (e.code !== "EEXIST") throw e;
|
|
6621
7879
|
}
|
|
@@ -6628,10 +7886,10 @@ var REAL_PATH_ENV_PINS = [
|
|
|
6628
7886
|
{ env: "YARN_GLOBAL_FOLDER", segments: [".yarn", "berry"] }
|
|
6629
7887
|
];
|
|
6630
7888
|
function realPathEnvPins(realHome) {
|
|
6631
|
-
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env,
|
|
7889
|
+
return Object.fromEntries(REAL_PATH_ENV_PINS.map(({ env, segments }) => [env, join11(realHome, ...segments)]));
|
|
6632
7890
|
}
|
|
6633
|
-
function
|
|
6634
|
-
|
|
7891
|
+
function dronePrivatePaths(realHome, swarmHome) {
|
|
7892
|
+
return [
|
|
6635
7893
|
// НЕ `${swarmHome}/**`. Worktree дрона живёт ВНУТРИ swarmHome
|
|
6636
7894
|
// (`<swarmHome>/worktrees.noindex/<repo>/<slug>`), и правило на весь каталог закрывало
|
|
6637
7895
|
// дрону его собственный рабочий каталог: Read/Write/Edit отвечали «File is in a directory
|
|
@@ -6652,11 +7910,6 @@ function droneDenyRules(realHome, swarmHome) {
|
|
|
6652
7910
|
`${realHome}/.claude/.credentials.json`,
|
|
6653
7911
|
`${realHome}/.npmrc`
|
|
6654
7912
|
];
|
|
6655
|
-
const abs = (p) => `//${p.replace(/^\/+/, "")}`;
|
|
6656
|
-
return paths.flatMap((p) => [`Read(${abs(p)})`, `Edit(${abs(p)})`]);
|
|
6657
|
-
}
|
|
6658
|
-
function droneSettingsJson(realHome, swarmHome) {
|
|
6659
|
-
return JSON.stringify({ permissions: { deny: droneDenyRules(realHome, swarmHome) } });
|
|
6660
7913
|
}
|
|
6661
7914
|
|
|
6662
7915
|
// src/testForks.ts
|
|
@@ -6688,6 +7941,7 @@ function droneTestEnv(droneCap, base3 = process.env, host = hostForkSpec()) {
|
|
|
6688
7941
|
}
|
|
6689
7942
|
|
|
6690
7943
|
// src/launcher.ts
|
|
7944
|
+
init_executors();
|
|
6691
7945
|
var requireFromHere = createRequire(import.meta.url);
|
|
6692
7946
|
function resolveTsxCli() {
|
|
6693
7947
|
try {
|
|
@@ -6698,29 +7952,12 @@ function resolveTsxCli() {
|
|
|
6698
7952
|
);
|
|
6699
7953
|
}
|
|
6700
7954
|
}
|
|
6701
|
-
var
|
|
6702
|
-
function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = [], settings) {
|
|
6703
|
-
const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
|
|
6704
|
-
const base3 = [
|
|
6705
|
-
"--dangerously-skip-permissions",
|
|
6706
|
-
...ISOLATION_FLAGS,
|
|
6707
|
-
"--mcp-config",
|
|
6708
|
-
cfgPath,
|
|
6709
|
-
...settings ? ["--settings", settings] : [],
|
|
6710
|
-
...pluginFlags,
|
|
6711
|
-
"--output-format",
|
|
6712
|
-
"json",
|
|
6713
|
-
"-p",
|
|
6714
|
-
prompt
|
|
6715
|
-
];
|
|
6716
|
-
const withModel = model ? ["--model", model, ...base3] : base3;
|
|
6717
|
-
return continueSession ? ["--continue", ...withModel] : withModel;
|
|
6718
|
-
}
|
|
7955
|
+
var MCP_SERVER_NAME = "overmind";
|
|
6719
7956
|
function droneEnv(worktree, base3 = process.env, isolation, droneCap = null) {
|
|
6720
7957
|
const env = { ...base3 };
|
|
6721
7958
|
const isolate = base3.SWARM_TURBO_ISOLATE !== "0";
|
|
6722
7959
|
if (isolate && !base3.TURBO_CACHE_DIR) {
|
|
6723
|
-
env.TURBO_CACHE_DIR =
|
|
7960
|
+
env.TURBO_CACHE_DIR = join12(worktree, ".turbo", "cache");
|
|
6724
7961
|
}
|
|
6725
7962
|
Object.assign(env, droneTestEnv(droneCap, base3));
|
|
6726
7963
|
for (const key of DAEMON_STATE_ENV) delete env[key];
|
|
@@ -6748,49 +7985,20 @@ var DAEMON_STATE_ENV = [
|
|
|
6748
7985
|
"TASKLESS_TOKEN"
|
|
6749
7986
|
];
|
|
6750
7987
|
function bundledRoleDir(kind) {
|
|
6751
|
-
return
|
|
7988
|
+
return join12(dirname2(fileURLToPath(import.meta.url)), "..", "roles", kind);
|
|
6752
7989
|
}
|
|
6753
7990
|
function roleDir(worktree, kind) {
|
|
6754
|
-
return
|
|
7991
|
+
return join12(worktree, ".claude", "roles", kind);
|
|
6755
7992
|
}
|
|
6756
7993
|
function roleDirs(worktree, kind) {
|
|
6757
|
-
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) =>
|
|
6758
|
-
}
|
|
6759
|
-
function transcriptDir(worktree, projectsDir) {
|
|
6760
|
-
return join8(projectsDir, worktree.replace(/[/.]/g, "-"));
|
|
6761
|
-
}
|
|
6762
|
-
function sessionTranscriptExists(worktree, projectsDir = join8(homedir2(), ".claude", "projects")) {
|
|
6763
|
-
try {
|
|
6764
|
-
return readdirSync5(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
|
|
6765
|
-
} catch {
|
|
6766
|
-
return false;
|
|
6767
|
-
}
|
|
6768
|
-
}
|
|
6769
|
-
function transcriptMtimeMs(worktree, projectsDir = join8(homedir2(), ".claude", "projects")) {
|
|
6770
|
-
const dir = transcriptDir(worktree, projectsDir);
|
|
6771
|
-
let files;
|
|
6772
|
-
try {
|
|
6773
|
-
files = readdirSync5(dir);
|
|
6774
|
-
} catch {
|
|
6775
|
-
return null;
|
|
6776
|
-
}
|
|
6777
|
-
let latest = null;
|
|
6778
|
-
for (const f of files) {
|
|
6779
|
-
if (!f.endsWith(".jsonl")) continue;
|
|
6780
|
-
try {
|
|
6781
|
-
const mtime2 = statSync4(join8(dir, f)).mtimeMs;
|
|
6782
|
-
if (latest === null || mtime2 > latest) latest = mtime2;
|
|
6783
|
-
} catch {
|
|
6784
|
-
}
|
|
6785
|
-
}
|
|
6786
|
-
return latest;
|
|
7994
|
+
return [bundledRoleDir(kind), roleDir(worktree, kind)].filter((dir) => existsSync8(dir));
|
|
6787
7995
|
}
|
|
6788
7996
|
function nextRunLogPath(runLogsDir, runId) {
|
|
6789
|
-
const dir =
|
|
6790
|
-
|
|
7997
|
+
const dir = join12(runLogsDir, String(runId));
|
|
7998
|
+
mkdirSync6(dir, { recursive: true });
|
|
6791
7999
|
let files = [];
|
|
6792
8000
|
try {
|
|
6793
|
-
files =
|
|
8001
|
+
files = readdirSync7(dir);
|
|
6794
8002
|
} catch {
|
|
6795
8003
|
}
|
|
6796
8004
|
let next = 0;
|
|
@@ -6798,7 +8006,7 @@ function nextRunLogPath(runLogsDir, runId) {
|
|
|
6798
8006
|
const m = /^(\d+)\.json/.exec(f);
|
|
6799
8007
|
if (m) next = Math.max(next, Number(m[1]) + 1);
|
|
6800
8008
|
}
|
|
6801
|
-
return
|
|
8009
|
+
return join12(dir, `${next}.json`);
|
|
6802
8010
|
}
|
|
6803
8011
|
function openNextRunLog(runLogsDir, runId) {
|
|
6804
8012
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
@@ -6833,57 +8041,63 @@ function makeLauncher(cfg) {
|
|
|
6833
8041
|
);
|
|
6834
8042
|
}
|
|
6835
8043
|
const channelEnv = cfg.channel && channelToken ? { SWARM_DRONE_CHANNEL_URL: cfg.channel.url, SWARM_DRONE_CHANNEL_TOKEN: channelToken } : { SWARM_DB: cfg.dbPath };
|
|
6836
|
-
const
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
|
|
6871
|
-
}
|
|
6872
|
-
}
|
|
8044
|
+
const mcp = {
|
|
8045
|
+
name: MCP_SERVER_NAME,
|
|
8046
|
+
command: process.execPath,
|
|
8047
|
+
args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["--conditions=development", resolveTsxCli(), cfg.overmindEntry, "--drone", "--run", String(run.id)],
|
|
8048
|
+
// SWARM_DRONE_CHANNEL_URL/TOKEN (TASWM-295) ИЛИ SWARM_DB (откат) — см. channelEnv выше.
|
|
8049
|
+
// Канал: дрон говорит только с демоном, файл базы ему не отдаётся вовсе. Токен подписан
|
|
8050
|
+
// на СТАБИЛЬНЫЙ ключ прогона, поэтому переживает рестарт демона.
|
|
8051
|
+
// SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — доверенный
|
|
8052
|
+
// источник «чья это сессия» (read_task и отчёт о сдаче доверяют ему, а не строке,
|
|
8053
|
+
// найденной по номеру прогона).
|
|
8054
|
+
// SWARM_RUN_KIND/SWARM_REVIEWS_RUN_ID/SWARM_HERO_RUN_ID (TASWM-263) и SWARM_RUN_NAME
|
|
8055
|
+
// (TASWM-265): роль, связи и имя прогона той же спавн-фиксацией. Нужны РЕЖИМУ ОТКАТА —
|
|
8056
|
+
// ensureRunHydrated заводит ими стаб-строку в sqlite, если демон в client-режиме её там
|
|
8057
|
+
// не заводил. На канале прогон отдаёт демон целиком, и стаб не нужен.
|
|
8058
|
+
// SWARM_WORKSPACE_ID (TASWM-288): ВОРКСПЕЙС прогона — той же спавн-фиксацией: задачные
|
|
8059
|
+
// вызовы (read_task, отчёт, тулы Героя) дрон делает НЕ через канал, а прямо в Taskless,
|
|
8060
|
+
// и воркспейс им нужен верно сразу. Без него read_task/отчёт дрона
|
|
8061
|
+
// ушли бы с глобальным пином демона — ровно то, что чинит эта задача. Пусто
|
|
8062
|
+
// (прогон до фичи / sqlite-бэкенд) → ключ не кладём, дрон берёт глобальный пин.
|
|
8063
|
+
// SWARM_HOME (TASWM-289): дрон едет с ТЕНЕВЫМ HOME, в котором `.taskless-swarm`
|
|
8064
|
+
// нет вовсе, — а дрон-MCP из этого же дерева процессов читает oauth.json
|
|
8065
|
+
// (connectTaskless в runDroneServer), пульс овермайнда и workspace.json. Наследовать
|
|
8066
|
+
// ему нечего, поэтому реальный SWARM_HOME кладём явно. Дрон этот конфиг прочитать
|
|
8067
|
+
// может, но нового ему это не даёт: его собственный worktree лежит ВНУТРИ
|
|
8068
|
+
// SWARM_HOME, путь он и так знает (см. «чего этот модуль не делает» в droneHome.ts).
|
|
8069
|
+
env: {
|
|
8070
|
+
...channelEnv,
|
|
8071
|
+
...cfg.droneIsolation ? { SWARM_HOME: cfg.droneIsolation.swarmHome } : {},
|
|
8072
|
+
SWARM_TASK_SLUG: run.task_slug,
|
|
8073
|
+
SWARM_RUN_KIND: run.kind,
|
|
8074
|
+
SWARM_RUN_NAME: run.name,
|
|
8075
|
+
...run.workspace_id ? { SWARM_WORKSPACE_ID: run.workspace_id } : {},
|
|
8076
|
+
...run.reviews_run_id != null ? { SWARM_REVIEWS_RUN_ID: String(run.reviews_run_id) } : {},
|
|
8077
|
+
...run.hero_run_id != null ? { SWARM_HERO_RUN_ID: String(run.hero_run_id) } : {}
|
|
6873
8078
|
}
|
|
6874
8079
|
};
|
|
6875
|
-
const
|
|
6876
|
-
|
|
6877
|
-
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
|
|
6878
|
-
const pluginDirs = roleDirs(worktree, run.kind);
|
|
6879
|
-
const realHome = cfg.droneIsolation?.realHome ?? homedir2();
|
|
8080
|
+
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
|
|
8081
|
+
const realHome = cfg.droneIsolation?.realHome ?? homedir4();
|
|
6880
8082
|
const home = cfg.droneIsolation ? buildDroneHome({ realHome, droneHome: cfg.droneIsolation.home }) : void 0;
|
|
6881
|
-
|
|
6882
|
-
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync6(join8(cfg.droneIsolation.swarmHome, "token"))) {
|
|
8083
|
+
if (cfg.droneIsolation && process.env.TASKLESS_TOKEN && !existsSync8(join12(cfg.droneIsolation.swarmHome, "token"))) {
|
|
6883
8084
|
console.error(
|
|
6884
|
-
`[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${
|
|
8085
|
+
`[launcher] TASKLESS_TOKEN \u0437\u0430\u0434\u0430\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0432 env \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0434\u0440\u043E\u043D\u0443 \u043E\u043D \u043D\u0435 \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F (TASWM-289), \u0430 \u0444\u0430\u0439\u043B\u0430 ${join12(cfg.droneIsolation.swarmHome, "token")} \u043D\u0435\u0442. read_task \u0434\u0440\u043E\u043D\u0430 \u0431\u0443\u0434\u0435\u0442 slug-only. \u041F\u043E\u0447\u0438\u043D\u0438: \`swarm login\` (oauth.json) \u0438\u043B\u0438 \u043F\u043E\u043B\u043E\u0436\u0438 \u0442\u043E\u043A\u0435\u043D \u0432 \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B.`
|
|
6885
8086
|
);
|
|
6886
8087
|
}
|
|
8088
|
+
const executor = cfg.executor ?? resolveExecutor();
|
|
8089
|
+
const plan = executor.plan({
|
|
8090
|
+
worktree,
|
|
8091
|
+
prompt,
|
|
8092
|
+
continueSession: !!continueSession,
|
|
8093
|
+
model,
|
|
8094
|
+
mcp,
|
|
8095
|
+
skillDirs: roleDirs(worktree, run.kind),
|
|
8096
|
+
// Закрытые дрону пути имеют смысл только при теневом HOME: без него дрон и так
|
|
8097
|
+
// сидит в HOME демона, и запирать половину дверей — самообман.
|
|
8098
|
+
denyPaths: cfg.droneIsolation ? dronePrivatePaths(realHome, cfg.droneIsolation.swarmHome) : [],
|
|
8099
|
+
realHome
|
|
8100
|
+
});
|
|
6887
8101
|
let stdio = "ignore";
|
|
6888
8102
|
let outFd;
|
|
6889
8103
|
let errFd;
|
|
@@ -6893,15 +8107,16 @@ function makeLauncher(cfg) {
|
|
|
6893
8107
|
errFd = fds.err ?? void 0;
|
|
6894
8108
|
stdio = ["ignore", outFd, errFd ?? "ignore"];
|
|
6895
8109
|
}
|
|
6896
|
-
const child = spawn(cfg.
|
|
8110
|
+
const child = spawn(cfg.bin ?? plan.bin, plan.args, {
|
|
6897
8111
|
cwd: worktree,
|
|
6898
8112
|
detached: true,
|
|
6899
8113
|
stdio,
|
|
6900
8114
|
// Пер-worktree TURBO_CACHE_DIR (TASWM-232): дрон не отыгрывает кеш соседнего
|
|
6901
8115
|
// worktree, проверки исполняются реально. Теневой HOME + чистка env от состояния
|
|
6902
8116
|
// демона (TASWM-289). Доля тестовых форков от числа дронов на хосте (TASWM-246).
|
|
6903
|
-
// Остальной env демона наследуется, как раньше.
|
|
6904
|
-
|
|
8117
|
+
// Остальной env демона наследуется, как раньше. Добавки адаптера — последними:
|
|
8118
|
+
// они про его собственное состояние, и перебивать их ройным нечем.
|
|
8119
|
+
env: { ...droneEnv(worktree, process.env, home ? { droneHome: home, realHome } : void 0, cfg.droneCap?.() ?? null), ...plan.env }
|
|
6905
8120
|
});
|
|
6906
8121
|
if (outFd !== void 0) closeSync2(outFd);
|
|
6907
8122
|
if (errFd !== void 0) closeSync2(errFd);
|
|
@@ -6915,92 +8130,59 @@ function makeLauncher(cfg) {
|
|
|
6915
8130
|
init_procCleanup();
|
|
6916
8131
|
|
|
6917
8132
|
// src/cost.ts
|
|
6918
|
-
|
|
6919
|
-
import {
|
|
6920
|
-
|
|
6921
|
-
// src/pricing.ts
|
|
6922
|
-
var MODEL_PRICING = {
|
|
6923
|
-
// Вводная цена Sonnet 5 действует до 31.08.2026, после чего вернётся к 3/15.
|
|
6924
|
-
// Дата не автоматизируется намеренно: одна строка раз в жизни дешевле, чем
|
|
6925
|
-
// ветвление по времени внутри расчёта стоимости.
|
|
6926
|
-
"claude-sonnet-5": { inputPerMTok: 2, outputPerMTok: 10 },
|
|
6927
|
-
"claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
6928
|
-
// Прогоны на 4.8 остаются в истории — строку держим ради их стоимости.
|
|
6929
|
-
"claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
6930
|
-
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
|
|
6931
|
-
};
|
|
6932
|
-
var CACHE_READ_FACTOR = 0.1;
|
|
6933
|
-
var CACHE_WRITE_5M_FACTOR = 1.25;
|
|
6934
|
-
function costFromUsage(usage, model) {
|
|
6935
|
-
const pricing = model ? MODEL_PRICING[model] : void 0;
|
|
6936
|
-
if (!pricing) {
|
|
6937
|
-
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` };
|
|
6938
|
-
}
|
|
6939
|
-
const input = usage.input_tokens ?? 0;
|
|
6940
|
-
const output = usage.output_tokens ?? 0;
|
|
6941
|
-
const cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
6942
|
-
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
6943
|
-
const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
|
|
6944
|
-
return { costUsd };
|
|
6945
|
-
}
|
|
6946
|
-
|
|
6947
|
-
// src/claudeResult.ts
|
|
6948
|
-
function parseClaudeResultJson(raw) {
|
|
6949
|
-
const trimmed = raw.trim();
|
|
6950
|
-
if (!trimmed) return null;
|
|
6951
|
-
try {
|
|
6952
|
-
const parsed = JSON.parse(trimmed);
|
|
6953
|
-
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
6954
|
-
} catch {
|
|
6955
|
-
return null;
|
|
6956
|
-
}
|
|
6957
|
-
}
|
|
6958
|
-
function launchCostUsd(result, model) {
|
|
6959
|
-
if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
|
|
6960
|
-
return { costUsd: result.total_cost_usd };
|
|
6961
|
-
}
|
|
6962
|
-
if (!result.usage) return { costUsd: 0 };
|
|
6963
|
-
return costFromUsage(result.usage, model);
|
|
6964
|
-
}
|
|
6965
|
-
|
|
6966
|
-
// src/cost.ts
|
|
8133
|
+
init_executors();
|
|
8134
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync8, renameSync as renameSync7 } from "fs";
|
|
8135
|
+
import { join as join13 } from "path";
|
|
6967
8136
|
var UNPROCESSED_LOG_RE = /^\d+\.json$/;
|
|
6968
8137
|
function unprocessedLogFiles(dir) {
|
|
6969
8138
|
try {
|
|
6970
|
-
return
|
|
8139
|
+
return readdirSync8(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
|
|
6971
8140
|
} catch {
|
|
6972
8141
|
return [];
|
|
6973
8142
|
}
|
|
6974
8143
|
}
|
|
6975
8144
|
function accountRunCost(deps, run) {
|
|
6976
|
-
const dir =
|
|
8145
|
+
const dir = join13(deps.runLogsDir, String(run.id));
|
|
6977
8146
|
const files = unprocessedLogFiles(dir);
|
|
6978
8147
|
if (files.length === 0) return;
|
|
6979
8148
|
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
|
|
8149
|
+
const executor = deps.executor ?? resolveExecutor();
|
|
6980
8150
|
let added = 0;
|
|
8151
|
+
let tokensIn = 0;
|
|
8152
|
+
let tokensOut = 0;
|
|
8153
|
+
let durationMs = 0;
|
|
6981
8154
|
const processed = [];
|
|
6982
8155
|
for (const f of files) {
|
|
6983
|
-
const full =
|
|
8156
|
+
const full = join13(dir, f);
|
|
6984
8157
|
let raw = null;
|
|
6985
8158
|
try {
|
|
6986
|
-
raw =
|
|
8159
|
+
raw = readFileSync10(full, "utf8");
|
|
6987
8160
|
} catch {
|
|
6988
8161
|
continue;
|
|
6989
8162
|
}
|
|
6990
|
-
const result =
|
|
8163
|
+
const result = executor.parseResult(raw, model);
|
|
6991
8164
|
if (!result) {
|
|
6992
8165
|
console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: \u043D\u0435 \u0440\u0430\u0441\u043F\u0430\u0440\u0441\u0438\u043B\u0441\u044F ${full} (\u043F\u0440\u043E\u0433\u043E\u043D \u0443\u043C\u0435\u0440 \u0434\u043E \u0437\u0430\u043F\u0438\u0441\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430?)`);
|
|
6993
8166
|
} else {
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
8167
|
+
if (result.warning) console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${result.warning}`);
|
|
8168
|
+
added += result.costUsd;
|
|
8169
|
+
tokensIn += result.tokensIn ?? 0;
|
|
8170
|
+
tokensOut += result.tokensOut ?? 0;
|
|
8171
|
+
durationMs += result.durationMs ?? 0;
|
|
6997
8172
|
}
|
|
6998
8173
|
processed.push(full);
|
|
6999
8174
|
}
|
|
7000
|
-
if (added > 0
|
|
8175
|
+
if (added > 0 || tokensIn > 0 || tokensOut > 0 || durationMs > 0) {
|
|
8176
|
+
deps.db.updateRun(run.id, {
|
|
8177
|
+
cost_usd: run.cost_usd + added,
|
|
8178
|
+
tokens_in: run.tokens_in + tokensIn,
|
|
8179
|
+
tokens_out: run.tokens_out + tokensOut,
|
|
8180
|
+
duration_ms: run.duration_ms + durationMs
|
|
8181
|
+
});
|
|
8182
|
+
}
|
|
7001
8183
|
for (const full of processed) {
|
|
7002
8184
|
try {
|
|
7003
|
-
|
|
8185
|
+
renameSync7(full, `${full}.done`);
|
|
7004
8186
|
} catch {
|
|
7005
8187
|
}
|
|
7006
8188
|
}
|
|
@@ -7010,14 +8192,15 @@ function makeCostAccountant(deps) {
|
|
|
7010
8192
|
}
|
|
7011
8193
|
|
|
7012
8194
|
// src/runOutcome.ts
|
|
7013
|
-
|
|
7014
|
-
import {
|
|
8195
|
+
init_executors();
|
|
8196
|
+
import { readdirSync as readdirSync9, readFileSync as readFileSync11 } from "fs";
|
|
8197
|
+
import { join as join14 } from "path";
|
|
7015
8198
|
var RESULT_FILE_RE = /^(\d+)\.json(\.done)?$/;
|
|
7016
|
-
function classifyExit(runLogsDir, runId) {
|
|
7017
|
-
const dir =
|
|
8199
|
+
function classifyExit(runLogsDir, runId, executor = resolveExecutor()) {
|
|
8200
|
+
const dir = join14(runLogsDir, String(runId));
|
|
7018
8201
|
let files;
|
|
7019
8202
|
try {
|
|
7020
|
-
files =
|
|
8203
|
+
files = readdirSync9(dir);
|
|
7021
8204
|
} catch {
|
|
7022
8205
|
return "unknown";
|
|
7023
8206
|
}
|
|
@@ -7031,22 +8214,24 @@ function classifyExit(runLogsDir, runId) {
|
|
|
7031
8214
|
if (!latest) return "unknown";
|
|
7032
8215
|
let raw;
|
|
7033
8216
|
try {
|
|
7034
|
-
raw =
|
|
8217
|
+
raw = readFileSync11(join14(dir, latest.name), "utf8");
|
|
7035
8218
|
} catch {
|
|
7036
8219
|
return "unknown";
|
|
7037
8220
|
}
|
|
7038
|
-
return
|
|
8221
|
+
return executor.parseResult(raw, null) ? "graceful" : "crash";
|
|
7039
8222
|
}
|
|
7040
|
-
function makeExitClassifier(runLogsDir) {
|
|
7041
|
-
|
|
8223
|
+
function makeExitClassifier(runLogsDir, executor) {
|
|
8224
|
+
const exec = executor ?? resolveExecutor();
|
|
8225
|
+
return (run) => classifyExit(runLogsDir, run.id, exec);
|
|
7042
8226
|
}
|
|
7043
8227
|
|
|
7044
8228
|
// src/orchestrator.ts
|
|
8229
|
+
init_src();
|
|
7045
8230
|
init_git();
|
|
7046
8231
|
|
|
7047
8232
|
// src/deps.ts
|
|
7048
|
-
import { existsSync as
|
|
7049
|
-
import { join as
|
|
8233
|
+
import { existsSync as existsSync10, readdirSync as readdirSync11, readFileSync as readFileSync12 } from "fs";
|
|
8234
|
+
import { join as join16 } from "path";
|
|
7050
8235
|
import { execa } from "execa";
|
|
7051
8236
|
var PREPARE_TIMEOUT_MS = 15 * 6e4;
|
|
7052
8237
|
var MAX_PLAN_COMMANDS = 12;
|
|
@@ -7062,21 +8247,21 @@ async function runCommand(cmd, worktree) {
|
|
|
7062
8247
|
}
|
|
7063
8248
|
function safeReadText(p) {
|
|
7064
8249
|
try {
|
|
7065
|
-
return
|
|
8250
|
+
return readFileSync12(p, "utf8");
|
|
7066
8251
|
} catch {
|
|
7067
8252
|
return null;
|
|
7068
8253
|
}
|
|
7069
8254
|
}
|
|
7070
8255
|
function safeListDirs(p) {
|
|
7071
8256
|
try {
|
|
7072
|
-
return
|
|
8257
|
+
return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
7073
8258
|
} catch {
|
|
7074
8259
|
return [];
|
|
7075
8260
|
}
|
|
7076
8261
|
}
|
|
7077
8262
|
function safeListFiles(p) {
|
|
7078
8263
|
try {
|
|
7079
|
-
return
|
|
8264
|
+
return readdirSync11(p, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
|
|
7080
8265
|
} catch {
|
|
7081
8266
|
return [];
|
|
7082
8267
|
}
|
|
@@ -7102,21 +8287,21 @@ function overridePlan(override) {
|
|
|
7102
8287
|
};
|
|
7103
8288
|
}
|
|
7104
8289
|
function detectPackageManager(worktree, io = {}) {
|
|
7105
|
-
const exists = io.exists ??
|
|
8290
|
+
const exists = io.exists ?? existsSync10;
|
|
7106
8291
|
const readText = io.readText ?? safeReadText;
|
|
7107
|
-
const hasPkgJson = exists(
|
|
8292
|
+
const hasPkgJson = exists(join16(worktree, "package.json"));
|
|
7108
8293
|
if (!hasPkgJson) {
|
|
7109
|
-
if (exists(
|
|
8294
|
+
if (exists(join16(worktree, "deno.json")) || exists(join16(worktree, "deno.jsonc")) || exists(join16(worktree, "deno.lock"))) return "deno";
|
|
7110
8295
|
return null;
|
|
7111
8296
|
}
|
|
7112
|
-
const pkg = readJson(readText,
|
|
8297
|
+
const pkg = readJson(readText, join16(worktree, "package.json"));
|
|
7113
8298
|
const declared = typeof pkg?.packageManager === "string" ? pkg.packageManager.split("@")[0] : void 0;
|
|
7114
8299
|
if (declared === "pnpm" || declared === "bun" || declared === "npm" || declared === "yarn") return declared;
|
|
7115
|
-
if (exists(
|
|
7116
|
-
if (exists(
|
|
7117
|
-
if (exists(
|
|
7118
|
-
if (exists(
|
|
7119
|
-
if (exists(
|
|
8300
|
+
if (exists(join16(worktree, "bun.lock")) || exists(join16(worktree, "bun.lockb"))) return "bun";
|
|
8301
|
+
if (exists(join16(worktree, "pnpm-lock.yaml"))) return "pnpm";
|
|
8302
|
+
if (exists(join16(worktree, "yarn.lock"))) return "yarn";
|
|
8303
|
+
if (exists(join16(worktree, "package-lock.json"))) return "npm";
|
|
8304
|
+
if (exists(join16(worktree, "deno.lock"))) return "deno";
|
|
7120
8305
|
return "npm";
|
|
7121
8306
|
}
|
|
7122
8307
|
function jsInstallCommand(pm) {
|
|
@@ -7169,7 +8354,7 @@ var ECOSYSTEM_MARKERS = [
|
|
|
7169
8354
|
// конфигурация — дело сборки, не подготовки
|
|
7170
8355
|
];
|
|
7171
8356
|
function detectPlan(worktree, io) {
|
|
7172
|
-
const exists = io.exists ??
|
|
8357
|
+
const exists = io.exists ?? existsSync10;
|
|
7173
8358
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
7174
8359
|
const pm = detectPackageManager(worktree, io);
|
|
7175
8360
|
if (pm) {
|
|
@@ -7177,7 +8362,7 @@ function detectPlan(worktree, io) {
|
|
|
7177
8362
|
// node_modules на месте — установку пропускаем (быстрый путь переиспользованного worktree).
|
|
7178
8363
|
// У deno каталога может не быть вовсе (кэш в DENO_DIR) — тогда `deno install` отработает
|
|
7179
8364
|
// заново, но он дешёвый: тянет из того же общего кэша.
|
|
7180
|
-
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists:
|
|
8365
|
+
{ label: `${pm} install`, run: jsInstallCommand(pm), skipIfExists: join16(worktree, "node_modules"), source: "detect" }
|
|
7181
8366
|
];
|
|
7182
8367
|
for (const target of workspaceBuildTargets(worktree, io)) {
|
|
7183
8368
|
commands.push({ label: `\u0441\u0431\u043E\u0440\u043A\u0430 ${target.name}`, run: jsRunScript(pm, "build"), cwd: target.dir, source: "detect" });
|
|
@@ -7185,12 +8370,12 @@ function detectPlan(worktree, io) {
|
|
|
7185
8370
|
return { source: "detect", ecosystem: `node/${pm}`, commands };
|
|
7186
8371
|
}
|
|
7187
8372
|
for (const marker of ECOSYSTEM_MARKERS) {
|
|
7188
|
-
const hit = marker.files.some((f) => exists(
|
|
8373
|
+
const hit = marker.files.some((f) => exists(join16(worktree, f))) || marker.ext !== void 0 && listFiles(worktree).some((n) => n.endsWith(marker.ext));
|
|
7189
8374
|
if (!hit) continue;
|
|
7190
8375
|
let ecosystem = marker.ecosystem;
|
|
7191
8376
|
let command = marker.command;
|
|
7192
8377
|
for (const refine of marker.refine ?? []) {
|
|
7193
|
-
if (!exists(
|
|
8378
|
+
if (!exists(join16(worktree, refine.file))) continue;
|
|
7194
8379
|
ecosystem = refine.ecosystem;
|
|
7195
8380
|
command = refine.command;
|
|
7196
8381
|
break;
|
|
@@ -7247,12 +8432,12 @@ var CI_INSTALL_PATTERNS = [
|
|
|
7247
8432
|
function ciWorkflowPlan(worktree, io) {
|
|
7248
8433
|
const readText = io.readText ?? safeReadText;
|
|
7249
8434
|
const listFiles = io.listFiles ?? safeListFiles;
|
|
7250
|
-
const dir =
|
|
8435
|
+
const dir = join16(worktree, ".github", "workflows");
|
|
7251
8436
|
const files = listFiles(dir).filter((n) => n.endsWith(".yml") || n.endsWith(".yaml"));
|
|
7252
8437
|
const seen = /* @__PURE__ */ new Set();
|
|
7253
8438
|
const commands = [];
|
|
7254
8439
|
for (const name of files.sort()) {
|
|
7255
|
-
const yaml = readText(
|
|
8440
|
+
const yaml = readText(join16(dir, name));
|
|
7256
8441
|
if (yaml === null) continue;
|
|
7257
8442
|
for (const raw of workflowRunLines(yaml)) {
|
|
7258
8443
|
const line = raw.replace(/\s+/g, " ").trim();
|
|
@@ -7267,7 +8452,7 @@ function ciWorkflowPlan(worktree, io) {
|
|
|
7267
8452
|
return commands.length ? { source: "ci-workflow", ecosystem: "ci-\u043A\u043E\u043D\u0444\u0438\u0433", commands } : null;
|
|
7268
8453
|
}
|
|
7269
8454
|
function workspaceGlobs(worktree, readText) {
|
|
7270
|
-
const yaml = readText(
|
|
8455
|
+
const yaml = readText(join16(worktree, "pnpm-workspace.yaml"));
|
|
7271
8456
|
if (yaml !== null) {
|
|
7272
8457
|
const globs = [];
|
|
7273
8458
|
let inPackages = false;
|
|
@@ -7287,7 +8472,7 @@ function workspaceGlobs(worktree, readText) {
|
|
|
7287
8472
|
}
|
|
7288
8473
|
if (globs.length) return globs;
|
|
7289
8474
|
}
|
|
7290
|
-
const pkg = readJson(readText,
|
|
8475
|
+
const pkg = readJson(readText, join16(worktree, "package.json"));
|
|
7291
8476
|
const ws = pkg?.workspaces;
|
|
7292
8477
|
const list = Array.isArray(ws) ? ws : Array.isArray(ws?.packages) ? ws.packages : [];
|
|
7293
8478
|
return list.filter((g) => typeof g === "string");
|
|
@@ -7298,14 +8483,14 @@ function workspaceDirs(worktree, globs, io) {
|
|
|
7298
8483
|
if (glob.startsWith("!")) continue;
|
|
7299
8484
|
const star = glob.indexOf("*");
|
|
7300
8485
|
if (star === -1) {
|
|
7301
|
-
const dir =
|
|
7302
|
-
if (io.exists(
|
|
8486
|
+
const dir = join16(worktree, glob);
|
|
8487
|
+
if (io.exists(join16(dir, "package.json"))) dirs.add(dir);
|
|
7303
8488
|
continue;
|
|
7304
8489
|
}
|
|
7305
|
-
const base3 =
|
|
8490
|
+
const base3 = join16(worktree, glob.slice(0, star).replace(/\/$/, ""));
|
|
7306
8491
|
for (const name of io.listDirs(base3)) {
|
|
7307
|
-
const dir =
|
|
7308
|
-
if (io.exists(
|
|
8492
|
+
const dir = join16(base3, name);
|
|
8493
|
+
if (io.exists(join16(dir, "package.json"))) dirs.add(dir);
|
|
7309
8494
|
}
|
|
7310
8495
|
}
|
|
7311
8496
|
return [...dirs];
|
|
@@ -7326,18 +8511,18 @@ function entryPaths(pkg) {
|
|
|
7326
8511
|
return out;
|
|
7327
8512
|
}
|
|
7328
8513
|
function workspaceBuildTargets(worktree, io = {}) {
|
|
7329
|
-
const exists = io.exists ??
|
|
8514
|
+
const exists = io.exists ?? existsSync10;
|
|
7330
8515
|
const readText = io.readText ?? safeReadText;
|
|
7331
8516
|
const listDirs = io.listDirs ?? safeListDirs;
|
|
7332
8517
|
const globs = workspaceGlobs(worktree, readText);
|
|
7333
8518
|
if (!globs.length) return [];
|
|
7334
8519
|
const pkgs = [];
|
|
7335
8520
|
for (const dir of workspaceDirs(worktree, globs, { exists, listDirs })) {
|
|
7336
|
-
const pkg = readJson(readText,
|
|
8521
|
+
const pkg = readJson(readText, join16(dir, "package.json"));
|
|
7337
8522
|
if (!pkg || typeof pkg.name !== "string") continue;
|
|
7338
8523
|
const scripts = pkg.scripts ?? {};
|
|
7339
8524
|
const entries = entryPaths(pkg);
|
|
7340
|
-
const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(
|
|
8525
|
+
const needsBuild = typeof scripts.build === "string" && entries.length > 0 && entries.some((e) => !exists(join16(dir, e)));
|
|
7341
8526
|
const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
|
|
7342
8527
|
pkgs.push({ name: pkg.name, dir, deps, needsBuild });
|
|
7343
8528
|
}
|
|
@@ -7365,7 +8550,7 @@ function planPrepare(worktree, io = {}) {
|
|
|
7365
8550
|
return { ...plan, commands: plan.commands.slice(0, MAX_PLAN_COMMANDS) };
|
|
7366
8551
|
}
|
|
7367
8552
|
async function ensureWorktreeDeps(worktree, io = {}) {
|
|
7368
|
-
const exists = io.exists ??
|
|
8553
|
+
const exists = io.exists ?? existsSync10;
|
|
7369
8554
|
const exec = io.exec ?? ((cmd) => runCommand(cmd, worktree));
|
|
7370
8555
|
const approve = io.approve ?? (() => true);
|
|
7371
8556
|
const plan = planPrepare(worktree, io);
|
|
@@ -7456,7 +8641,9 @@ var BaseBranchMissingError = class extends Error {
|
|
|
7456
8641
|
branch;
|
|
7457
8642
|
repo;
|
|
7458
8643
|
};
|
|
7459
|
-
var
|
|
8644
|
+
var SpawnDeferredError = class extends Error {
|
|
8645
|
+
};
|
|
8646
|
+
var AreaBusyError = class extends SpawnDeferredError {
|
|
7460
8647
|
constructor(area) {
|
|
7461
8648
|
super(`\u0437\u043E\u043D\u0430 ${area} \u0437\u0430\u043D\u044F\u0442\u0430 \u0434\u0440\u0443\u0433\u0438\u043C \u0434\u0440\u043E\u043D\u043E\u043C \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0441\u0442\u0430\u0451\u0442\u0441\u044F \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438`);
|
|
7462
8649
|
this.area = area;
|
|
@@ -7464,10 +8651,26 @@ var AreaBusyError = class extends Error {
|
|
|
7464
8651
|
}
|
|
7465
8652
|
area;
|
|
7466
8653
|
};
|
|
8654
|
+
var FileOverlapError = class extends SpawnDeferredError {
|
|
8655
|
+
constructor(message, blockedByRunId) {
|
|
8656
|
+
super(message);
|
|
8657
|
+
this.blockedByRunId = blockedByRunId;
|
|
8658
|
+
this.name = "FileOverlapError";
|
|
8659
|
+
}
|
|
8660
|
+
blockedByRunId;
|
|
8661
|
+
};
|
|
8662
|
+
function healTaskSlug(db, run, task) {
|
|
8663
|
+
const slug = task?.slug;
|
|
8664
|
+
if (!slug || slug === run.task_slug || !isTaskSlug(slug) || isTaskSlug(run.task_slug)) return run;
|
|
8665
|
+
db.addEvent(run.id, "step", `\u0441\u043B\u0430\u0433 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D: ${run.task_slug} \u2192 ${slug} (\u043F\u0440\u043E\u0434 \u043E\u0442\u0434\u0430\u043B \u043F\u0440\u043E\u0433\u043E\u043D \u0431\u0435\u0437 \u0441\u043B\u0430\u0433\u0430 \u2014 TASWM-329)`);
|
|
8666
|
+
return db.updateRun(run.id, { task_slug: slug }) ?? run;
|
|
8667
|
+
}
|
|
7467
8668
|
async function spawnDrone(deps, run) {
|
|
7468
8669
|
const repo = normalizeRepo(run.repo || deps.repo || "");
|
|
7469
8670
|
if (!repo) throw new Error(`\u0443 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} (${run.task_slug}) \u043D\u0435\u0442 repo \u0438 \u043D\u0435\u0442 \u0434\u0435\u0444\u043E\u043B\u0442\u0430 SWARM_REPO`);
|
|
7470
|
-
|
|
8671
|
+
const conflict = deps.conflicts ? await deps.conflicts(run, repo) : { kind: "unknown" };
|
|
8672
|
+
if (conflict.kind === "wait") throw new FileOverlapError(conflict.message, conflict.blockedBy.id);
|
|
8673
|
+
if (conflict.kind === "unknown" && run.area && deps.db.runningAreas(repo).includes(run.area)) {
|
|
7471
8674
|
throw new AreaBusyError(run.area);
|
|
7472
8675
|
}
|
|
7473
8676
|
const branch = run.task_slug;
|
|
@@ -7518,13 +8721,13 @@ function isGithubPrUrl(url) {
|
|
|
7518
8721
|
return GH_PR_RE.test(url.trim());
|
|
7519
8722
|
}
|
|
7520
8723
|
function softStatus() {
|
|
7521
|
-
return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [] };
|
|
8724
|
+
return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [], headBranch: null };
|
|
7522
8725
|
}
|
|
7523
8726
|
async function prStatus(prUrl, run = realRunner) {
|
|
7524
8727
|
if (!isGithubPrUrl(prUrl)) return softStatus();
|
|
7525
8728
|
let out;
|
|
7526
8729
|
try {
|
|
7527
|
-
out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files"]);
|
|
8730
|
+
out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files,headRefName"]);
|
|
7528
8731
|
} catch {
|
|
7529
8732
|
return softStatus();
|
|
7530
8733
|
}
|
|
@@ -7545,9 +8748,20 @@ async function prStatus(prUrl, run = realRunner) {
|
|
|
7545
8748
|
// не смержен, открыт и GitHub уверенно говорит CONFLICTING
|
|
7546
8749
|
conflicting: !merged && state === "OPEN" && mergeable === "CONFLICTING",
|
|
7547
8750
|
isDraft: j.isDraft === true,
|
|
7548
|
-
files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : []
|
|
8751
|
+
files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : [],
|
|
8752
|
+
headBranch: typeof j.headRefName === "string" && j.headRefName ? j.headRefName : null
|
|
7549
8753
|
};
|
|
7550
8754
|
}
|
|
8755
|
+
async function prHeadBranch(prUrl, run = realRunner) {
|
|
8756
|
+
if (!isGithubPrUrl(prUrl)) return null;
|
|
8757
|
+
try {
|
|
8758
|
+
const out = await run(["pr", "view", prUrl, "--json", "headRefName"]);
|
|
8759
|
+
const j = JSON.parse(out);
|
|
8760
|
+
return typeof j.headRefName === "string" && j.headRefName ? j.headRefName : null;
|
|
8761
|
+
} catch {
|
|
8762
|
+
return null;
|
|
8763
|
+
}
|
|
8764
|
+
}
|
|
7551
8765
|
async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
|
|
7552
8766
|
if (!branch || !repoDir) return [];
|
|
7553
8767
|
try {
|
|
@@ -7755,6 +8969,13 @@ function reviewTransitionPatch(run, prUrl) {
|
|
|
7755
8969
|
}
|
|
7756
8970
|
return { isFixIteration: false, cycle: null, patch: { status: "review", pr_url: prUrl } };
|
|
7757
8971
|
}
|
|
8972
|
+
function branchFromPrPatch(currentBranch, headBranch) {
|
|
8973
|
+
if (!headBranch || headBranch === currentBranch) return null;
|
|
8974
|
+
return {
|
|
8975
|
+
patch: { branch: headBranch },
|
|
8976
|
+
event: currentBranch ? `\u0432\u0435\u0442\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0432\u0437\u044F\u0442\u0430 \u0443 PR: ${currentBranch} \u2192 ${headBranch} (\u043F\u043E\u043B\u0435 \u0431\u044B\u043B\u043E \u0441\u043E\u0431\u0440\u0430\u043D\u043E \u0438\u0437 \u0441\u043B\u0430\u0433\u0430 \u2014 TASWM-356)` : `\u0432\u0435\u0442\u043A\u0430 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0432\u0437\u044F\u0442\u0430 \u0443 PR: ${headBranch} (TASWM-356)`
|
|
8977
|
+
};
|
|
8978
|
+
}
|
|
7758
8979
|
function applyReviewTransition(db, run, prUrl) {
|
|
7759
8980
|
const { patch, ...t } = reviewTransitionPatch(run, prUrl);
|
|
7760
8981
|
db.updateRun(run.id, patch);
|
|
@@ -7763,14 +8984,14 @@ function applyReviewTransition(db, run, prUrl) {
|
|
|
7763
8984
|
|
|
7764
8985
|
// src/droneServer.ts
|
|
7765
8986
|
init_src();
|
|
7766
|
-
function freezeInstruction(
|
|
8987
|
+
function freezeInstruction(branch, reason = "\u0435\u0433\u043E \u043F\u0443\u043B\u044C\u0441 (heartbeat) \u0443\u0441\u0442\u0430\u0440\u0435\u043B") {
|
|
7767
8988
|
return [
|
|
7768
8989
|
`\u26D4 OVERMIND \u041D\u0415\u0414\u041E\u0421\u0422\u0423\u041F\u0415\u041D \u2014 ${reason}. \u041D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0430\u0434 \u0442\u043E\u0431\u043E\u0439 \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0442: \u043D\u0435\u043A\u043E\u043C\u0443`,
|
|
7769
8990
|
"\u043F\u0440\u0438\u043D\u044F\u0442\u044C \u0442\u0432\u043E\u0439 \u0432\u043E\u043F\u0440\u043E\u0441, \u0437\u0430\u043C\u0435\u0442\u0438\u0442\u044C \u0441\u043C\u0435\u0440\u0442\u044C, \u0441\u043C\u0435\u0440\u0436\u0438\u0442\u044C PR \u0438\u043B\u0438 \u0440\u0430\u0437\u0440\u0443\u043B\u0438\u0442\u044C \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442. \u0420\u0430\u0431\u043E\u0442\u0430\u0442\u044C \u0432\u0441\u043B\u0435\u043F\u0443\u044E \u043D\u0435\u043B\u044C\u0437\u044F.",
|
|
7770
8991
|
"\u0417\u0410\u041C\u0420\u0418 \u0438 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0441\u044C, \u0447\u0442\u043E\u0431\u044B \u0440\u0430\u0431\u043E\u0442\u0430 \u043D\u0435 \u043F\u0440\u043E\u043F\u0430\u043B\u0430:",
|
|
7771
8992
|
"1. \u0417\u0430\u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0412\u0421\u042E \u0442\u0435\u043A\u0443\u0449\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443 \u043A\u0430\u043A \u0435\u0441\u0442\u044C: `git add -A && git commit` (\u0442\u0440\u0435\u0439\u043B\u0435\u0440 \u043F\u0440\u043E \u0434\u0440\u043E\u043D\u0430 \u2014 \u043A\u0430\u043A \u043E\u0431\u044B\u0447\u043D\u043E).",
|
|
7772
8993
|
" \u041D\u043E\u0432\u044B\u0439 \u043A\u043E\u0434 \u041D\u0415 \u043F\u0438\u0448\u0438, \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u0430\u043B\u044C\u0448\u0435 \u041D\u0415 \u0434\u0435\u043B\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u043E\u0445\u0440\u0430\u043D\u0438 \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043D\u043E\u0435, \u0431\u0435\u0437 \u043C\u0443\u0441\u043E\u0440\u043D\u044B\u0445 \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432.",
|
|
7773
|
-
`2. \u0417\u0430\u043F\u0443\u0448\u044C \u0432\u0435\u0442\u043A\u0443: \`git push -u origin ${
|
|
8994
|
+
`2. \u0417\u0430\u043F\u0443\u0448\u044C \u0432\u0435\u0442\u043A\u0443: \`git push -u origin ${branch}\` \u2014 \u043D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0440\u0451\u0442 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u043C.`,
|
|
7774
8995
|
"3. \u0417\u0430\u0432\u0435\u0440\u0448\u0438 \u0440\u0430\u0431\u043E\u0442\u0443 \u2014 \u0431\u043E\u043B\u044C\u0448\u0435 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u043E\u0432 \u041D\u0415 \u0437\u043E\u0432\u0438 (\u043D\u0438 report_step, \u043D\u0438 ask, \u043D\u0438 done). \u041F\u0440\u043E\u0441\u0442\u043E \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438 \u0441\u0435\u0441\u0441\u0438\u044E.",
|
|
7775
8996
|
"\u041F\u0440\u043E\u0433\u043E\u043D \u043F\u043E\u043C\u0435\u0447\u0435\u043D \xABpaused\xBB. \u041A\u043E\u0433\u0434\u0430 \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F \u043D\u043E\u0432\u044B\u0439 overmind, \u043E\u043D \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442 \u0442\u0435\u0431\u044F \u0441 \u044D\u0442\u043E\u0433\u043E \u043C\u0435\u0441\u0442\u0430 (--continue) \u2014",
|
|
7776
8997
|
"\u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0448\u044C \u043E\u0442\u0442\u0443\u0434\u0430. \u041D\u0435 \u043F\u0435\u0440\u0435\u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0439 \u0438 \u043D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0437\u0430\u0434\u0430\u0447\u0443 \u0441\u0435\u0439\u0447\u0430\u0441."
|
|
@@ -7803,10 +9024,12 @@ async function buildDroneServer(store, opts = {}) {
|
|
|
7803
9024
|
const slugForInstruction = (fallback) => opts.expectedTaskSlug ?? fallback ?? "<slug>";
|
|
7804
9025
|
const freeze = async (withdraw = false) => {
|
|
7805
9026
|
let slug;
|
|
9027
|
+
let branch;
|
|
7806
9028
|
try {
|
|
7807
9029
|
if (withdraw) await store.withdrawMyQuestion("overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u0437\u0430\u043C\u0435\u0440, \u0432\u043E\u043F\u0440\u043E\u0441 \u0441\u043D\u044F\u0442");
|
|
7808
9030
|
const run = await store.myRun();
|
|
7809
9031
|
slug = run?.task_slug;
|
|
9032
|
+
branch = !opts.expectedTaskSlug || opts.expectedTaskSlug === run?.task_slug ? run?.branch : null;
|
|
7810
9033
|
if (run && run.status !== "paused") {
|
|
7811
9034
|
await store.updateRun(null, { status: "paused" });
|
|
7812
9035
|
await store.addEvent(null, "paused", "overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u0437\u0430\u043C\u0435\u0440, \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 origin, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430");
|
|
@@ -7814,7 +9037,7 @@ async function buildDroneServer(store, opts = {}) {
|
|
|
7814
9037
|
} catch (e) {
|
|
7815
9038
|
console.error("[overmind-drone] \u043F\u043E\u043C\u0435\u0442\u0438\u0442\u044C \u043F\u0440\u043E\u0433\u043E\u043D paused \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C (\u043D\u0430\u0434\u0437\u043E\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D):", e);
|
|
7816
9039
|
}
|
|
7817
|
-
return { content: [{ type: "text", text: freezeInstruction(slugForInstruction(slug), opts.deadReason) }] };
|
|
9040
|
+
return { content: [{ type: "text", text: freezeInstruction(branch || slugForInstruction(slug), opts.deadReason) }] };
|
|
7818
9041
|
};
|
|
7819
9042
|
const guard = (fn) => async (a) => {
|
|
7820
9043
|
try {
|
|
@@ -7838,7 +9061,7 @@ async function buildDroneServer(store, opts = {}) {
|
|
|
7838
9061
|
);
|
|
7839
9062
|
server.tool(
|
|
7840
9063
|
"read_task",
|
|
7841
|
-
"\u0421\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0442\u0432\u043E\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u041F\u0420\u042F\u041C\u041E \u0421\u0415\u0419\u0427\u0410\u0421: \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 + \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (\u0430\u0432\u0442\u043E\u0440+\u0432\u0440\u0435\u043C\u044F+\u0442\u0435\u043A\u0441\u0442) + \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0445 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432 (\u0441\u043F\u0435\u043A\u0438 \u043E\u0431\u044B\u0447\u043D\u043E \u043B\u0435\u0436\u0430\u0442 \u0438\u043C\u0435\u043D\u043D\u043E \u0442\u0430\u043C). \u0411\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432. \u0417\u043E\u0432\u0438 \u043F\u0435\u0440\u0435\u0434 done() \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0433 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0443\u0436\u0435 \u043F\u043E\u0441\u043B\u0435 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430, \u0430 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0443\u0432\u0438\u0434\u0438\u0442 (\u043E\u043D \u0441\u043E\u0431\u0440\u0430\u043D \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435).",
|
|
9064
|
+
"\u0421\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0442\u0432\u043E\u0435\u0439 \u0437\u0430\u0434\u0430\u0447\u0438 \u041F\u0420\u042F\u041C\u041E \u0421\u0415\u0419\u0427\u0410\u0421: \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 + \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438 + \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (\u0430\u0432\u0442\u043E\u0440+\u0432\u0440\u0435\u043C\u044F+\u0442\u0435\u043A\u0441\u0442) + \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0445 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432 (\u0441\u043F\u0435\u043A\u0438 \u043E\u0431\u044B\u0447\u043D\u043E \u043B\u0435\u0436\u0430\u0442 \u0438\u043C\u0435\u043D\u043D\u043E \u0442\u0430\u043C) + \u0447\u0435\u043C \u043A\u043E\u043D\u0447\u0438\u043B\u0438\u0441\u044C \u0441\u043E\u0441\u0435\u0434\u043D\u0438\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0442\u043E\u0439 \u0436\u0435 \u0437\u043E\u043D\u0435 \u043A\u043E\u0434\u0430 \u0438 \u043A\u0442\u043E \u0438\u0437 \u0440\u043E\u044F \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0440\u044F\u0434\u043E\u043C. \u0411\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432. \u0417\u043E\u0432\u0438 \u043F\u0435\u0440\u0435\u0434 done() \u2014 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0433 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0443\u0436\u0435 \u043F\u043E\u0441\u043B\u0435 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430, \u0430 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0443\u0432\u0438\u0434\u0438\u0442 (\u043E\u043D \u0441\u043E\u0431\u0440\u0430\u043D \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435).",
|
|
7842
9065
|
{},
|
|
7843
9066
|
async () => {
|
|
7844
9067
|
if (!opts.taskless) {
|
|
@@ -8051,9 +9274,12 @@ ${report}${shots.warning}`,
|
|
|
8051
9274
|
await store.addEvent(null, "step", `\u043E\u0442\u0447\u0451\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 ${taskSlug} \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${e instanceof Error ? e.message : String(e)}`);
|
|
8052
9275
|
}
|
|
8053
9276
|
};
|
|
9277
|
+
const headBranch = opts.prHeadBranch ? await opts.prHeadBranch(pr_url).catch(() => null) : null;
|
|
9278
|
+
const branchFix = branchFromPrPatch(run?.branch, headBranch);
|
|
9279
|
+
if (branchFix) await store.addEvent(null, "step", branchFix.event);
|
|
8054
9280
|
if (run) {
|
|
8055
9281
|
const t = reviewTransitionPatch(run, pr_url);
|
|
8056
|
-
await store.updateRun(null, t.patch);
|
|
9282
|
+
await store.updateRun(null, { ...t.patch, ...branchFix?.patch ?? {} });
|
|
8057
9283
|
await store.addEvent(null, "pr_opened", pr_url);
|
|
8058
9284
|
await postReport(reportSlug(run.task_slug), t.cycle ?? run.review_cycle ?? 0);
|
|
8059
9285
|
if (t.isFixIteration) {
|
|
@@ -8066,7 +9292,7 @@ ${report}${shots.warning}`,
|
|
|
8066
9292
|
}
|
|
8067
9293
|
return { content: [{ type: "text", text: `\u043F\u0440\u0438\u043D\u044F\u0442\u043E, \u0443\u0448\u043B\u043E \u043D\u0430 \u0440\u0435\u0432\u044C\u044E${shotsNote(shots)}` }] };
|
|
8068
9294
|
}
|
|
8069
|
-
await store.updateRun(null, { status: "review", pr_url });
|
|
9295
|
+
await store.updateRun(null, { status: "review", pr_url, ...branchFix?.patch ?? {} });
|
|
8070
9296
|
await store.addEvent(null, "pr_opened", pr_url);
|
|
8071
9297
|
const fallbackSlug = reportSlug(void 0);
|
|
8072
9298
|
if (fallbackSlug) await postReport(fallbackSlug, void 0);
|
|
@@ -8231,7 +9457,7 @@ async function runDroneServer(runId) {
|
|
|
8231
9457
|
);
|
|
8232
9458
|
const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
|
|
8233
9459
|
const expectedKind = toRunKind(process.env.SWARM_RUN_KIND);
|
|
8234
|
-
const common = { taskless, runKind: expectedKind, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
|
|
9460
|
+
const common = { taskless, runKind: expectedKind, prHeadBranch, ...expectedTaskSlug ? { expectedTaskSlug } : {} };
|
|
8235
9461
|
const channelUrl = process.env.SWARM_DRONE_CHANNEL_URL;
|
|
8236
9462
|
const channelToken = process.env.SWARM_DRONE_CHANNEL_TOKEN;
|
|
8237
9463
|
if (channelUrl && channelToken) {
|
|
@@ -8324,12 +9550,15 @@ var AnomalyTracker = class {
|
|
|
8324
9550
|
for (const [k, v] of Object.entries(restored.since)) this.since.set(k, v);
|
|
8325
9551
|
for (const [k, v] of Object.entries(restored.lastFiredAt)) this.lastFiredAt.set(k, v);
|
|
8326
9552
|
for (const [k, v] of Object.entries(restored.fires)) this.fires.set(k, v);
|
|
9553
|
+
for (const k of restored.resolved ?? []) this.resolved.add(k);
|
|
8327
9554
|
}
|
|
8328
9555
|
}
|
|
8329
9556
|
renotifyCooldownMs;
|
|
8330
9557
|
since = /* @__PURE__ */ new Map();
|
|
8331
9558
|
lastFiredAt = /* @__PURE__ */ new Map();
|
|
8332
9559
|
fires = /* @__PURE__ */ new Map();
|
|
9560
|
+
/** TAS-485: очередь «уже звонили, а условие ушло» — к гашению в инбоксе. */
|
|
9561
|
+
resolved = /* @__PURE__ */ new Set();
|
|
8333
9562
|
maxRepeats;
|
|
8334
9563
|
io;
|
|
8335
9564
|
/** Кандидаты, готовые к сигналу (отлежали minDurationMs, не в кулдауне) — НИЧЕГО не коммитит. */
|
|
@@ -8350,6 +9579,7 @@ var AnomalyTracker = class {
|
|
|
8350
9579
|
for (const map of [this.since, this.lastFiredAt, this.fires]) {
|
|
8351
9580
|
for (const k of [...map.keys()]) {
|
|
8352
9581
|
if (!seen.has(k)) {
|
|
9582
|
+
if (map === this.lastFiredAt) this.resolved.add(k);
|
|
8353
9583
|
map.delete(k);
|
|
8354
9584
|
pruned = true;
|
|
8355
9585
|
}
|
|
@@ -8362,8 +9592,25 @@ var AnomalyTracker = class {
|
|
|
8362
9592
|
commit(key, now) {
|
|
8363
9593
|
this.lastFiredAt.set(key, now);
|
|
8364
9594
|
this.fires.set(key, (this.fires.get(key) ?? 0) + 1);
|
|
9595
|
+
this.resolved.delete(key);
|
|
8365
9596
|
this.persist();
|
|
8366
9597
|
}
|
|
9598
|
+
/**
|
|
9599
|
+
* TAS-485: ключи, чьё условие ушло ПОСЛЕ того, как о нём звонили, — их записи
|
|
9600
|
+
* в инбоксе пора погасить. Список только ЧИТАЕТСЯ: вычёркивает доставленное
|
|
9601
|
+
* вызывающий (`forgetResolved`) и только после успеха — та же дисциплина, что
|
|
9602
|
+
* у eligible/commit. Проглоти мы сбой доставки как успех, тревога о том, чего
|
|
9603
|
+
* нет, осталась бы висеть навсегда и молча.
|
|
9604
|
+
*/
|
|
9605
|
+
pendingResolved() {
|
|
9606
|
+
return [...this.resolved];
|
|
9607
|
+
}
|
|
9608
|
+
/** Гашение по этим ключам доставлено — забываем их. */
|
|
9609
|
+
forgetResolved(keys) {
|
|
9610
|
+
let changed = false;
|
|
9611
|
+
for (const k of keys) changed = this.resolved.delete(k) || changed;
|
|
9612
|
+
if (changed) this.persist();
|
|
9613
|
+
}
|
|
8367
9614
|
/**
|
|
8368
9615
|
* Следующий сигнал по этому ключу — ПОСЛЕДНИЙ разрешённый в эпизоде: вызывающий меняет
|
|
8369
9616
|
* формулировку (LAST_REMINDER_SUFFIX), чтобы затихание не выглядело как «рой забыл».
|
|
@@ -8379,7 +9626,8 @@ var AnomalyTracker = class {
|
|
|
8379
9626
|
this.io.save({
|
|
8380
9627
|
since: Object.fromEntries(this.since),
|
|
8381
9628
|
lastFiredAt: Object.fromEntries(this.lastFiredAt),
|
|
8382
|
-
fires: Object.fromEntries(this.fires)
|
|
9629
|
+
fires: Object.fromEntries(this.fires),
|
|
9630
|
+
resolved: [...this.resolved]
|
|
8383
9631
|
});
|
|
8384
9632
|
} catch (e) {
|
|
8385
9633
|
console.error("[anomalies] \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043D\u0430\u0434\u0437\u043E\u0440\u0430 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u043D\u0430 \u0434\u0438\u0441\u043A:", e);
|
|
@@ -8433,8 +9681,9 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8433
9681
|
const changedAt = sqliteMs(run.updated_at);
|
|
8434
9682
|
return changedAt >= q.from && (q.to == null || changedAt <= q.to);
|
|
8435
9683
|
};
|
|
9684
|
+
const closedBeforeStart = (run) => opts.daemonStartedAt != null && sqliteMs(run.updated_at) < opts.daemonStartedAt;
|
|
8436
9685
|
const recentFailed = allRuns.filter(
|
|
8437
|
-
(r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r)
|
|
9686
|
+
(r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs && !killedByOperator(r) && !closedBeforeStart(r)
|
|
8438
9687
|
);
|
|
8439
9688
|
if (recentFailed.length >= thresholds.failedSpikeCount) {
|
|
8440
9689
|
candidates.push({
|
|
@@ -8471,6 +9720,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8471
9720
|
if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
|
|
8472
9721
|
if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
|
|
8473
9722
|
if (killedByOperator(run)) continue;
|
|
9723
|
+
if (closedBeforeStart(run)) continue;
|
|
8474
9724
|
const repeat = isRepeatDown(run);
|
|
8475
9725
|
candidates.push({
|
|
8476
9726
|
key: `run_stalled:${run.id}`,
|
|
@@ -8491,6 +9741,7 @@ function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
|
8491
9741
|
if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
|
|
8492
9742
|
if (ageMs(now, run.updated_at) > thresholds.runDownWindowMs) continue;
|
|
8493
9743
|
if (killedByOperator(run)) continue;
|
|
9744
|
+
if (closedBeforeStart(run)) continue;
|
|
8494
9745
|
const repeat = isRepeatDown(run);
|
|
8495
9746
|
candidates.push({
|
|
8496
9747
|
key: `run_failed:${run.id}`,
|
|
@@ -8910,10 +10161,31 @@ function makeExclusiveReader(opts) {
|
|
|
8910
10161
|
}
|
|
8911
10162
|
|
|
8912
10163
|
// src/envGate.ts
|
|
8913
|
-
import { existsSync as
|
|
8914
|
-
import { join as
|
|
10164
|
+
import { existsSync as existsSync13 } from "fs";
|
|
10165
|
+
import { join as join23 } from "path";
|
|
8915
10166
|
import { createConnection } from "net";
|
|
8916
10167
|
import { execa as execa2 } from "execa";
|
|
10168
|
+
|
|
10169
|
+
// src/platform.ts
|
|
10170
|
+
import { existsSync as existsSync12, readFileSync as readFileSync17 } from "fs";
|
|
10171
|
+
import { delimiter, join as join22 } from "path";
|
|
10172
|
+
function resolveCommandPath(name, env = process.env, platform = process.platform) {
|
|
10173
|
+
if (name.includes("/") || name.includes("\\")) return existsSync12(name) ? name : null;
|
|
10174
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(delimiter).filter(Boolean);
|
|
10175
|
+
const exts = platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
|
|
10176
|
+
for (const dir of dirs) {
|
|
10177
|
+
for (const ext of exts) {
|
|
10178
|
+
const candidate = join22(dir, name + ext);
|
|
10179
|
+
if (existsSync12(candidate)) return candidate;
|
|
10180
|
+
}
|
|
10181
|
+
}
|
|
10182
|
+
return null;
|
|
10183
|
+
}
|
|
10184
|
+
function hasCommandInPath(name, env = process.env, platform = process.platform) {
|
|
10185
|
+
return resolveCommandPath(name, env, platform) !== null;
|
|
10186
|
+
}
|
|
10187
|
+
|
|
10188
|
+
// src/envGate.ts
|
|
8917
10189
|
var PROBE_TIMEOUT_MS = 2500;
|
|
8918
10190
|
var PORT_TIMEOUT_MS = 1e3;
|
|
8919
10191
|
function parseRequirements(lines) {
|
|
@@ -8962,8 +10234,8 @@ var COMPOSE_MARKERS = [
|
|
|
8962
10234
|
"compose.yaml"
|
|
8963
10235
|
];
|
|
8964
10236
|
function detectRequirements(repo, io = {}) {
|
|
8965
|
-
const exists = io.exists ??
|
|
8966
|
-
const marker = COMPOSE_MARKERS.find((f) => exists(
|
|
10237
|
+
const exists = io.exists ?? existsSync13;
|
|
10238
|
+
const marker = COMPOSE_MARKERS.find((f) => exists(join23(repo, f)));
|
|
8967
10239
|
return marker ? [{ kind: "docker", raw: `docker (\u0434\u0435\u0442\u0435\u043A\u0442: ${marker})` }] : [];
|
|
8968
10240
|
}
|
|
8969
10241
|
function resolveRequirements(repo, declared, io = {}) {
|
|
@@ -8982,17 +10254,14 @@ async function dockerAlive() {
|
|
|
8982
10254
|
}
|
|
8983
10255
|
}
|
|
8984
10256
|
async function hasCommand(name, env) {
|
|
8985
|
-
|
|
8986
|
-
const dirs = (env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
8987
|
-
const exts = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
|
|
8988
|
-
return dirs.some((dir) => exts.some((ext) => existsSync10(join18(dir, name + ext))));
|
|
10257
|
+
return hasCommandInPath(name, env);
|
|
8989
10258
|
}
|
|
8990
10259
|
async function portOpen(host, port) {
|
|
8991
|
-
return new Promise((
|
|
10260
|
+
return new Promise((resolve4) => {
|
|
8992
10261
|
const socket = createConnection({ host, port });
|
|
8993
10262
|
const finish = (ok) => {
|
|
8994
10263
|
socket.destroy();
|
|
8995
|
-
|
|
10264
|
+
resolve4(ok);
|
|
8996
10265
|
};
|
|
8997
10266
|
socket.setTimeout(PORT_TIMEOUT_MS);
|
|
8998
10267
|
socket.once("connect", () => finish(true));
|
|
@@ -9102,8 +10371,8 @@ import { basename as basename3 } from "path";
|
|
|
9102
10371
|
import { serve as serve2 } from "@hono/node-server";
|
|
9103
10372
|
|
|
9104
10373
|
// src/diff.ts
|
|
9105
|
-
import { statSync as
|
|
9106
|
-
import { join as
|
|
10374
|
+
import { statSync as statSync8 } from "fs";
|
|
10375
|
+
import { join as join24 } from "path";
|
|
9107
10376
|
import { $ as $4 } from "execa";
|
|
9108
10377
|
var MAX_FILES = 200;
|
|
9109
10378
|
var MAX_LINES_PER_FILE = 1500;
|
|
@@ -9246,7 +10515,7 @@ async function resolveBase(wt, baseBranch) {
|
|
|
9246
10515
|
}
|
|
9247
10516
|
function mtime(path) {
|
|
9248
10517
|
try {
|
|
9249
|
-
return
|
|
10518
|
+
return statSync8(path).mtimeMs;
|
|
9250
10519
|
} catch {
|
|
9251
10520
|
return 0;
|
|
9252
10521
|
}
|
|
@@ -9271,7 +10540,7 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
9271
10540
|
files.push(pf);
|
|
9272
10541
|
}
|
|
9273
10542
|
}
|
|
9274
|
-
files.sort((a, b) => mtime(
|
|
10543
|
+
files.sort((a, b) => mtime(join24(worktree, b.path)) - mtime(join24(worktree, a.path)));
|
|
9275
10544
|
const truncatedFiles = files.length > MAX_FILES;
|
|
9276
10545
|
if (truncatedFiles) files = files.slice(0, MAX_FILES);
|
|
9277
10546
|
const added = files.reduce((s, f) => s + f.added, 0);
|
|
@@ -9286,11 +10555,25 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
9286
10555
|
...truncatedFiles ? { truncated: true } : {}
|
|
9287
10556
|
};
|
|
9288
10557
|
}
|
|
10558
|
+
async function changedPaths(worktree, opts = {}) {
|
|
10559
|
+
if (!worktree) return [];
|
|
10560
|
+
try {
|
|
10561
|
+
const isWt = (await $4({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
|
|
10562
|
+
if (isWt !== "true") return [];
|
|
10563
|
+
const from = await resolveBase(worktree, opts.baseBranch) ?? "HEAD";
|
|
10564
|
+
const tracked = (await $4({ reject: false })`git -C ${worktree} diff --name-only --find-renames ${from} --`).stdout;
|
|
10565
|
+
const untracked = (await $4({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
|
|
10566
|
+
const all = [...tracked.split("\n"), ...untracked.split("\0")].map((s) => s.trim()).filter(Boolean);
|
|
10567
|
+
return [...new Set(all)].sort();
|
|
10568
|
+
} catch {
|
|
10569
|
+
return [];
|
|
10570
|
+
}
|
|
10571
|
+
}
|
|
9289
10572
|
|
|
9290
10573
|
// package.json
|
|
9291
10574
|
var package_default = {
|
|
9292
10575
|
name: "@taskless-app/swarm",
|
|
9293
|
-
version: "0.
|
|
10576
|
+
version: "0.6.0",
|
|
9294
10577
|
type: "module",
|
|
9295
10578
|
description: "Local control plane for a fleet of AI coding agents (drones), coordinating through Taskless. The overmind runs the swarm.",
|
|
9296
10579
|
keywords: [
|
|
@@ -9342,13 +10625,16 @@ var package_default = {
|
|
|
9342
10625
|
}
|
|
9343
10626
|
};
|
|
9344
10627
|
|
|
10628
|
+
// src/version.ts
|
|
10629
|
+
var PACKAGE_VERSION = package_default.version;
|
|
10630
|
+
var MIN_NODE_MAJOR = Number(/(\d+)/.exec(package_default.engines?.node ?? "")?.[1] ?? 0);
|
|
10631
|
+
|
|
9345
10632
|
// src/web.ts
|
|
9346
10633
|
init_storm();
|
|
9347
10634
|
init_resources();
|
|
9348
10635
|
init_config();
|
|
9349
10636
|
init_droneChannel();
|
|
9350
10637
|
init_oauth();
|
|
9351
|
-
var PACKAGE_VERSION = package_default.version;
|
|
9352
10638
|
var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
9353
10639
|
var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
|
|
9354
10640
|
var TASKLESS_MARK = `<svg viewBox="0 0 60 54" width="19" height="19" fill="none" aria-hidden="true" style="display:block"><g transform="matrix(1.0512,0,0,1.0512,0,-0.021)"><path fill="currentColor" fill-rule="nonzero" d="M46.92,0 L10.24,0 C4.59,0 0,4.59 0,10.24 L0,10.4 C0,16.05 4.59,20.64 10.24,20.64 L16.84,20.64 C19.56,20.64 22.12,19.59 24.05,17.67 L36.06,5.77 C37.25,4.6 38.82,3.95 40.49,3.95 L46.93,3.95 C50.4,3.95 53.22,6.77 53.22,10.24 L53.22,10.4 C53.22,13.87 50.4,16.69 46.93,16.69 L39.82,16.69 C37.05,16.69 34.36,17.83 32.44,19.83 L21.13,31.58 C19.28,33.5 18.27,36.02 18.27,38.68 L18.27,40.94 C18.27,46.59 22.86,51.18 28.51,51.18 C34.16,51.18 38.75,46.59 38.75,40.94 L38.75,20.74 C39.1,20.68 39.46,20.64 39.82,20.64 L46.93,20.64 C52.58,20.64 57.17,16.05 57.17,10.4 L57.17,10.24 C57.17,4.59 52.58,0 46.93,0 L46.92,0 Z M21.26,14.87 C20.07,16.04 18.5,16.69 16.83,16.69 L10.23,16.69 C6.76,16.69 3.94,13.87 3.94,10.4 L3.94,10.24 C3.94,6.77 6.76,3.95 10.23,3.95 L32.27,3.95 L21.25,14.87 L21.26,14.87 Z M34.79,40.94 C34.79,44.41 31.97,47.23 28.5,47.23 C25.03,47.23 22.21,44.41 22.21,40.94 L22.21,38.68 C22.21,37.05 22.83,35.5 23.97,34.32 L34.79,23.07 L34.79,40.94 L34.79,40.94 Z"/></g></svg>`;
|
|
@@ -9404,6 +10690,10 @@ function roySnapshot(db, now = Date.now(), overmindLog = [], paused = false, dae
|
|
|
9404
10690
|
stopReason: r.stop_reason,
|
|
9405
10691
|
reason,
|
|
9406
10692
|
updated: r.updated_at,
|
|
10693
|
+
startedAt: r.created_at,
|
|
10694
|
+
// TASWM-351: возраст прогона на карточке
|
|
10695
|
+
costUsd: r.cost_usd || 0,
|
|
10696
|
+
// TASWM-351: то же поле, что уходит в прод (swarm_run_upsert{costUsd})
|
|
9407
10697
|
step,
|
|
9408
10698
|
hasDecision: withQ.has(r.id),
|
|
9409
10699
|
rescued: evs.some((e) => e.kind === "rescued"),
|
|
@@ -10656,7 +11946,8 @@ function daemonStatus(deps) {
|
|
|
10656
11946
|
restart: deps.restart ? { phase: deps.restart.phase, intent: deps.restart.intent, sessions: live } : null,
|
|
10657
11947
|
counts,
|
|
10658
11948
|
live,
|
|
10659
|
-
version: PACKAGE_VERSION
|
|
11949
|
+
version: PACKAGE_VERSION,
|
|
11950
|
+
repos: deps.repoMap?.() ?? []
|
|
10660
11951
|
};
|
|
10661
11952
|
}
|
|
10662
11953
|
function eventsStream(db, narratorLog, control, daemonVersion, restart, liveSessionIds2, selfHost) {
|
|
@@ -10904,8 +12195,8 @@ function startWeb(deps, port = WEB_PORT) {
|
|
|
10904
12195
|
return { port, server };
|
|
10905
12196
|
}
|
|
10906
12197
|
function closeWebServer(server) {
|
|
10907
|
-
return new Promise((
|
|
10908
|
-
server.close(() =>
|
|
12198
|
+
return new Promise((resolve4) => {
|
|
12199
|
+
server.close(() => resolve4());
|
|
10909
12200
|
server.closeAllConnections?.();
|
|
10910
12201
|
});
|
|
10911
12202
|
}
|
|
@@ -10949,24 +12240,57 @@ async function checkDaemonVersion(start, now = () => /* @__PURE__ */ new Date())
|
|
|
10949
12240
|
}
|
|
10950
12241
|
}
|
|
10951
12242
|
|
|
12243
|
+
// src/release.ts
|
|
12244
|
+
init_claudeBin();
|
|
12245
|
+
var SWARM_PACKAGE = "@taskless-app/swarm";
|
|
12246
|
+
var LATEST_TIMEOUT_MS = 4e3;
|
|
12247
|
+
async function fetchLatestVersion(opts = {}) {
|
|
12248
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
12249
|
+
const timeoutMs = opts.timeoutMs ?? LATEST_TIMEOUT_MS;
|
|
12250
|
+
const pkg = opts.pkg ?? SWARM_PACKAGE;
|
|
12251
|
+
const ctrl = new AbortController();
|
|
12252
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
12253
|
+
try {
|
|
12254
|
+
const res = await fetchFn(`https://registry.npmjs.org/${pkg}/latest`, {
|
|
12255
|
+
signal: ctrl.signal,
|
|
12256
|
+
headers: { accept: "application/vnd.npm.install-v1+json, application/json" }
|
|
12257
|
+
});
|
|
12258
|
+
if (!res.ok) return null;
|
|
12259
|
+
const body = await res.json();
|
|
12260
|
+
return typeof body.version === "string" && body.version ? body.version : null;
|
|
12261
|
+
} catch {
|
|
12262
|
+
return null;
|
|
12263
|
+
} finally {
|
|
12264
|
+
clearTimeout(timer);
|
|
12265
|
+
}
|
|
12266
|
+
}
|
|
12267
|
+
function isOutdated(current, latest) {
|
|
12268
|
+
if (!latest || !current) return false;
|
|
12269
|
+
return compareVersionDirs(latest, current) > 0;
|
|
12270
|
+
}
|
|
12271
|
+
function outdatedLine(current, latest, updateCommand = "swarm update") {
|
|
12272
|
+
if (!isOutdated(current, latest)) return null;
|
|
12273
|
+
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}`;
|
|
12274
|
+
}
|
|
12275
|
+
|
|
10952
12276
|
// src/hostCommands.ts
|
|
10953
12277
|
init_src();
|
|
10954
|
-
import { readFileSync as
|
|
10955
|
-
import { join as
|
|
12278
|
+
import { readFileSync as readFileSync18, renameSync as renameSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
12279
|
+
import { join as join25 } from "path";
|
|
10956
12280
|
function hostCommandStatePath(home) {
|
|
10957
|
-
return
|
|
12281
|
+
return join25(home, "host-command.json");
|
|
10958
12282
|
}
|
|
10959
12283
|
function readState(path) {
|
|
10960
12284
|
try {
|
|
10961
|
-
return JSON.parse(
|
|
12285
|
+
return JSON.parse(readFileSync18(path, "utf8"));
|
|
10962
12286
|
} catch {
|
|
10963
12287
|
return {};
|
|
10964
12288
|
}
|
|
10965
12289
|
}
|
|
10966
12290
|
function writeState(path, state) {
|
|
10967
12291
|
try {
|
|
10968
|
-
|
|
10969
|
-
|
|
12292
|
+
writeFileSync13(`${path}.tmp`, JSON.stringify(state));
|
|
12293
|
+
renameSync8(`${path}.tmp`, path);
|
|
10970
12294
|
} catch {
|
|
10971
12295
|
}
|
|
10972
12296
|
}
|
|
@@ -11064,6 +12388,28 @@ async function executeOne(cmd, opts) {
|
|
|
11064
12388
|
await ack(cmd.id, "done", { result: `\u043F\u0440\u043E\u0433\u043E\u043D ${slug} \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D` });
|
|
11065
12389
|
return false;
|
|
11066
12390
|
}
|
|
12391
|
+
// TASWM-351: «перезапустить с того же места». Отказ побудки (дрон уже работает,
|
|
12392
|
+
// продолжать нечем) уезжает как `rejected` с ЕЁ формулировкой — повторять такую
|
|
12393
|
+
// команду бессмысленно, и это ровно та разница между rejected и failed, ради
|
|
12394
|
+
// которой они разделены (см. HOST_COMMAND_STATUSES).
|
|
12395
|
+
case "wake_run": {
|
|
12396
|
+
const slug = cmd.runSlug?.trim();
|
|
12397
|
+
if (!slug) {
|
|
12398
|
+
await ack(cmd.id, "rejected", { result: "\u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043D\u043E, \u043A\u0430\u043A\u043E\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0442\u044C" });
|
|
12399
|
+
return false;
|
|
12400
|
+
}
|
|
12401
|
+
const woken = await effects.wakeRun(slug);
|
|
12402
|
+
if (!woken) {
|
|
12403
|
+
await ack(cmd.id, "rejected", { result: `\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043F\u043E ${slug} \u0443 \u043C\u0435\u043D\u044F \u043D\u0435\u0442` });
|
|
12404
|
+
return false;
|
|
12405
|
+
}
|
|
12406
|
+
if (!woken.ok) {
|
|
12407
|
+
await ack(cmd.id, "rejected", { result: woken.why });
|
|
12408
|
+
return false;
|
|
12409
|
+
}
|
|
12410
|
+
await ack(cmd.id, "done", { result: woken.why });
|
|
12411
|
+
return false;
|
|
12412
|
+
}
|
|
11067
12413
|
case "restart":
|
|
11068
12414
|
return await doRestart(cmd, opts, "\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B\u0441\u044F \u043F\u043E \u043A\u043E\u043C\u0430\u043D\u0434\u0435 \u0438\u0437 Taskless");
|
|
11069
12415
|
case "update": {
|
|
@@ -11101,8 +12447,8 @@ async function doRestart(cmd, opts, note) {
|
|
|
11101
12447
|
|
|
11102
12448
|
// src/selfUpdate.ts
|
|
11103
12449
|
init_git();
|
|
11104
|
-
import { existsSync as
|
|
11105
|
-
import { join as
|
|
12450
|
+
import { existsSync as existsSync14 } from "fs";
|
|
12451
|
+
import { join as join26 } from "path";
|
|
11106
12452
|
import { $ as $6 } from "execa";
|
|
11107
12453
|
var DAEMON_PACKAGE = "@taskless-app/swarm";
|
|
11108
12454
|
var defaultRun = async (cmd, args, cwd) => {
|
|
@@ -11115,7 +12461,7 @@ function tail(text, max = 400) {
|
|
|
11115
12461
|
}
|
|
11116
12462
|
async function runSelfUpdate(io) {
|
|
11117
12463
|
const run = io.run ?? defaultRun;
|
|
11118
|
-
const exists = io.exists ??
|
|
12464
|
+
const exists = io.exists ?? existsSync14;
|
|
11119
12465
|
const repo = io.repo;
|
|
11120
12466
|
await io.onStep("fetch", "\u0442\u044F\u043D\u0443 \u0441\u0432\u0435\u0436\u0438\u0439 \u043A\u043E\u0434");
|
|
11121
12467
|
const fetched = await run("git", ["-C", repo, "fetch", "--quiet", "origin"], repo);
|
|
@@ -11144,7 +12490,7 @@ async function runSelfUpdate(io) {
|
|
|
11144
12490
|
};
|
|
11145
12491
|
}
|
|
11146
12492
|
await io.onStep("install", "\u0441\u0442\u0430\u0432\u043B\u044E \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438");
|
|
11147
|
-
if (!exists(
|
|
12493
|
+
if (!exists(join26(repo, "pnpm-lock.yaml"))) {
|
|
11148
12494
|
return { ok: false, step: "install", message: "\u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0434\u0435\u043C\u043E\u043D\u0430 \u043D\u0435\u0442 pnpm-lock.yaml \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u0435\u0433\u043E \u044D\u0442\u0438\u043C \u043F\u0443\u0442\u0451\u043C \u043D\u0435 \u0431\u0435\u0440\u0443\u0441\u044C" };
|
|
11149
12495
|
}
|
|
11150
12496
|
const installed = await run("pnpm", ["install", "--frozen-lockfile"], repo);
|
|
@@ -11178,13 +12524,14 @@ function makeOvermindNarrator() {
|
|
|
11178
12524
|
}
|
|
11179
12525
|
|
|
11180
12526
|
// src/probe.ts
|
|
12527
|
+
init_executors();
|
|
11181
12528
|
import { spawn as spawn2 } from "child_process";
|
|
11182
|
-
var LIMIT_RE = /(weekly limit|usage limit|hit your .*limit)/i;
|
|
11183
12529
|
var RESET_RE = /resets[^\n]*/i;
|
|
11184
|
-
function
|
|
11185
|
-
const
|
|
12530
|
+
function executorHealthy(opts = {}) {
|
|
12531
|
+
const executor = opts.executor ?? resolveExecutor();
|
|
12532
|
+
const bin = opts.bin ?? executor.resolveBin();
|
|
11186
12533
|
const timeoutMs = opts.timeoutMs ?? 2e4;
|
|
11187
|
-
return new Promise((
|
|
12534
|
+
return new Promise((resolve4) => {
|
|
11188
12535
|
let done = false;
|
|
11189
12536
|
let out = "";
|
|
11190
12537
|
const finish = (r) => {
|
|
@@ -11195,9 +12542,9 @@ function anthropicHealthy(opts = {}) {
|
|
|
11195
12542
|
child.kill("SIGKILL");
|
|
11196
12543
|
} catch {
|
|
11197
12544
|
}
|
|
11198
|
-
|
|
12545
|
+
resolve4(r);
|
|
11199
12546
|
};
|
|
11200
|
-
const child = spawn2(bin, [...
|
|
12547
|
+
const child = spawn2(bin, [...executor.probe.args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
11201
12548
|
child.stdout?.on("data", (d) => {
|
|
11202
12549
|
out += d.toString();
|
|
11203
12550
|
});
|
|
@@ -11208,7 +12555,9 @@ function anthropicHealthy(opts = {}) {
|
|
|
11208
12555
|
child.on("error", () => finish({ ok: false, reason: "exec", resetHint: null }));
|
|
11209
12556
|
child.on("exit", (code) => {
|
|
11210
12557
|
if (code === 0) return finish({ ok: true, reason: "ok", resetHint: null });
|
|
11211
|
-
if (
|
|
12558
|
+
if (executor.probe.limitPattern.test(out)) {
|
|
12559
|
+
return finish({ ok: false, reason: "limit", resetHint: out.match(RESET_RE)?.[0]?.trim() ?? null });
|
|
12560
|
+
}
|
|
11212
12561
|
finish({ ok: false, reason: "provider", resetHint: null });
|
|
11213
12562
|
});
|
|
11214
12563
|
});
|
|
@@ -11234,18 +12583,21 @@ async function fetchExternalStorm(url, opts = {}) {
|
|
|
11234
12583
|
}
|
|
11235
12584
|
|
|
11236
12585
|
// src/anomalyState.ts
|
|
11237
|
-
import { existsSync as
|
|
11238
|
-
import { join as
|
|
12586
|
+
import { existsSync as existsSync15, readFileSync as readFileSync19, renameSync as renameSync9, writeFileSync as writeFileSync14 } from "fs";
|
|
12587
|
+
import { join as join27 } from "path";
|
|
11239
12588
|
var KEY_TTL_MS = 7 * 24 * 60 * 6e4;
|
|
11240
|
-
var
|
|
12589
|
+
var MAX_RESOLVED = 200;
|
|
12590
|
+
function emptyState() {
|
|
12591
|
+
return { since: {}, lastFiredAt: {}, fires: {}, resolved: [], stopQuiet: null };
|
|
12592
|
+
}
|
|
11241
12593
|
function anomalyStatePath(home) {
|
|
11242
|
-
return
|
|
12594
|
+
return join27(home, "anomaly-state.json");
|
|
11243
12595
|
}
|
|
11244
12596
|
function loadAnomalyState(home, now = Date.now(), io = {}) {
|
|
11245
|
-
const exists = io.exists ??
|
|
11246
|
-
const readFile2 = io.readFile ?? ((p) =>
|
|
12597
|
+
const exists = io.exists ?? existsSync15;
|
|
12598
|
+
const readFile2 = io.readFile ?? ((p) => readFileSync19(p, "utf8"));
|
|
11247
12599
|
const path = anomalyStatePath(home);
|
|
11248
|
-
if (!exists(path)) return
|
|
12600
|
+
if (!exists(path)) return emptyState();
|
|
11249
12601
|
try {
|
|
11250
12602
|
const raw = JSON.parse(readFile2(path));
|
|
11251
12603
|
const since = numbers(raw.since);
|
|
@@ -11260,17 +12612,21 @@ function loadAnomalyState(home, now = Date.now(), io = {}) {
|
|
|
11260
12612
|
since: pick(since, fresh),
|
|
11261
12613
|
lastFiredAt: pick(lastFiredAt, fresh),
|
|
11262
12614
|
fires: pick(fires, fresh),
|
|
12615
|
+
// TAS-485: очередь гашения НЕ фильтруем тем же TTL, что ключи. Она про
|
|
12616
|
+
// уведомление, которое УЖЕ висит у человека в инбоксе: пролежав неделю
|
|
12617
|
+
// недоставленным, гашение не становится лишним — оно становится нужнее.
|
|
12618
|
+
resolved: strings(raw.resolved).slice(0, MAX_RESOLVED),
|
|
11263
12619
|
stopQuiet: parseQuiet(raw.stopQuiet)
|
|
11264
12620
|
};
|
|
11265
12621
|
} catch {
|
|
11266
|
-
return
|
|
12622
|
+
return emptyState();
|
|
11267
12623
|
}
|
|
11268
12624
|
}
|
|
11269
12625
|
function saveAnomalyState(home, state, io = {}) {
|
|
11270
12626
|
const path = anomalyStatePath(home);
|
|
11271
12627
|
const writeFile2 = io.writeFile ?? ((p, data) => {
|
|
11272
|
-
|
|
11273
|
-
|
|
12628
|
+
writeFileSync14(`${p}.tmp`, data);
|
|
12629
|
+
renameSync9(`${p}.tmp`, p);
|
|
11274
12630
|
});
|
|
11275
12631
|
writeFile2(path, JSON.stringify(state, null, 2));
|
|
11276
12632
|
}
|
|
@@ -11286,9 +12642,14 @@ var AnomalyStateStore = class {
|
|
|
11286
12642
|
/** Инъекция в AnomalyTracker (см. AnomalyTrackerOpts.io). */
|
|
11287
12643
|
trackerIo() {
|
|
11288
12644
|
return {
|
|
11289
|
-
load: () => ({
|
|
12645
|
+
load: () => ({
|
|
12646
|
+
since: this.state.since,
|
|
12647
|
+
lastFiredAt: this.state.lastFiredAt,
|
|
12648
|
+
fires: this.state.fires,
|
|
12649
|
+
resolved: this.state.resolved
|
|
12650
|
+
}),
|
|
11290
12651
|
save: (snapshot) => {
|
|
11291
|
-
this.state = { ...snapshot, stopQuiet: this.state.stopQuiet };
|
|
12652
|
+
this.state = { ...snapshot, resolved: (snapshot.resolved ?? []).slice(0, MAX_RESOLVED), stopQuiet: this.state.stopQuiet };
|
|
11292
12653
|
this.flush();
|
|
11293
12654
|
}
|
|
11294
12655
|
};
|
|
@@ -11330,6 +12691,10 @@ function numbers(raw) {
|
|
|
11330
12691
|
}
|
|
11331
12692
|
return out;
|
|
11332
12693
|
}
|
|
12694
|
+
function strings(raw) {
|
|
12695
|
+
if (!Array.isArray(raw)) return [];
|
|
12696
|
+
return raw.filter((v) => typeof v === "string" && v.length > 0);
|
|
12697
|
+
}
|
|
11333
12698
|
function pick(src, keys) {
|
|
11334
12699
|
const out = {};
|
|
11335
12700
|
for (const k of keys) if (src[k] != null) out[k] = src[k];
|
|
@@ -11362,7 +12727,7 @@ function isRepeatEvent(prev, cur) {
|
|
|
11362
12727
|
}
|
|
11363
12728
|
function swarmLastActivityMs(db) {
|
|
11364
12729
|
let latest = null;
|
|
11365
|
-
const
|
|
12730
|
+
const bump2 = (iso) => {
|
|
11366
12731
|
const ms = sqliteMs(iso);
|
|
11367
12732
|
if (latest == null || ms > latest) latest = ms;
|
|
11368
12733
|
};
|
|
@@ -11370,12 +12735,12 @@ function swarmLastActivityMs(db) {
|
|
|
11370
12735
|
if (isTerminalStatus(run.status)) continue;
|
|
11371
12736
|
const evs = db.events(run.id);
|
|
11372
12737
|
if (!evs.length) {
|
|
11373
|
-
|
|
12738
|
+
bump2(run.updated_at);
|
|
11374
12739
|
continue;
|
|
11375
12740
|
}
|
|
11376
12741
|
let i = evs.length - 1;
|
|
11377
12742
|
while (i > 0 && isRepeatEvent(evs[i - 1], evs[i])) i--;
|
|
11378
|
-
|
|
12743
|
+
bump2(evs[i].at);
|
|
11379
12744
|
}
|
|
11380
12745
|
return latest;
|
|
11381
12746
|
}
|
|
@@ -11501,7 +12866,193 @@ function decideWatchdogAction(db, now, state, thresholds, droneCap, wt, exclusiv
|
|
|
11501
12866
|
// src/overmind.ts
|
|
11502
12867
|
init_storm();
|
|
11503
12868
|
init_src();
|
|
12869
|
+
|
|
12870
|
+
// src/zones.ts
|
|
12871
|
+
init_src();
|
|
12872
|
+
|
|
12873
|
+
// src/gitHistory.ts
|
|
12874
|
+
import { $ as $7 } from "execa";
|
|
12875
|
+
var REC = "";
|
|
12876
|
+
var FIELD = "";
|
|
12877
|
+
var LOG_FORMAT = `${REC}%H${FIELD}%s`;
|
|
12878
|
+
var SLUG_IN_SUBJECT = /\b([A-Z][A-Z0-9]{0,7}-\d+)\b/;
|
|
12879
|
+
function slugFromSubject(subject) {
|
|
12880
|
+
return SLUG_IN_SUBJECT.exec(subject)?.[1] ?? null;
|
|
12881
|
+
}
|
|
12882
|
+
function unquotePath(raw) {
|
|
12883
|
+
const p = raw.trim();
|
|
12884
|
+
return p.length >= 2 && p.startsWith('"') && p.endsWith('"') ? p.slice(1, -1) : p;
|
|
12885
|
+
}
|
|
12886
|
+
function parseGitLog(out) {
|
|
12887
|
+
const commits = [];
|
|
12888
|
+
for (const chunk of out.split(REC)) {
|
|
12889
|
+
if (!chunk.trim()) continue;
|
|
12890
|
+
const [head, ...rest] = chunk.split("\n");
|
|
12891
|
+
const [sha, subject] = (head ?? "").split(FIELD);
|
|
12892
|
+
if (!sha) continue;
|
|
12893
|
+
const files = rest.map(unquotePath).filter(Boolean);
|
|
12894
|
+
commits.push({ sha, subject: subject ?? "", files });
|
|
12895
|
+
}
|
|
12896
|
+
return commits;
|
|
12897
|
+
}
|
|
12898
|
+
var NOISE_RE = /^(journal\/|changelog\/|docs\/|design\/|scratch\/|\.claude\/|.*\.lock$|pnpm-lock\.yaml$)/;
|
|
12899
|
+
function isNoisePath(path) {
|
|
12900
|
+
return NOISE_RE.test(path);
|
|
12901
|
+
}
|
|
12902
|
+
var GROUP_DEFAULTS = { maxFilesPerCommit: 120, ignore: isNoisePath };
|
|
12903
|
+
function groupTaskHistory(commits, opts = {}) {
|
|
12904
|
+
const o = { ...GROUP_DEFAULTS, ...opts };
|
|
12905
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
12906
|
+
for (const c of commits) {
|
|
12907
|
+
const slug = slugFromSubject(c.subject);
|
|
12908
|
+
if (!slug) continue;
|
|
12909
|
+
if (c.files.length > o.maxFilesPerCommit) continue;
|
|
12910
|
+
const files = c.files.filter((f) => !o.ignore(f));
|
|
12911
|
+
if (!files.length) continue;
|
|
12912
|
+
let acc = bySlug.get(slug);
|
|
12913
|
+
if (!acc) bySlug.set(slug, acc = { files: /* @__PURE__ */ new Set(), texts: [] });
|
|
12914
|
+
for (const f of files) acc.files.add(f);
|
|
12915
|
+
acc.texts.push(c.subject);
|
|
12916
|
+
}
|
|
12917
|
+
return [...bySlug.entries()].map(([slug, acc]) => ({
|
|
12918
|
+
slug,
|
|
12919
|
+
files: [...acc.files],
|
|
12920
|
+
text: acc.texts.join(" \xB7 ")
|
|
12921
|
+
}));
|
|
12922
|
+
}
|
|
12923
|
+
async function readCommitLog(repo, opts = {}) {
|
|
12924
|
+
const max = opts.maxCommits ?? 1500;
|
|
12925
|
+
try {
|
|
12926
|
+
const { stdout } = await $7({ cwd: repo, reject: false })`git log --no-merges --name-only --pretty=format:${LOG_FORMAT} -n ${String(max)} ${opts.ref ?? "HEAD"}`;
|
|
12927
|
+
return parseGitLog(stdout ?? "");
|
|
12928
|
+
} catch {
|
|
12929
|
+
return [];
|
|
12930
|
+
}
|
|
12931
|
+
}
|
|
12932
|
+
async function readTaskHistory(repo, opts = {}) {
|
|
12933
|
+
return groupTaskHistory(await readCommitLog(repo, opts), opts);
|
|
12934
|
+
}
|
|
12935
|
+
async function headSha(repo) {
|
|
12936
|
+
try {
|
|
12937
|
+
const { stdout, exitCode } = await $7({ cwd: repo, reject: false })`git rev-parse HEAD`;
|
|
12938
|
+
return exitCode === 0 ? stdout.trim() : null;
|
|
12939
|
+
} catch {
|
|
12940
|
+
return null;
|
|
12941
|
+
}
|
|
12942
|
+
}
|
|
12943
|
+
|
|
12944
|
+
// src/zones.ts
|
|
12945
|
+
function runText(run) {
|
|
12946
|
+
return [run.task_title ?? "", run.task_desc ?? ""].join(" ").trim();
|
|
12947
|
+
}
|
|
12948
|
+
function predictForRun(model, run) {
|
|
12949
|
+
return predictFiles(model, { zone: run.area, text: runText(run) });
|
|
12950
|
+
}
|
|
12951
|
+
function short(files, n = 3) {
|
|
12952
|
+
return files.slice(0, n).join(", ") + (files.length > n ? ` \u0438 \u0435\u0449\u0451 ${files.length - n}` : "");
|
|
12953
|
+
}
|
|
12954
|
+
var SPINE_PREDICTION_WEIGHT = 0.7;
|
|
12955
|
+
function decideConflict({ model, candidate, active }) {
|
|
12956
|
+
if ((candidate.kind ?? "drone") !== "drone") return { kind: "clear" };
|
|
12957
|
+
if (!hasZoneModel(model)) return { kind: "unknown" };
|
|
12958
|
+
const predicted = predictForRun(model, candidate);
|
|
12959
|
+
if (!predicted.length) return { kind: "unknown" };
|
|
12960
|
+
if (!active.length) return { kind: "clear" };
|
|
12961
|
+
const spine = new Set(model.spine);
|
|
12962
|
+
const candidateSpine = predicted.filter((p) => spine.has(p.path) && p.weight >= SPINE_PREDICTION_WEIGHT).map((p) => p.path);
|
|
12963
|
+
if (candidateSpine.length) {
|
|
12964
|
+
const blocker = active[0];
|
|
12965
|
+
return {
|
|
12966
|
+
kind: "wait",
|
|
12967
|
+
blockedBy: blocker.run,
|
|
12968
|
+
message: `\u0437\u0430\u0434\u0430\u0447\u0430 \u0442\u0440\u043E\u0433\u0430\u0435\u0442 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${short(candidateSpine)}) \u2014 \u0442\u0430\u043A\u0438\u0435 \u0438\u0434\u0443\u0442 \u043F\u043E\u0441\u043B\u0435\u0434\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E \u043E\u0442 \u0437\u043E\u043D\u044B; \u0436\u0434\u0443 ${blocker.run.task_slug} (#${blocker.run.id})`
|
|
12969
|
+
};
|
|
12970
|
+
}
|
|
12971
|
+
for (const w of active) {
|
|
12972
|
+
if (!w.actual) continue;
|
|
12973
|
+
const theirSpine = w.files.filter((f) => spine.has(f));
|
|
12974
|
+
if (theirSpine.length && zoneForFiles(model, w.files)?.zone === SPINE_ZONE) {
|
|
12975
|
+
return {
|
|
12976
|
+
kind: "wait",
|
|
12977
|
+
blockedBy: w.run,
|
|
12978
|
+
message: `${w.run.task_slug} (#${w.run.id}) \u043F\u0440\u044F\u043C\u043E \u0441\u0435\u0439\u0447\u0430\u0441 \u043F\u0440\u0430\u0432\u0438\u0442 \u043E\u0431\u0449\u0438\u0439 \u0445\u0440\u0435\u0431\u0435\u0442 (${short(theirSpine)}) \u2014 \u043E\u0442\u0432\u0435\u0442\u0432\u043B\u044F\u0442\u044C\u0441\u044F \u043E\u0442 \u043D\u0435\u0433\u043E \u0440\u0430\u043D\u043E, \u0436\u0434\u0443`
|
|
12979
|
+
};
|
|
12980
|
+
}
|
|
12981
|
+
}
|
|
12982
|
+
for (const w of active) {
|
|
12983
|
+
const risk = overlapRisk(predicted, w.files);
|
|
12984
|
+
if (!risk.overlap) continue;
|
|
12985
|
+
const what = risk.sharedFiles.length ? `\u043E\u0434\u043D\u0438 \u0444\u0430\u0439\u043B\u044B (${short(risk.sharedFiles)})` : `\u043E\u0434\u0438\u043D \u043A\u0430\u0442\u0430\u043B\u043E\u0433 (${short(risk.sharedDirs)})`;
|
|
12986
|
+
return {
|
|
12987
|
+
kind: "wait",
|
|
12988
|
+
blockedBy: w.run,
|
|
12989
|
+
message: `\u044D\u0442\u0438 \u0434\u0432\u0435 \u0437\u0430\u0434\u0430\u0447\u0438 \u0442\u0440\u043E\u0433\u0430\u044E\u0442 ${what} \u2014 \u0441\u0442\u0430\u0432\u0438\u043C \u043F\u043E\u0441\u043B\u0435\u0434\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u043D\u043E: \u0436\u0434\u0443 ${w.run.task_slug} (#${w.run.id})` + (w.actual ? "" : " (\u043F\u043E \u043F\u0440\u0435\u0434\u0441\u043A\u0430\u0437\u0430\u043D\u0438\u044E \u2014 \u0434\u0440\u043E\u043D \u0435\u0449\u0451 \u043D\u0435 \u043D\u0430\u0447\u0430\u043B \u043F\u0438\u0441\u0430\u0442\u044C \u043A\u043E\u0434)")
|
|
12990
|
+
};
|
|
12991
|
+
}
|
|
12992
|
+
return { kind: "clear" };
|
|
12993
|
+
}
|
|
12994
|
+
function suggestAreaForRun(model, run) {
|
|
12995
|
+
if (!hasZoneModel(model)) return null;
|
|
12996
|
+
const text = runText(run);
|
|
12997
|
+
if (!text) return null;
|
|
12998
|
+
const top = suggestZonesFromText(model, text)[0];
|
|
12999
|
+
return top && top.confidence >= 0.3 ? { zone: top.zone, confidence: top.confidence, why: top.why } : null;
|
|
13000
|
+
}
|
|
13001
|
+
var DEFAULT_TTL_MS = 10 * 6e4;
|
|
13002
|
+
var ZoneModels = class {
|
|
13003
|
+
constructor(opts = {}) {
|
|
13004
|
+
this.opts = opts;
|
|
13005
|
+
}
|
|
13006
|
+
opts;
|
|
13007
|
+
cache = /* @__PURE__ */ new Map();
|
|
13008
|
+
/** Модель репо. Истории нет / это не git — EMPTY_ZONE_MODEL (раздача на старом локе). */
|
|
13009
|
+
async forRepo(repo, now = Date.now()) {
|
|
13010
|
+
if (!repo) return EMPTY_ZONE_MODEL;
|
|
13011
|
+
const head = await headSha(repo);
|
|
13012
|
+
const hit = this.cache.get(repo);
|
|
13013
|
+
if (hit && hit.head === head && now - hit.builtAt < (this.opts.ttlMs ?? DEFAULT_TTL_MS)) return hit.model;
|
|
13014
|
+
const model = await this.build(repo);
|
|
13015
|
+
this.cache.set(repo, { head, builtAt: now, model });
|
|
13016
|
+
return model;
|
|
13017
|
+
}
|
|
13018
|
+
/** Сбросить кэш (тесты, `swarm zones --refresh`). */
|
|
13019
|
+
clear() {
|
|
13020
|
+
this.cache.clear();
|
|
13021
|
+
}
|
|
13022
|
+
async build(repo) {
|
|
13023
|
+
const history = await readTaskHistory(repo, { ...this.opts.maxCommits ? { maxCommits: this.opts.maxCommits } : {} });
|
|
13024
|
+
return buildZoneModel(withManualAreas(history, this.opts.manualAreas?.() ?? /* @__PURE__ */ new Map()));
|
|
13025
|
+
}
|
|
13026
|
+
};
|
|
13027
|
+
var ActiveFilesCache = class {
|
|
13028
|
+
constructor(read, ttlMs = 3e4) {
|
|
13029
|
+
this.read = read;
|
|
13030
|
+
this.ttlMs = ttlMs;
|
|
13031
|
+
}
|
|
13032
|
+
read;
|
|
13033
|
+
ttlMs;
|
|
13034
|
+
cache = /* @__PURE__ */ new Map();
|
|
13035
|
+
async filesOf(run, now = Date.now()) {
|
|
13036
|
+
const hit = this.cache.get(run.id);
|
|
13037
|
+
if (hit && now - hit.at < this.ttlMs) return hit.files;
|
|
13038
|
+
const files = await this.read(run);
|
|
13039
|
+
this.cache.set(run.id, { at: now, files });
|
|
13040
|
+
return files;
|
|
13041
|
+
}
|
|
13042
|
+
/** Забыть всех, кого нет среди живых (дрон закончил — его файлы больше не заняты). */
|
|
13043
|
+
prune(liveIds) {
|
|
13044
|
+
const live = new Set(liveIds);
|
|
13045
|
+
for (const id of [...this.cache.keys()]) if (!live.has(id)) this.cache.delete(id);
|
|
13046
|
+
}
|
|
13047
|
+
};
|
|
13048
|
+
function withManualAreas(history, areas) {
|
|
13049
|
+
return history.map((e) => ({ ...e, area: normalizeZoneName(areas.get(e.slug) ?? null) }));
|
|
13050
|
+
}
|
|
13051
|
+
|
|
13052
|
+
// src/overmind.ts
|
|
13053
|
+
var ANOMALY_RESOLVE_BATCH = 50;
|
|
11504
13054
|
var THIS_FILE = fileURLToPath2(import.meta.url);
|
|
13055
|
+
var executorHasSession = (p) => EXECUTOR.session.exists(p);
|
|
11505
13056
|
var OVERMIND_ENTRY = THIS_FILE;
|
|
11506
13057
|
var STALL_MS = Number(process.env.SWARM_STALL_MS) || 10 * 60 * 1e3;
|
|
11507
13058
|
var STALL_FLOOR_MS = Number(process.env.SWARM_STALL_FLOOR_MS) || 30 * 60 * 1e3;
|
|
@@ -11515,6 +13066,10 @@ var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
|
|
|
11515
13066
|
var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
|
|
11516
13067
|
var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
|
|
11517
13068
|
var REVIEW_ESCALATION_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F";
|
|
13069
|
+
var REVIEW_UNRECOVERABLE_MARK = "\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u044C";
|
|
13070
|
+
function reviewUnrecoverableMark(branch) {
|
|
13071
|
+
return `${REVIEW_UNRECOVERABLE_MARK} (\u0432\u0435\u0442\u043A\u0430 ${branch})`;
|
|
13072
|
+
}
|
|
11518
13073
|
var REVIEW_CYCLE_LIMIT = Number(process.env.SWARM_REVIEW_CYCLE_LIMIT) || 3;
|
|
11519
13074
|
var REVIEW_CYCLE_ESCALATION_MARK = "\u0440\u0435-\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0446\u0438\u043A\u043B\u0438\u043B\u043E\u0441\u044C";
|
|
11520
13075
|
var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
|
|
@@ -11541,6 +13096,7 @@ var CHANGES_WAKE_ESCALATION_MARK = "\u0444\u0438\u043A\u0441-\u0438\u0442\u0435\
|
|
|
11541
13096
|
var NEEDS_HUMAN_MARKS = [
|
|
11542
13097
|
RESOLVE_ESCALATION_MARK,
|
|
11543
13098
|
REVIEW_ESCALATION_MARK,
|
|
13099
|
+
REVIEW_UNRECOVERABLE_MARK,
|
|
11544
13100
|
REVIEW_CYCLE_ESCALATION_MARK,
|
|
11545
13101
|
CI_WAKE_ESCALATION_MARK,
|
|
11546
13102
|
CHANGES_WAKE_ESCALATION_MARK
|
|
@@ -11869,8 +13425,12 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
11869
13425
|
// авторские подписи внутри него уже приведены к одной строке (renderAttachmentsBlock).
|
|
11870
13426
|
...attachmentsBlock ? ["", attachmentsBlock] : [],
|
|
11871
13427
|
"",
|
|
11872
|
-
|
|
11873
|
-
|
|
13428
|
+
// TASWM-356: ветка worktree — НЕ обязательно слаг. У продолжения чужого PR она своя
|
|
13429
|
+
// (её завёл прошлый дрон), и `git push -u origin <slug>` оттуда либо откажет, либо заведёт
|
|
13430
|
+
// вторую ветку мимо PR. Пишем ту, что реально записана на прогоне; пусто (первый спавн —
|
|
13431
|
+
// ветку заводит spawnDrone из слага) — прежняя формулировка.
|
|
13432
|
+
`\u0422\u044B \u2014 \u0434\u0440\u043E\u043D \u0440\u043E\u044F. \u0420\u0430\u0431\u043E\u0442\u0430\u0435\u0448\u044C \u0432 \u044D\u0442\u043E\u043C git-worktree, \u0432\u0435\u0442\u043A\u0430 \`${run.branch || run.task_slug}\`${run.branch && run.branch !== run.task_slug ? " (\u043E\u043D\u0430 \u041D\u0415 \u0440\u0430\u0432\u043D\u0430 \u0441\u043B\u0430\u0433\u0443 \u0437\u0430\u0434\u0430\u0447\u0438 \u2014 \u044D\u0442\u043E \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u0435 \u0443\u0436\u0435 \u043E\u0442\u043A\u0440\u044B\u0442\u043E\u0433\u043E PR, \u043F\u0443\u0448 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u043D\u0435\u0451)" : " \u0441\u043E\u0437\u0434\u0430\u043D\u0430 \u043F\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438"}.`,
|
|
13433
|
+
"\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B overmind (MCP): report_step(text) \u2014 \u043B\u043E\u0433\u0438\u0440\u0443\u0439 \u0445\u043E\u0434 \u0440\u0430\u0431\u043E\u0442\u044B; read_task() \u2014 \u0441\u0432\u0435\u0436\u0438\u0439 \u0441\u043D\u0430\u043F\u0448\u043E\u0442 \u0437\u0430\u0434\u0430\u0447\u0438 (\u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435+\u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0438 \u0433\u043E\u0442\u043E\u0432\u043D\u043E\u0441\u0442\u0438+\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u044B+\u0434\u043E\u043A\u0438+\u0438\u0441\u0445\u043E\u0434\u044B \u0441\u043E\u0441\u0435\u0434\u043D\u0438\u0445 \u0437\u0430\u0434\u0430\u0447 \u043F\u043E \u0437\u043E\u043D\u0435 \u043A\u043E\u0434\u0430) \u0431\u0435\u0437 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432, \u0435\u0441\u043B\u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043C\u043E\u0433\u043B\u0438 \u0434\u043E\u043F\u0438\u0441\u0430\u0442\u044C \u043F\u043E\u0441\u043B\u0435 \u0441\u0442\u0430\u0440\u0442\u0430; ask(question, options?) \u2014 \u0435\u0441\u043B\u0438 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0420\u0415\u0428\u0415\u041D\u0418\u0415, \u0441\u043F\u0440\u043E\u0441\u0438 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (\u0434\u0430\u0439 2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430 \u0432 options, \u0435\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F) \u0438 \u0434\u043E\u0436\u0434\u0438\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0430 (\u041D\u0415 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439); done(pr_url) \u2014 \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043A\u0440\u043E\u0435\u0448\u044C PR; note(text) \u2014 \u0437\u0430\u043C\u0435\u0442\u043A\u0430 \xAB\u0437\u0430\u043C\u0435\u0447\u0435\u043D\u043E \u043F\u043E \u043F\u0443\u0442\u0438\xBB \u0411\u0415\u0417 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0438: \u043D\u0430\u0442\u043A\u043D\u0443\u043B\u0441\u044F \u043D\u0430 \u043F\u0440\u043E\u0431\u0435\u043B \u0432 \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435, \u043F\u0440\u043E\u0442\u0443\u0445\u0448\u0443\u044E \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u044E, \u0431\u0430\u0433 \u0440\u044F\u0434\u043E\u043C \u0441\u043E \u0441\u0432\u043E\u0438\u043C \u0434\u0438\u0444\u0444\u043E\u043C \u2014 \u0441\u043A\u0430\u0436\u0438 \u0438 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u0434\u0430\u043B\u044C\u0448\u0435, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0443\u0432\u0438\u0434\u0438\u0442 \u0438 \u0441\u0430\u043C \u0440\u0435\u0448\u0438\u0442, \u0437\u0430\u0432\u043E\u0434\u0438\u0442\u044C \u043B\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043D\u0435 \u043F\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0439 \u0442\u0430\u043A\u043E\u0435 \u0432 \u0434\u043E\u0440\u043E\u0433\u043E\u0439 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 ask \u0438 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u0438 \u0432 \u043E\u0442\u0447\u0451\u0442\u0435 \u043E \u0441\u0434\u0430\u0447\u0435).",
|
|
11874
13434
|
"\u041F\u0440\u0430\u0432\u0438\u043B\u0430: \u0442\u0440\u043E\u0433\u0430\u0435\u0448\u044C UI \u2014 \u0421\u041D\u0410\u0427\u0410\u041B\u0410 \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0438, \u0435\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0441\u043A\u0438\u043B\u043B \u0441 UI-\u043F\u0440\u0430\u0432\u0438\u043B\u0430\u043C\u0438 \u043F\u0440\u043E\u0435\u043A\u0442\u0430, \u0438 \u0441\u043B\u0435\u0434\u0443\u0439 \u0435\u0433\u043E \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435 (\u043D\u0435\u0442 \u043D\u0443\u0436\u043D\u043E\u0433\u043E \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0430 \u2014 \u0437\u0430\u0432\u0435\u0434\u0438 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u0443\u044E \u0437\u0430\u0434\u0430\u0447\u0443, \u043D\u0435 \u0445\u0430\u0440\u0434\u043A\u043E\u0434\u044C \u043E\u0434\u043D\u043E\u0440\u0430\u0437\u043E\u0432\u043E\u0435); \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0441\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438 \u0432 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0438; \u043F\u043E\u0441\u043B\u0435 UI-\u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u0441\u043A\u0440\u0438\u043D\u044B mobile+desktop \u041A \u0417\u0410\u0414\u0410\u0427\u0415 (\u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 screenshots \u0443 done); \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0447\u0435\u0440\u0435\u0437 gh \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url).",
|
|
11875
13435
|
// TASWM-293: живой случай SCE-5 — ревьюер заблокировал PR за отсутствие CI (у проекта
|
|
11876
13436
|
// его нет и не предполагается), автора разбудили «закрывать замечания», замечаний по коду
|
|
@@ -11887,7 +13447,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
11887
13447
|
`\u0412\u0435\u0434\u0438 \u0434\u043D\u0435\u0432\u043D\u0438\u043A \u0437\u0430\u0434\u0430\u0447\u0438: \u0437\u0430\u0432\u0435\u0434\u0438 \`journal/${run.task_slug}.md\` \u0438 \u043F\u043E \u0445\u043E\u0434\u0443 \u0440\u0430\u0431\u043E\u0442\u044B (\u043D\u0435 \u043E\u0434\u043D\u0438\u043C \u043C\u0430\u0445\u043E\u043C \u0432 \u043A\u043E\u043D\u0446\u0435) \u0434\u043E\u043F\u0438\u0441\u044B\u0432\u0430\u0439 \u0442\u0443\u0434\u0430 \u0448\u0430\u0433\u0438 \u0438 \u043F\u0440\u0438\u043D\u044F\u0442\u044B\u0435 \u0440\u0435\u0448\u0435\u043D\u0438\u044F \u2014 \u044D\u0442\u043E \u0445\u0440\u043E\u043D\u0438\u043A\u0430 \u0434\u043B\u044F \u0440\u0435\u0442\u0440\u043E\u0441\u043F\u0435\u043A\u0442\u0438\u0432\u044B, \u0441\u0432\u043E\u0439 \u0444\u0430\u0439\u043B \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0434\u0440\u0430\u0442\u044C\u0441\u044F \u0437\u0430 \u043E\u0431\u0449\u0438\u0439 \u0441 \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u044B\u043C\u0438 \u0434\u0440\u043E\u043D\u0430\u043C\u0438. \u041A\u043E\u043C\u043C\u0438\u0442\u044C \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u043C\u0438 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F\u043C\u0438 PR, \u043E\u0431\u044B\u0447\u043D\u044B\u043C \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u043C. \u041D\u0435 \u0434\u0443\u0431\u043B\u0438\u0440\u0443\u0439 \u0441 summary \u0432 done(): \u0434\u043D\u0435\u0432\u043D\u0438\u043A \u2014 \u0441\u044B\u0440\u0430\u044F \u0445\u0440\u043E\u043D\u0438\u043A\u0430 (\u0447\u0442\u043E \u043F\u0440\u043E\u0431\u043E\u0432\u0430\u043B, \u0447\u0442\u043E \u043E\u0442\u0431\u0440\u043E\u0441\u0438\u043B \u0438 \u043F\u043E\u0447\u0435\u043C\u0443), summary \u2014 \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u0439 \u0438\u0442\u043E\u0433 \u0434\u043B\u044F \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430; \u0435\u0441\u043B\u0438 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0443\u0436\u0435 \u0440\u0430\u0441\u043F\u0438\u0441\u0430\u043D\u043E \u0432 \u0434\u043D\u0435\u0432\u043D\u0438\u043A\u0435, \u0432 summary \u043F\u0440\u043E\u0441\u0442\u043E \u0441\u043E\u0448\u043B\u0438\u0441\u044C \u043D\u0430 \u043D\u0435\u0433\u043E.`,
|
|
11888
13448
|
"",
|
|
11889
13449
|
"\u0421\u041E\u0425\u0420\u0410\u041D\u042F\u0419 \u0420\u0410\u0411\u041E\u0422\u0423 \u041F\u041E \u0425\u041E\u0414\u0423 \u2014 \u0438\u043D\u0430\u0447\u0435 \u043E\u043D\u0430 \u043F\u0440\u043E\u043F\u0430\u0434\u0451\u0442, \u0435\u0441\u043B\u0438 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0443\u0431\u044C\u044E\u0442:",
|
|
11890
|
-
`\u2022 \u041F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0437\u043D\u0430\u0447\u0438\u043C\u043E\u0433\u043E \u0448\u0430\u0433\u0430 \u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0438 \u0441\u0440\u0430\u0437\u0443 \u043F\u0443\u0448\u044C: git push -u origin ${run.
|
|
13450
|
+
`\u2022 \u041F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0437\u043D\u0430\u0447\u0438\u043C\u043E\u0433\u043E \u0448\u0430\u0433\u0430 \u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0438 \u0441\u0440\u0430\u0437\u0443 \u043F\u0443\u0448\u044C: git push -u origin ${run.branch || run.task_slug}. \u041D\u0435\u0437\u0430\u043F\u0443\u0448\u0435\u043D\u043D\u043E\u0435 \u0443\u043C\u0438\u0440\u0430\u0435\u0442 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0434\u0440\u043E\u043D\u043E\u043C \u2014 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u044B \u0438 \u043F\u0440\u0430\u0432\u043A\u0438 \u0432 worktree \u043D\u0435 \u043F\u0435\u0440\u0435\u0436\u0438\u0432\u0443\u0442 \u0441\u043C\u0435\u0440\u0442\u044C \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430.`,
|
|
11891
13451
|
"\u2022 \u041D\u0435 \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439 \u0434\u043E\u043B\u0433\u0438\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0432 \u0444\u043E\u043D\u0435 \xAB\u0447\u0442\u043E\u0431\u044B \u043F\u043E\u0434\u043E\u0436\u0434\u0430\u0442\u044C\xBB: \u0441\u0435\u0441\u0441\u0438\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u0442\u0441\u044F, \u043A\u0430\u043A \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u0451\u0448\u044C \u0437\u0432\u0430\u0442\u044C \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B, \u0438 \u0444\u043E\u043D\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043D\u0435 \u0441\u043F\u0430\u0441\u0451\u0442. \u0414\u043E\u043B\u0433\u043E\u0435 \u0434\u0435\u0440\u0436\u0438 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u043C \u0432\u044B\u0437\u043E\u0432\u043E\u043C; \u0434\u043B\u0438\u043D\u043D\u0443\u044E \u0441\u0435\u0440\u0438\u044E \u0434\u0440\u043E\u0431\u0438 \u043D\u0430 \u0447\u0430\u0441\u0442\u0438 \u0438 \u043A\u043E\u043C\u043C\u0438\u0442\u044C+\u043F\u0443\u0448\u044C \u043C\u0435\u0436\u0434\u0443 \u043D\u0438\u043C\u0438.",
|
|
11892
13452
|
'\u2022 \u041F\u0435\u0440\u0435\u0434 \u0434\u043E\u043B\u0433\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 (\u0442\u0435\u0441\u0442\u044B, \u0441\u0431\u043E\u0440\u043A\u0430 \u2014 \u043C\u0438\u043D\u0443\u0442\u044B \u0431\u0435\u0437 \u0432\u044B\u0432\u043E\u0434\u0430) \u043A\u0438\u043D\u044C report_step("\u0433\u043E\u043D\u044E \u0442\u0435\u0441\u0442\u044B\u2026") \u2014 \u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E (\u0440\u043E\u0439 \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E \u043E\u0442\u0441\u043B\u0435\u0436\u0438\u0432\u0430\u0435\u0442 \u0436\u0438\u0432\u043E\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u043A\u043E\u043C\u0430\u043D\u0434\u044B), \u043D\u043E \u043F\u043E\u043B\u0435\u0437\u043D\u0435\u0435 \u0434\u043B\u044F \u0432\u0438\u0434\u0438\u043C\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441\u0430 \u0432 \u0434\u0430\u0448\u0431\u043E\u0440\u0434\u0435 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430.',
|
|
11893
13453
|
"",
|
|
@@ -11912,10 +13472,10 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
11912
13472
|
}
|
|
11913
13473
|
return parts.join("\n");
|
|
11914
13474
|
}
|
|
11915
|
-
var HERO_CHARTER_PATH =
|
|
13475
|
+
var HERO_CHARTER_PATH = join28(dirname5(THIS_FILE), "..", "docs", "HERO.md");
|
|
11916
13476
|
function buildHeroPrompt(task, run, taskContext, attachmentsBlock) {
|
|
11917
13477
|
const name = run.name || `#${run.id}`;
|
|
11918
|
-
const charter =
|
|
13478
|
+
const charter = readFileSync20(HERO_CHARTER_PATH, "utf8");
|
|
11919
13479
|
const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
|
|
11920
13480
|
|
|
11921
13481
|
${task.description}` : `\u042D\u041F\u0418\u041A ${run.task_slug} (\u0442\u0435\u043B\u043E \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E slug \u0438 \u0437\u0434\u0440\u0430\u0432\u043E\u043C\u0443 \u0441\u043C\u044B\u0441\u043B\u0443).`;
|
|
@@ -12166,8 +13726,8 @@ async function wakeRun(deps, runId) {
|
|
|
12166
13726
|
if (!run) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0437\u0435\u0440\u043A\u0430\u043B\u0435 \u0440\u043E\u044F` };
|
|
12167
13727
|
if (run.pid != null && deps.isAlive(run.pid)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0443\u0436\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 (pid ${run.pid}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
|
|
12168
13728
|
if (isTerminalStatus(run.status)) return { ok: false, why: `\u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435\u043D (${run.status}) \u2014 \u0431\u0443\u0434\u0438\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E` };
|
|
12169
|
-
const worktreeExists = deps.worktreeExists ??
|
|
12170
|
-
const hasTranscript = deps.hasTranscript ??
|
|
13729
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
13730
|
+
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
12171
13731
|
const wt = run.worktree_path;
|
|
12172
13732
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
12173
13733
|
const isService = run.reviews_run_id != null || run.resolves_run_id != null;
|
|
@@ -12251,9 +13811,9 @@ function repoHasCi(deps, run) {
|
|
|
12251
13811
|
function repoHasCiWorkflows(worktreePath, repo) {
|
|
12252
13812
|
for (const base3 of [worktreePath, repo]) {
|
|
12253
13813
|
if (!base3) continue;
|
|
12254
|
-
const dir =
|
|
13814
|
+
const dir = join28(base3, ".github", "workflows");
|
|
12255
13815
|
try {
|
|
12256
|
-
if (
|
|
13816
|
+
if (readdirSync12(dir).some((f) => f.endsWith(".yml") || f.endsWith(".yaml"))) return true;
|
|
12257
13817
|
} catch {
|
|
12258
13818
|
}
|
|
12259
13819
|
}
|
|
@@ -12400,8 +13960,8 @@ async function wakeReviewerOnCiReady(deps, run, over) {
|
|
|
12400
13960
|
return true;
|
|
12401
13961
|
}
|
|
12402
13962
|
function worktreeAndTranscriptAlive(deps, worktree) {
|
|
12403
|
-
const exists = deps.worktreeExists ??
|
|
12404
|
-
const transcript = deps.hasTranscript ??
|
|
13963
|
+
const exists = deps.worktreeExists ?? existsSync16;
|
|
13964
|
+
const transcript = deps.hasTranscript ?? executorHasSession;
|
|
12405
13965
|
return exists(worktree) && transcript(worktree);
|
|
12406
13966
|
}
|
|
12407
13967
|
async function spawnFreshReviewerAfterStall(deps, run, why) {
|
|
@@ -12504,6 +14064,22 @@ async function envGateAllows(deps, run) {
|
|
|
12504
14064
|
function effectiveRepo(deps, run) {
|
|
12505
14065
|
return normalizeRepo(run.repo || deps.repoFallback || "");
|
|
12506
14066
|
}
|
|
14067
|
+
async function zoneConflictFor(db, models, activeFiles, run, repo, repoFallback) {
|
|
14068
|
+
const model = await models.forRepo(repo);
|
|
14069
|
+
if (!hasZoneModel(model)) return { kind: "unknown" };
|
|
14070
|
+
const live = db.listRuns().filter((r) => (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status));
|
|
14071
|
+
activeFiles.prune(live.map((r) => r.id));
|
|
14072
|
+
const active = [];
|
|
14073
|
+
for (const r of live) {
|
|
14074
|
+
if (r.id === run.id) continue;
|
|
14075
|
+
if (normalizeRepo(r.repo || repoFallback || "") !== repo) continue;
|
|
14076
|
+
const files = await activeFiles.filesOf(r);
|
|
14077
|
+
active.push(
|
|
14078
|
+
files.length ? { run: r, files, actual: true } : { run: r, files: predictForRun(model, r).map((p) => p.path), actual: false }
|
|
14079
|
+
);
|
|
14080
|
+
}
|
|
14081
|
+
return decideConflict({ model, candidate: run, active });
|
|
14082
|
+
}
|
|
12507
14083
|
function activeDronesOnRepo(deps, repo, exceptRunId) {
|
|
12508
14084
|
return deps.db.listRuns().filter(
|
|
12509
14085
|
(r) => r.id !== exceptRunId && (r.kind ?? "drone") === "drone" && ACTIVE_DRONE_STATUSES.has(r.status) && effectiveRepo(deps, r) === repo
|
|
@@ -12543,6 +14119,15 @@ async function exclusiveGateAllows(deps, run) {
|
|
|
12543
14119
|
}
|
|
12544
14120
|
return true;
|
|
12545
14121
|
}
|
|
14122
|
+
function pruneGateLogs(deps) {
|
|
14123
|
+
if (!deps.envGateLogged?.size && !deps.exclusiveLogged?.size && !deps.overlapLogged?.size) return;
|
|
14124
|
+
const open = /* @__PURE__ */ new Set();
|
|
14125
|
+
for (const run of deps.db.listRuns()) if (!isTerminalStatus(run.status)) open.add(run.id);
|
|
14126
|
+
for (const map of [deps.envGateLogged, deps.exclusiveLogged, deps.overlapLogged]) {
|
|
14127
|
+
if (!map) continue;
|
|
14128
|
+
for (const id of [...map.keys()]) if (!open.has(id)) map.delete(id);
|
|
14129
|
+
}
|
|
14130
|
+
}
|
|
12546
14131
|
async function exclusivityBlockedRunIds(deps) {
|
|
12547
14132
|
const blocked = /* @__PURE__ */ new Set();
|
|
12548
14133
|
if (!deps.exclusiveProject) return blocked;
|
|
@@ -12559,6 +14144,18 @@ async function exclusivityBlockedRunIds(deps) {
|
|
|
12559
14144
|
}
|
|
12560
14145
|
return blocked;
|
|
12561
14146
|
}
|
|
14147
|
+
async function fillSuggestedArea(deps, run) {
|
|
14148
|
+
if (run.area || !deps.suggestArea) return;
|
|
14149
|
+
const guess = await deps.suggestArea(run).catch(() => null);
|
|
14150
|
+
if (!guess) return;
|
|
14151
|
+
deps.db.updateRun(run.id, { area: guess.zone });
|
|
14152
|
+
run.area = guess.zone;
|
|
14153
|
+
deps.db.addEvent(
|
|
14154
|
+
run.id,
|
|
14155
|
+
"step",
|
|
14156
|
+
`\u0437\u043E\u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0438 \u0438\u0437 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 \u0440\u0435\u043F\u043E: ${guess.zone} (${Math.round(guess.confidence * 100)}% \u2014 ${guess.why}) \u2014 TASWM-348`
|
|
14157
|
+
);
|
|
14158
|
+
}
|
|
12562
14159
|
async function trySpawnQueuedRun(deps, run) {
|
|
12563
14160
|
if (deps.control?.paused) return "skipped";
|
|
12564
14161
|
const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
|
|
@@ -12574,11 +14171,21 @@ async function trySpawnQueuedRun(deps, run) {
|
|
|
12574
14171
|
if (!await exclusiveGateAllows(deps, run)) return "skipped";
|
|
12575
14172
|
if (!await prodStillAllows(deps, run, ["queued"])) return "skipped";
|
|
12576
14173
|
if (deps.db.takeRun && !await deps.db.takeRun(run.id)) return "skipped";
|
|
14174
|
+
await fillSuggestedArea(deps, run);
|
|
12577
14175
|
try {
|
|
12578
14176
|
await deps.spawn(run);
|
|
12579
14177
|
narrate(deps, `\u0411\u0435\u0440\u0443 ${run.task_slug} \u0438\u0437 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0441\u043F\u0430\u0432\u043D\u044E ${droneLabel(run)}.`);
|
|
12580
14178
|
return "spawned";
|
|
12581
14179
|
} catch (e) {
|
|
14180
|
+
if (e instanceof FileOverlapError) {
|
|
14181
|
+
const logged = deps.overlapLogged ??= /* @__PURE__ */ new Map();
|
|
14182
|
+
if (logged.get(run.id) !== e.message) {
|
|
14183
|
+
logged.set(run.id, e.message);
|
|
14184
|
+
deps.db.addEvent(run.id, "step", `${e.message} \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 \u043E\u0441\u0442\u0430\u0451\u0442\u0441\u044F \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 (TASWM-348)`);
|
|
14185
|
+
narrate(deps, `${droneLabel(run)} \u2014 ${e.message}. \u041D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E \u043F\u0430\u0440\u0430\u043B\u043B\u0435\u043B\u044C\u043D\u043E.`);
|
|
14186
|
+
}
|
|
14187
|
+
return "skipped";
|
|
14188
|
+
}
|
|
12582
14189
|
if (e instanceof AreaBusyError) return "skipped";
|
|
12583
14190
|
deps.db.updateRun(run.id, { status: "failed" });
|
|
12584
14191
|
deps.db.addEvent(run.id, "failed", String(e));
|
|
@@ -12605,8 +14212,8 @@ async function resumeWatchdogParked(deps, droneCap) {
|
|
|
12605
14212
|
}
|
|
12606
14213
|
if (!candidate) return;
|
|
12607
14214
|
if (candidate.pid != null && deps.isAlive(candidate.pid)) return;
|
|
12608
|
-
const worktreeExists = deps.worktreeExists ??
|
|
12609
|
-
const hasTranscript = deps.hasTranscript ??
|
|
14215
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
14216
|
+
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
12610
14217
|
const wt = candidate.worktree_path;
|
|
12611
14218
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
12612
14219
|
const mode = canContinue ? "continue" : "fresh";
|
|
@@ -12774,8 +14381,8 @@ async function manageRelief(deps, now, droneCap, stormActive) {
|
|
|
12774
14381
|
if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
|
|
12775
14382
|
if (!deps.respawn) return;
|
|
12776
14383
|
const wt = resumeCandidate.worktree_path;
|
|
12777
|
-
const worktreeExists = deps.worktreeExists ??
|
|
12778
|
-
const hasTranscript = deps.hasTranscript ??
|
|
14384
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
14385
|
+
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
12779
14386
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
12780
14387
|
const mode = canContinue ? "continue" : "fresh";
|
|
12781
14388
|
if (mode === "continue" && wt && deps.reclaimWorktree) {
|
|
@@ -12898,15 +14505,28 @@ ${journal}`);
|
|
|
12898
14505
|
deps.db.addEvent(run.id, "step", `\u0434\u0435\u043C\u043E\u043D-\u043E\u0442\u0447\u0451\u0442 \u0432 \u0437\u0430\u0434\u0430\u0447\u0443 ${run.task_slug} \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D: ${e instanceof Error ? e.message : String(e)}`);
|
|
12899
14506
|
});
|
|
12900
14507
|
}
|
|
14508
|
+
function syncBranchFromPr(db, run, headBranch) {
|
|
14509
|
+
const fix = branchFromPrPatch(run.branch, headBranch);
|
|
14510
|
+
if (!fix) return run;
|
|
14511
|
+
const updated = db.updateRun(run.id, fix.patch) ?? run;
|
|
14512
|
+
db.addEvent(run.id, "step", fix.event);
|
|
14513
|
+
return updated;
|
|
14514
|
+
}
|
|
14515
|
+
async function worktreeBranchFor(run, prHead) {
|
|
14516
|
+
if (run.branch) return run.branch;
|
|
14517
|
+
const fromPr = run.pr_url && prHead ? await prHead(run.pr_url).catch(() => null) : null;
|
|
14518
|
+
return fromPr || run.task_slug;
|
|
14519
|
+
}
|
|
12901
14520
|
async function adoptDroneToReview(deps, run, prUrl) {
|
|
12902
14521
|
const t = applyReviewTransition(deps.db, run, prUrl);
|
|
14522
|
+
const adopted = syncBranchFromPr(deps.db, run, deps.prHeadBranch ? await deps.prHeadBranch(prUrl).catch(() => null) : null);
|
|
12903
14523
|
deps.db.addEvent(run.id, "pr_opened", prUrl);
|
|
12904
14524
|
deps.db.addEvent(
|
|
12905
14525
|
run.id,
|
|
12906
14526
|
"step",
|
|
12907
14527
|
t.isFixIteration ? `\u0434\u0440\u043E\u043D \u043E\u0442\u043A\u0440\u044B\u043B PR \u043D\u0430 \u0444\u0438\u043A\u0441-\u0438\u0442\u0435\u0440\u0430\u0446\u0438\u0438 \u0438 \u0443\u043C\u0435\u0440 \u0434\u043E done() \u2014 \u0443\u0441\u044B\u043D\u043E\u0432\u043B\u0451\u043D \u043D\u0430 \u0440\u0435-\u0440\u0435\u0432\u044C\u044E #${t.cycle} (\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \xAB\u043D\u0443\u0436\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438\xBB \u0441\u0431\u0440\u043E\u0448\u0435\u043D, pr_url \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0438\u0437 gh): ${prUrl}` : `\u0434\u0440\u043E\u043D \u043E\u0442\u043A\u0440\u044B\u043B PR \u043F\u0435\u0440\u0435\u0434 \u0441\u043C\u0435\u0440\u0442\u044C\u044E \u2014 \u0443\u0441\u044B\u043D\u043E\u0432\u043B\u0451\u043D \u043D\u0430 \u0440\u0435\u0432\u044C\u044E (pr_url \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0438\u0437 gh): ${prUrl}`
|
|
12908
14528
|
);
|
|
12909
|
-
await postAdoptionReport(deps,
|
|
14529
|
+
await postAdoptionReport(deps, adopted, prUrl, t.cycle ?? run.review_cycle ?? 0);
|
|
12910
14530
|
}
|
|
12911
14531
|
async function postScrapedVerdictReport(deps, runId, taskSlug, verdict, droneName, workspaceId, cycle) {
|
|
12912
14532
|
if (!deps.reportComment) return;
|
|
@@ -12936,8 +14556,8 @@ function buildEscalationCandidates(db) {
|
|
|
12936
14556
|
}
|
|
12937
14557
|
async function tickOnce(deps) {
|
|
12938
14558
|
const now = deps.now ? deps.now() : Date.now();
|
|
12939
|
-
const worktreeExists = deps.worktreeExists ??
|
|
12940
|
-
const hasTranscript = deps.hasTranscript ??
|
|
14559
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
14560
|
+
const hasTranscript = deps.hasTranscript ?? executorHasSession;
|
|
12941
14561
|
if (deps.rehydrate) {
|
|
12942
14562
|
const rh = deps.rehydrate;
|
|
12943
14563
|
const sinceLast = rh.lastAt == null ? Infinity : now - rh.lastAt;
|
|
@@ -12988,6 +14608,18 @@ async function tickOnce(deps) {
|
|
|
12988
14608
|
await dv.run();
|
|
12989
14609
|
}
|
|
12990
14610
|
}
|
|
14611
|
+
if (deps.repoMap) {
|
|
14612
|
+
const rm3 = deps.repoMap;
|
|
14613
|
+
const sinceLast = rm3.lastAt == null ? Infinity : now - rm3.lastAt;
|
|
14614
|
+
if (sinceLast >= rm3.intervalMs) {
|
|
14615
|
+
rm3.lastAt = now;
|
|
14616
|
+
try {
|
|
14617
|
+
await rm3.run();
|
|
14618
|
+
} catch (e) {
|
|
14619
|
+
console.error("[overmind] \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435 \u043A\u0430\u0440\u0442\u044B \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:", e);
|
|
14620
|
+
}
|
|
14621
|
+
}
|
|
14622
|
+
}
|
|
12991
14623
|
if (deps.rehydrate && deps.kill) {
|
|
12992
14624
|
const kill = deps.kill;
|
|
12993
14625
|
for (const run of deps.db.listRuns()) {
|
|
@@ -13013,6 +14645,19 @@ async function tickOnce(deps) {
|
|
|
13013
14645
|
}
|
|
13014
14646
|
}
|
|
13015
14647
|
}
|
|
14648
|
+
if (deps.logRotate) {
|
|
14649
|
+
const lr = deps.logRotate;
|
|
14650
|
+
const sinceLast = lr.lastAt == null ? Infinity : now - lr.lastAt;
|
|
14651
|
+
if (sinceLast >= lr.intervalMs) {
|
|
14652
|
+
lr.lastAt = now;
|
|
14653
|
+
try {
|
|
14654
|
+
lr.run();
|
|
14655
|
+
} catch (e) {
|
|
14656
|
+
console.error("[overmind] \u0440\u043E\u0442\u0430\u0446\u0438\u044F \u043B\u043E\u0433\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C:", e);
|
|
14657
|
+
}
|
|
14658
|
+
}
|
|
14659
|
+
}
|
|
14660
|
+
pruneGateLogs(deps);
|
|
13016
14661
|
let stormActive = false;
|
|
13017
14662
|
if (deps.storm) {
|
|
13018
14663
|
for (const run of deps.db.listRuns("running")) {
|
|
@@ -13038,7 +14683,7 @@ async function tickOnce(deps) {
|
|
|
13038
14683
|
if (!stormActive) {
|
|
13039
14684
|
await resumeWatchdogParked(deps, droneCap);
|
|
13040
14685
|
}
|
|
13041
|
-
for (
|
|
14686
|
+
for (let run of deps.db.listRuns("review")) {
|
|
13042
14687
|
try {
|
|
13043
14688
|
if (run.resolves_run_id != null) {
|
|
13044
14689
|
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
@@ -13057,7 +14702,9 @@ async function tickOnce(deps) {
|
|
|
13057
14702
|
continue;
|
|
13058
14703
|
}
|
|
13059
14704
|
if (!run.pr_url) continue;
|
|
13060
|
-
const
|
|
14705
|
+
const prUrl = run.pr_url;
|
|
14706
|
+
const st = await deps.prStatus(prUrl);
|
|
14707
|
+
run = syncBranchFromPr(deps.db, run, st.headBranch);
|
|
13061
14708
|
if (st.merged) {
|
|
13062
14709
|
await finalizeMergedReview(deps, run);
|
|
13063
14710
|
narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${run.task_slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430, \u0441\u043B\u043E\u0442 \u0441\u0432\u043E\u0431\u043E\u0434\u0435\u043D.`);
|
|
@@ -13126,11 +14773,13 @@ async function tickOnce(deps) {
|
|
|
13126
14773
|
continue;
|
|
13127
14774
|
}
|
|
13128
14775
|
const attempts = deps.db.reviewerAttemptCountFor(run.id);
|
|
14776
|
+
const runEvents = deps.db.events(run.id);
|
|
14777
|
+
const unrecoverableMark = reviewUnrecoverableMark(run.branch || run.task_slug);
|
|
13129
14778
|
let verdictFromComment = null;
|
|
13130
14779
|
let lastAttempt = null;
|
|
13131
14780
|
if (attempts > 0 && deps.verdictComment) {
|
|
13132
14781
|
lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
|
|
13133
|
-
verdictFromComment = await deps.verdictComment(
|
|
14782
|
+
verdictFromComment = await deps.verdictComment(prUrl, lastAttempt?.created_at ?? run.created_at).catch(() => null);
|
|
13134
14783
|
}
|
|
13135
14784
|
if (verdictFromComment) {
|
|
13136
14785
|
const mark = verdictCommentMark(verdictFromComment.createdAt);
|
|
@@ -13145,15 +14794,16 @@ async function tickOnce(deps) {
|
|
|
13145
14794
|
await wakeAuthorOnChanges(deps, run, now, droneCap);
|
|
13146
14795
|
}
|
|
13147
14796
|
}
|
|
14797
|
+
} else if (runEvents.some((e) => (e.text ?? "").includes(unrecoverableMark))) {
|
|
13148
14798
|
} else if (attempts >= REVIEW_SPAWN_LIMIT) {
|
|
13149
|
-
if (!
|
|
14799
|
+
if (!runEvents.some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
|
|
13150
14800
|
deps.db.addEvent(run.id, "step", `${REVIEW_ESCALATION_MARK}: ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u0440\u0435\u0432\u044C\u044E \u0431\u0435\u0437 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
|
|
13151
14801
|
narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u043D\u0435 \u0441\u043C\u043E\u0433\u043B\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C\u0441\u044F \u0437\u0430 ${attempts} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A.`);
|
|
13152
14802
|
}
|
|
13153
14803
|
} else {
|
|
13154
14804
|
const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
|
|
13155
14805
|
if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk) {
|
|
13156
|
-
const freshSt = await deps.prStatus(
|
|
14806
|
+
const freshSt = await deps.prStatus(prUrl);
|
|
13157
14807
|
if (freshSt.merged) {
|
|
13158
14808
|
await finalizeMergedReview(deps, run);
|
|
13159
14809
|
narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E, \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
|
|
@@ -13166,11 +14816,21 @@ async function tickOnce(deps) {
|
|
|
13166
14816
|
} else if (freshSt.conflicting || freshSt.isDraft) {
|
|
13167
14817
|
deps.db.addEvent(run.id, "step", "PR \u0441\u0442\u0430\u043B \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u044E\u0449\u0438\u043C/\u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u043E\u043C, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D \u0434\u043E \u0441\u043B\u0435\u0434. \u0442\u0438\u043A\u0430");
|
|
13168
14818
|
} else {
|
|
14819
|
+
run = syncBranchFromPr(deps.db, run, freshSt.headBranch);
|
|
13169
14820
|
try {
|
|
13170
14821
|
await deps.spawnReviewer(run, st.files ?? []);
|
|
13171
14822
|
narrate(deps, `PR ${droneLabel(run)} \u0433\u043E\u0442\u043E\u0432 \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u043D\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0433\u043E \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430.`);
|
|
13172
14823
|
} catch (e) {
|
|
13173
|
-
|
|
14824
|
+
if (isUnrecoverableRefError(e)) {
|
|
14825
|
+
deps.db.addEvent(
|
|
14826
|
+
run.id,
|
|
14827
|
+
"step",
|
|
14828
|
+
`${unrecoverableMark}: ${String(e)} \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`
|
|
14829
|
+
);
|
|
14830
|
+
narrate(deps, `\u0420\u0435\u0432\u044C\u044E\u0435\u0440\u0430 ${droneLabel(run)} \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u0442\u044C: \u0432\u0435\u0442\u043A\u0438 PR \u043D\u0435\u0442 \u043D\u0430 origin \u2014 \u0437\u043E\u0432\u0443 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0441\u0440\u0430\u0437\u0443, \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u043D\u0435 \u0436\u0433\u0443.`);
|
|
14831
|
+
} else {
|
|
14832
|
+
deps.db.addEvent(run.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u043E\u0441\u044C: ${String(e)}`);
|
|
14833
|
+
}
|
|
13174
14834
|
}
|
|
13175
14835
|
}
|
|
13176
14836
|
}
|
|
@@ -13437,7 +15097,9 @@ async function tickOnce(deps) {
|
|
|
13437
15097
|
// resource-gate-stall (TASWM-212): та же причина, что видит лог manageRelief — один
|
|
13438
15098
|
// deps.gateReason() на тик (кэш hostReading() внутри resources.ts, лишнего спавна нет).
|
|
13439
15099
|
...deps.gateReason ? { gateReason: deps.gateReason() } : {},
|
|
13440
|
-
...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {}
|
|
15100
|
+
...az.stopQuiet ? { stopQuiet: az.stopQuiet() } : {},
|
|
15101
|
+
// Рестарт не переигрывает падения (TAS-485) — см. closedBeforeStart в anomalies.ts.
|
|
15102
|
+
...az.startedAt != null ? { daemonStartedAt: az.startedAt } : {}
|
|
13441
15103
|
}),
|
|
13442
15104
|
...buildEscalationCandidates(deps.db),
|
|
13443
15105
|
// ЦЕНА РАЗГРУЗКИ (TASWM-316): «машина не тянет» с числами и рекомендацией — та же
|
|
@@ -13467,6 +15129,20 @@ async function tickOnce(deps) {
|
|
|
13467
15129
|
}
|
|
13468
15130
|
narrate(deps, `\u26A0\uFE0F ${signal.title}`);
|
|
13469
15131
|
}
|
|
15132
|
+
const stale = az.tracker.pendingResolved();
|
|
15133
|
+
if (stale.length > 0) {
|
|
15134
|
+
if (!az.resolve) {
|
|
15135
|
+
az.tracker.forgetResolved(stale);
|
|
15136
|
+
} else {
|
|
15137
|
+
const batch = stale.slice(0, ANOMALY_RESOLVE_BATCH);
|
|
15138
|
+
try {
|
|
15139
|
+
await az.resolve(batch);
|
|
15140
|
+
az.tracker.forgetResolved(batch);
|
|
15141
|
+
} catch (e) {
|
|
15142
|
+
console.error("[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0433\u0430\u0448\u0435\u043D\u0438\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0432\u0448\u0438\u0445\u0441\u044F \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u0439 \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u043E, \u043F\u043E\u0432\u0442\u043E\u0440\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:", e);
|
|
15143
|
+
}
|
|
15144
|
+
}
|
|
15145
|
+
}
|
|
13470
15146
|
}
|
|
13471
15147
|
if (deps.watchdog) {
|
|
13472
15148
|
try {
|
|
@@ -13532,7 +15208,7 @@ async function cancelRun(deps, slug, reason) {
|
|
|
13532
15208
|
}
|
|
13533
15209
|
async function resolveConflict(deps, reviewRun) {
|
|
13534
15210
|
const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
|
|
13535
|
-
const branch = reviewRun
|
|
15211
|
+
const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
|
|
13536
15212
|
const resolver = deps.db.createRun({
|
|
13537
15213
|
task_slug: reviewRun.task_slug,
|
|
13538
15214
|
repo,
|
|
@@ -13542,7 +15218,7 @@ async function resolveConflict(deps, reviewRun) {
|
|
|
13542
15218
|
workspace_id: reviewRun.workspace_id
|
|
13543
15219
|
// та же задача, тот же воркспейс (TASWM-288)
|
|
13544
15220
|
});
|
|
13545
|
-
const wt =
|
|
15221
|
+
const wt = join28(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-resolve-${resolver.id}`);
|
|
13546
15222
|
try {
|
|
13547
15223
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
13548
15224
|
await deps.git.setWorktreeIdentity(wt, resolver.name);
|
|
@@ -13572,14 +15248,14 @@ async function resolveConflict(deps, reviewRun) {
|
|
|
13572
15248
|
await deps.git.removeWorktree(repo, wt).catch(() => {
|
|
13573
15249
|
});
|
|
13574
15250
|
deps.db.updateRun(resolver.id, { status: "failed" });
|
|
13575
|
-
const marker = isTransientResourceError(e) ? "\u041E\u0421 \u043D\u0435 \u0434\u0430\u0451\u0442 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C git-\u043F\u0440\u043E\u0446\u0435\u0441\u0441\u044B (\u0440\u0435\u0441\u0443\u0440\u0441, \u043D\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442)" : "merge-main-in \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F";
|
|
15251
|
+
const marker = isUnrecoverableRefError(e) ? "\u0432\u0435\u0442\u043A\u0438 PR \u043D\u0435\u0442 \u043D\u0430 origin \u2014 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u043C \u043D\u0435 \u043B\u0435\u0447\u0438\u0442\u0441\u044F, \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A" : isTransientResourceError(e) ? "\u041E\u0421 \u043D\u0435 \u0434\u0430\u0451\u0442 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C git-\u043F\u0440\u043E\u0446\u0435\u0441\u0441\u044B (\u0440\u0435\u0441\u0443\u0440\u0441, \u043D\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442)" : "merge-main-in \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F";
|
|
13576
15252
|
deps.db.addEvent(resolver.id, "failed", `${marker}: ${String(e)}`);
|
|
13577
15253
|
throw e;
|
|
13578
15254
|
}
|
|
13579
15255
|
}
|
|
13580
15256
|
async function spawnReviewer(deps, reviewRun, files) {
|
|
13581
15257
|
const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
|
|
13582
|
-
const branch = reviewRun
|
|
15258
|
+
const branch = await worktreeBranchFor(reviewRun, deps.prHeadBranch);
|
|
13583
15259
|
const priorReview = reviewRun.review_cycle > 0 ? lastChangesSummary(deps.db, reviewRun.id) : null;
|
|
13584
15260
|
const verifyMode = deps.verifyMode ? await deps.verifyMode(repo).catch(() => void 0) : void 0;
|
|
13585
15261
|
const checklist = buildReviewChecklist(reviewRun, files, priorReview, verifyMode);
|
|
@@ -13594,7 +15270,7 @@ async function spawnReviewer(deps, reviewRun, files) {
|
|
|
13594
15270
|
// (своей у ревьюера нет, TASWM-204), и коммент с вердиктом должен уехать именно туда.
|
|
13595
15271
|
workspace_id: reviewRun.workspace_id
|
|
13596
15272
|
});
|
|
13597
|
-
const wt =
|
|
15273
|
+
const wt = join28(deps.worktreesDir, basename4(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
|
|
13598
15274
|
try {
|
|
13599
15275
|
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
13600
15276
|
await deps.git.setWorktreeIdentity(wt, reviewer.name);
|
|
@@ -13629,7 +15305,7 @@ async function respawnRun(deps, run, mode, nudge) {
|
|
|
13629
15305
|
]);
|
|
13630
15306
|
const base3 = effectiveBaseBranch(run.base_branch, projectBase);
|
|
13631
15307
|
const runToSpawn = base3 && run.base_branch !== base3 ? deps.db.updateRun(run.id, { base_branch: base3 }) ?? run : run;
|
|
13632
|
-
if (!
|
|
15308
|
+
if (!existsSync16(wt)) {
|
|
13633
15309
|
const branch = runToSpawn.branch || runToSpawn.task_slug;
|
|
13634
15310
|
const source = await addWorktree(repo, wt, branch, {
|
|
13635
15311
|
...base3 ? { base: await remoteBaseRef(repo, base3) } : {},
|
|
@@ -13654,11 +15330,11 @@ async function respawnRun(deps, run, mode, nudge) {
|
|
|
13654
15330
|
const prompt = notice ? `${freshPrompt}
|
|
13655
15331
|
|
|
13656
15332
|
${notice}` : freshPrompt;
|
|
13657
|
-
const pid = deps.launch({ worktree: wt, run, prompt });
|
|
15333
|
+
const pid = deps.launch({ worktree: wt, run: runToSpawn, prompt });
|
|
13658
15334
|
deps.db.updateRun(run.id, { status: "running", pid });
|
|
13659
15335
|
}
|
|
13660
15336
|
async function syncCrashedOnStartup(deps) {
|
|
13661
|
-
const worktreeExists = deps.worktreeExists ??
|
|
15337
|
+
const worktreeExists = deps.worktreeExists ?? existsSync16;
|
|
13662
15338
|
let orphaned = 0;
|
|
13663
15339
|
let merged = 0;
|
|
13664
15340
|
let requeued = 0;
|
|
@@ -13721,39 +15397,37 @@ async function makeStore(repo, worktreesDir, repoFlag) {
|
|
|
13721
15397
|
console.warn(`[overmind] \u043D\u0435 \u0441\u043C\u043E\u0433 \u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u044C \u043F\u0443\u0442\u044C \u043A \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044E (${e instanceof Error ? e.message : e}) \u2014 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u0437\u0430\u043F\u0443\u0441\u043A \u0441\u043D\u043E\u0432\u0430 \u043F\u043E\u043F\u0440\u043E\u0441\u0438\u0442 --repo`);
|
|
13722
15398
|
}
|
|
13723
15399
|
}
|
|
15400
|
+
const scanRootsList = resolveScanRoots(SWARM_SCAN_ROOTS_RAW, homedir5(), repoPaths);
|
|
15401
|
+
const repoMap = emptyRepoMapState(scanRootsList);
|
|
13724
15402
|
if (SWARM_BACKEND !== "client") {
|
|
13725
15403
|
const { SwarmDb: SwarmDb2 } = await loadSqlite();
|
|
13726
|
-
return { store: new SwarmDb2(DB_PATH, repo), reconciled: true, host };
|
|
15404
|
+
return { store: new SwarmDb2(DB_PATH, repo), reconciled: true, host, repoMap };
|
|
13727
15405
|
}
|
|
13728
15406
|
const call = resilientSwarmMcp({
|
|
13729
15407
|
onDown: (ms) => console.warn(`[overmind] \u043F\u0440\u043E\u0434-MCP \u043C\u043E\u043B\u0447\u0438\u0442 ${Math.round(ms / 1e3)}\u0441 \u2014 \u0440\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432\u0441\u043B\u0435\u043F\u0443\u044E, \u0441\u0434\u0430\u0447\u0438 \u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u044B \u043D\u0435 \u0434\u043E\u0435\u0437\u0436\u0430\u044E\u0442.`),
|
|
13730
15408
|
onUp: () => console.warn("[overmind] \u043F\u0440\u043E\u0434-MCP \u0441\u043D\u043E\u0432\u0430 \u043D\u0430 \u0441\u0432\u044F\u0437\u0438.")
|
|
13731
15409
|
});
|
|
13732
15410
|
const explicitRegistry = await buildRepoRegistry(repoPaths);
|
|
13733
|
-
|
|
13734
|
-
const scanRootsList = defaultScanRoots(homedir3(), repoPaths);
|
|
13735
|
-
const scanStartedAt = Date.now();
|
|
13736
|
-
const scanned = scanRoots(scanRootsList, { maxDepth: SWARM_SCAN_DEPTH });
|
|
13737
|
-
let addedByScan = 0;
|
|
13738
|
-
for (const [full, path] of scanned) {
|
|
13739
|
-
if (byName.has(full)) continue;
|
|
13740
|
-
byName.set(full, path);
|
|
13741
|
-
addedByScan++;
|
|
13742
|
-
}
|
|
13743
|
-
if (addedByScan > 0) {
|
|
13744
|
-
console.log(
|
|
13745
|
-
`[overmind] \u043D\u0430\u0448\u0451\u043B \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435 ${addedByScan} \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439(\u0435\u0432) \u0441\u043A\u0430\u043D\u043E\u043C \u0437\u0430 ${Date.now() - scanStartedAt}\u043C\u0441: ${[...scanned.keys()].slice(0, 8).join(", ")}${scanned.size > 8 ? ` \u0438 \u0435\u0449\u0451 ${scanned.size - 8}` : ""}`
|
|
13746
|
-
);
|
|
13747
|
-
}
|
|
13748
|
-
const repoRegistry = [...byName].map(([repoFullName, path]) => ({ repoFullName, path }));
|
|
13749
|
-
if (repoPaths.length > 0 && repoRegistry.length < repoPaths.length) {
|
|
15411
|
+
if (repoPaths.length > 0 && explicitRegistry.length < repoPaths.length) {
|
|
13750
15412
|
console.warn(
|
|
13751
|
-
`[overmind] \u0440\u0435\u043F\u043E-\u0440\u0435\u0435\u0441\u0442\u0440: ${
|
|
15413
|
+
`[overmind] \u0440\u0435\u043F\u043E-\u0440\u0435\u0435\u0441\u0442\u0440: ${explicitRegistry.length}/${repoPaths.length} \u043F\u0443\u0442\u0435\u0439 \u0440\u0435\u0437\u043E\u043B\u0432\u043D\u0443\u043B\u0438\u0441\u044C \u0432 repoFullName (\u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u0435 \u2014 \u043D\u0435 git-\u0440\u0435\u043F\u043E \u0438\u043B\u0438 \u0431\u0435\u0437 remote origin)`
|
|
13752
15414
|
);
|
|
13753
15415
|
}
|
|
15416
|
+
const rebuildRegistry = () => refreshRepoMap({
|
|
15417
|
+
call,
|
|
15418
|
+
configured: explicitRegistry,
|
|
15419
|
+
roots: scanRootsList,
|
|
15420
|
+
maxDepth: SWARM_SCAN_DEPTH,
|
|
15421
|
+
cachePath: REPO_MAP_PATH,
|
|
15422
|
+
state: repoMap,
|
|
15423
|
+
log: (m) => console.log(m)
|
|
15424
|
+
});
|
|
15425
|
+
const repoRegistry = await rebuildRegistry();
|
|
13754
15426
|
if (repoRegistry.length === 0) {
|
|
13755
15427
|
console.warn(
|
|
13756
|
-
`[overmind] \u041D\u0415 \u041D\u0410\u0428\u0401\u041B \u041D\u0418 \u041E\u0414\u041D\u041E\u0413\u041E \u0420\u0415\u041F\u041E\u0417\u0418\u0422\u041E\u0420\u0418\u042F \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u2014 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0431\u0440\u0430\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0443\u0442: \u0438\u0445 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043F\u0440\u0438\u0445\u043E\u0434\u0438\u0442 \u0438\u043C\u0435\u043D\u0435\u043C owner/repo, \u0430 \u0441\u043E\u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u0441 \u0447\u0435\u043C. \u0418\u0441\u043A\u0430\u043B \u0432: ${scanRootsList.slice(0, 6).join(", ")}.
|
|
15428
|
+
`[overmind] \u041D\u0415 \u041D\u0410\u0428\u0401\u041B \u041D\u0418 \u041E\u0414\u041D\u041E\u0413\u041E \u0420\u0415\u041F\u041E\u0417\u0418\u0422\u041E\u0420\u0418\u042F \u043D\u0430 \u044D\u0442\u043E\u0439 \u043C\u0430\u0448\u0438\u043D\u0435 \u2014 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B \u0431\u0440\u0430\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0443\u0442: \u0438\u0445 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043F\u0440\u0438\u0445\u043E\u0434\u0438\u0442 \u0438\u043C\u0435\u043D\u0435\u043C owner/repo, \u0430 \u0441\u043E\u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0435\u0433\u043E \u043D\u0435 \u0441 \u0447\u0435\u043C. \u0418\u0441\u043A\u0430\u043B \u0432: ${scanRootsList.slice(0, 6).join(", ")}. ` + // TASWM-336: почему пусто — три РАЗНЫЕ причины, и лечатся они по-разному. Одна
|
|
15429
|
+
// общая фраза заставляла бы человека гадать, что из этого его случай.
|
|
15430
|
+
(repoMap.unknown ? "\u041F\u0440\u043E\u0434 \u043D\u0435 \u0441\u043A\u0430\u0437\u0430\u043B, \u043A\u0430\u043A\u0438\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0443 \u0440\u043E\u044F, \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C `swarm login` \u0438 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0441\u0442\u044C \u0441\u0435\u0440\u0432\u0435\u0440\u0430. " : repoMap.missing.length ? `\u041D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0438\u0437 ${repoMap.missing.length} \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u0440\u043E\u044F \u0442\u0443\u0442 \u043D\u0435\u0442. ` : "\u0423 \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0430 \u043D\u0435\u0442 \u043F\u0440\u043E\u0435\u043A\u0442\u043E\u0432 \u0441 \u0432\u043A\u043B\u044E\u0447\u0451\u043D\u043D\u044B\u043C \u0440\u043E\u0435\u043C. ") + "\u0415\u0441\u043B\u0438 \u043A\u043B\u043E\u043D\u044B \u043B\u0435\u0436\u0430\u0442 \u0432 \u0434\u0440\u0443\u0433\u043E\u043C \u043C\u0435\u0441\u0442\u0435 \u2014 \u0443\u043A\u0430\u0436\u0438: swarm start --repo <\u043F\u0443\u0442\u044C \u043A \u0440\u0435\u043F\u043E> (\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u0442\u0441\u044F)."
|
|
13757
15431
|
);
|
|
13758
15432
|
}
|
|
13759
15433
|
const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo, repoRegistry, host });
|
|
@@ -13768,7 +15442,17 @@ async function makeStore(repo, worktreesDir, repoFlag) {
|
|
|
13768
15442
|
`overmind backend=client (\u0442\u043E\u043D\u043A\u0438\u0439 \u043A\u043B\u0438\u0435\u043D\u0442, outbox=${SWARM_OUTBOX}); \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F (ps \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D) \u2014 running/blocked \u043E\u0441\u0442\u0430\u044E\u0442\u0441\u044F \u043D\u0435\u0440\u0435\u043A\u043E\u043D\u0441\u0438\u043B\u0438\u0440\u043E\u0432\u0430\u043D\u044B, startup-sync (TASWM-128) \u0431\u0443\u0434\u0435\u0442 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D`
|
|
13769
15443
|
);
|
|
13770
15444
|
}
|
|
13771
|
-
return {
|
|
15445
|
+
return {
|
|
15446
|
+
store: client,
|
|
15447
|
+
reconciled: rec.reconciled,
|
|
15448
|
+
host,
|
|
15449
|
+
repoMap,
|
|
15450
|
+
// Периодическая пересборка (шаг 0a4 тика): применяем новый реестр к ЖИВОМУ клиенту —
|
|
15451
|
+
// и список для claim/heartbeat, и обратную карту owner/repo → путь.
|
|
15452
|
+
refreshRepoMap: async () => {
|
|
15453
|
+
client.setRepoRegistry(await rebuildRegistry());
|
|
15454
|
+
}
|
|
15455
|
+
};
|
|
13772
15456
|
}
|
|
13773
15457
|
var crashGuardsInstalled = false;
|
|
13774
15458
|
function installCrashGuards() {
|
|
@@ -13787,22 +15471,22 @@ async function migrateWorktreesAtStartup() {
|
|
|
13787
15471
|
legacyDir: WORKTREES_LEGACY_DIR,
|
|
13788
15472
|
targetDir: WORKTREES_DIR,
|
|
13789
15473
|
envOverride: process.env.SWARM_WORKTREES || "",
|
|
13790
|
-
exists: (d) =>
|
|
15474
|
+
exists: (d) => existsSync16(d),
|
|
13791
15475
|
isNonEmptyDir: (d) => {
|
|
13792
15476
|
try {
|
|
13793
|
-
return
|
|
15477
|
+
return readdirSync12(d).length > 0;
|
|
13794
15478
|
} catch {
|
|
13795
15479
|
return false;
|
|
13796
15480
|
}
|
|
13797
15481
|
},
|
|
13798
15482
|
liveDronesUnder: (d) => pidsWithCwdUnder(d).length > 0,
|
|
13799
|
-
rename: (from, to) =>
|
|
15483
|
+
rename: (from, to) => renameSync10(from, to),
|
|
13800
15484
|
repairMoved: (t) => repairMovedWorktrees(t),
|
|
13801
15485
|
log: (m) => console.log(m)
|
|
13802
15486
|
});
|
|
13803
15487
|
} catch (e) {
|
|
13804
15488
|
console.error("[migrate] \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u044F worktrees \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C \u2014 \u0444\u043E\u043B\u0431\u044D\u043A:", e);
|
|
13805
|
-
return
|
|
15489
|
+
return existsSync16(WORKTREES_DIR) ? WORKTREES_DIR : WORKTREES_LEGACY_DIR;
|
|
13806
15490
|
}
|
|
13807
15491
|
}
|
|
13808
15492
|
async function reportWorkspaceAccess(tl, db) {
|
|
@@ -13820,13 +15504,22 @@ async function reportWorkspaceAccess(tl, db) {
|
|
|
13820
15504
|
async function runDaemon(opts) {
|
|
13821
15505
|
installCrashGuards();
|
|
13822
15506
|
const daemonStart = await captureDaemonStart(THIS_FILE);
|
|
15507
|
+
const latestRelease = fetchLatestVersion();
|
|
13823
15508
|
const worktreesDir = await migrateWorktreesAtStartup();
|
|
13824
15509
|
if (!opts.repo) {
|
|
13825
15510
|
const remembered = effectiveRepoPaths("", loadRememberedRepos(SWARM_HOME))[0];
|
|
13826
15511
|
if (remembered) opts.repo = remembered;
|
|
13827
15512
|
}
|
|
13828
|
-
const {
|
|
15513
|
+
const {
|
|
15514
|
+
store: db,
|
|
15515
|
+
reconciled,
|
|
15516
|
+
host,
|
|
15517
|
+
repoMap: repoMapState,
|
|
15518
|
+
refreshRepoMap: rebuildRepoMap
|
|
15519
|
+
} = await makeStore(opts.repo, worktreesDir, opts.repoFlag);
|
|
13829
15520
|
await preflightAuth();
|
|
15521
|
+
const outdated = outdatedLine(PACKAGE_VERSION, await latestRelease);
|
|
15522
|
+
if (outdated) console.warn(`[overmind] ${outdated}`);
|
|
13830
15523
|
const tl = await connectTaskless();
|
|
13831
15524
|
const tlFor = (workspaceId) => tl ? tl.forWorkspace(workspaceId) : null;
|
|
13832
15525
|
if (tl) await reportWorkspaceAccess(tl, db);
|
|
@@ -13863,18 +15556,23 @@ async function runDaemon(opts) {
|
|
|
13863
15556
|
overmindEntry: THIS_FILE,
|
|
13864
15557
|
dbPath: DB_PATH,
|
|
13865
15558
|
runLogsDir: RUN_LOGS_DIR,
|
|
15559
|
+
// Кем исполнять (TASWM-349) — настройка SWARM_EXECUTOR, один резолв на весь демон.
|
|
15560
|
+
executor: EXECUTOR,
|
|
13866
15561
|
droneIsolation: DRONE_ISOLATION,
|
|
13867
15562
|
channel: droneChannelLaunch,
|
|
13868
15563
|
droneCap: () => lastDroneCap
|
|
13869
15564
|
});
|
|
13870
15565
|
let prepareReader;
|
|
15566
|
+
const zoneModels = new ZoneModels({ manualAreas: () => new Map(db.listRuns().map((r) => [r.task_slug, r.area])) });
|
|
15567
|
+
const activeFiles = new ActiveFilesCache((r) => changedPaths(r.worktree_path, { baseBranch: r.base_branch ?? null }));
|
|
13871
15568
|
const baseSpawn = {
|
|
13872
15569
|
db,
|
|
13873
15570
|
repo: opts.repo,
|
|
13874
15571
|
// фолбэк, если у прогона repo пуст
|
|
13875
15572
|
worktreesDir,
|
|
13876
15573
|
launch,
|
|
13877
|
-
prepare: (repo) => prepareReader ? prepareReader(repo) : Promise.resolve(null)
|
|
15574
|
+
prepare: (repo) => prepareReader ? prepareReader(repo) : Promise.resolve(null),
|
|
15575
|
+
conflicts: (run, repo) => zoneConflictFor(db, zoneModels, activeFiles, run, repo, opts.repo)
|
|
13878
15576
|
};
|
|
13879
15577
|
let intake;
|
|
13880
15578
|
if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_MAX_IN_FLIGHT > 0) {
|
|
@@ -13886,6 +15584,10 @@ async function runDaemon(opts) {
|
|
|
13886
15584
|
const client = db;
|
|
13887
15585
|
rehydrate = { run: () => client.hydrate(), ready: () => client.pendingCount() === 0, intervalMs: SWARM_REHYDRATE_MS };
|
|
13888
15586
|
}
|
|
15587
|
+
let repoMap;
|
|
15588
|
+
if (rebuildRepoMap && SWARM_REPO_MAP_MS > 0) {
|
|
15589
|
+
repoMap = { run: rebuildRepoMap, intervalMs: SWARM_REPO_MAP_MS, lastAt: Date.now() };
|
|
15590
|
+
}
|
|
13889
15591
|
let daemonVersion;
|
|
13890
15592
|
if (daemonStart && SWARM_VERSION_CHECK_MS > 0) {
|
|
13891
15593
|
let cached = null;
|
|
@@ -13904,7 +15606,7 @@ async function runDaemon(opts) {
|
|
|
13904
15606
|
const client = db;
|
|
13905
15607
|
heartbeat = {
|
|
13906
15608
|
push: async () => {
|
|
13907
|
-
const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null);
|
|
15609
|
+
const reply = await client.heartbeat(daemonVersion?.get() ?? null, control.state ?? null, lastDroneCap);
|
|
13908
15610
|
if (reply.commands.length) onHostCommands(reply.commands);
|
|
13909
15611
|
if (reply.logRequests.length) {
|
|
13910
15612
|
await serveLogRequests(
|
|
@@ -13974,7 +15676,14 @@ async function runDaemon(opts) {
|
|
|
13974
15676
|
autoReviewEnabled: SWARM_AUTO_REVIEW,
|
|
13975
15677
|
outboxPendingCount: () => client.pendingCount(),
|
|
13976
15678
|
notify: (signal) => client.notifyAnomaly(signal),
|
|
13977
|
-
|
|
15679
|
+
// TAS-485: условие ушло → гасим свою же тревогу в инбоксе.
|
|
15680
|
+
resolve: (keys) => client.resolveAnomalies(keys),
|
|
15681
|
+
stopQuiet: () => anomalyState.stopQuiet(),
|
|
15682
|
+
// TAS-485: этот демон отвечает за то, что случилось ПРИ НЁМ. Прогон, закрытый до
|
|
15683
|
+
// старта, он не переоткрывает — иначе каждый перезапуск начинает волну заново
|
|
15684
|
+
// (id прогонов в зеркале свои у каждой жизни процесса, durable-кулдаун по ним не
|
|
15685
|
+
// совпадает — см. closedBeforeStart в anomalies.ts).
|
|
15686
|
+
startedAt: Date.now()
|
|
13978
15687
|
};
|
|
13979
15688
|
}
|
|
13980
15689
|
const watchdog = {
|
|
@@ -14047,6 +15756,9 @@ async function runDaemon(opts) {
|
|
|
14047
15756
|
worktreesDir,
|
|
14048
15757
|
launch,
|
|
14049
15758
|
git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
|
|
15759
|
+
// TASWM-356: ветка ревьюера — фактическая ветка PR. Штатно она уже в поле прогона
|
|
15760
|
+
// (реконсайл синхронит её из headBranch), этот ридер — для прогонов без неё.
|
|
15761
|
+
prHeadBranch,
|
|
14050
15762
|
// TASWM-293: ТОТ ЖЕ живой ридер verifyMode, что у исполнителя (spawnDrone) —
|
|
14051
15763
|
// иначе ревьюер судит проект с verifyMode=local по правилам ci и блокирует PR
|
|
14052
15764
|
// за отсутствие CI, которого у проекта нет и не предполагается.
|
|
@@ -14117,8 +15829,9 @@ async function runDaemon(opts) {
|
|
|
14117
15829
|
baseBranchReader ? baseBranchReader(repoOfRun) : Promise.resolve("")
|
|
14118
15830
|
]);
|
|
14119
15831
|
const task = taskClient ? requireTaskBody(rawTask, run.task_slug, run.workspace_id) : null;
|
|
14120
|
-
const
|
|
14121
|
-
const
|
|
15832
|
+
const healed = healTaskSlug(db, run, task);
|
|
15833
|
+
const base3 = effectiveBaseBranch(healed.base_branch, projectBase);
|
|
15834
|
+
const runToSpawn = base3 && healed.base_branch !== base3 ? db.updateRun(healed.id, { base_branch: base3 }) ?? healed : healed;
|
|
14122
15835
|
const taskContext = task && taskClient ? await fetchTaskContext(taskClient, task) : void 0;
|
|
14123
15836
|
await spawnDrone(
|
|
14124
15837
|
{
|
|
@@ -14136,9 +15849,9 @@ async function runDaemon(opts) {
|
|
|
14136
15849
|
runToSpawn
|
|
14137
15850
|
);
|
|
14138
15851
|
if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
|
|
14139
|
-
if (taskClient) await taskClient.startTask(
|
|
15852
|
+
if (taskClient) await taskClient.startTask(runToSpawn.task_slug);
|
|
14140
15853
|
},
|
|
14141
|
-
prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state })),
|
|
15854
|
+
prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state, headBranch: s.headBranch })),
|
|
14142
15855
|
// Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
|
|
14143
15856
|
// (фолбэк — дефолт-репо роя). Реальный gh; логика усыновления — в tickOnce (тестируется).
|
|
14144
15857
|
openPrsForBranch: (branch, repoDir) => openPrsForBranch(branch, repoDir || opts.repo),
|
|
@@ -14162,6 +15875,8 @@ async function runDaemon(opts) {
|
|
|
14162
15875
|
reportComment: (slug, body, label, ws) => tl.forWorkspace(ws).addComment(slug, body, label)
|
|
14163
15876
|
} : {},
|
|
14164
15877
|
prCommitTitles: (prUrl) => prCommitTitles(prUrl),
|
|
15878
|
+
prHeadBranch: (prUrl) => prHeadBranch(prUrl),
|
|
15879
|
+
// TASWM-356: ветка PR на усыновлении, узким запросом
|
|
14165
15880
|
journalForBranch: (repo, branch, slug) => journalForBranch(repo || opts.repo, branch, slug),
|
|
14166
15881
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p),
|
|
14167
15882
|
// TASWM-303: тот же фолбэк repo, что у removeWorktree/journalForBranch — run.repo пуст
|
|
@@ -14171,15 +15886,17 @@ async function runDaemon(opts) {
|
|
|
14171
15886
|
repoHasCi: (run) => repoHasCiWorkflows(run.worktree_path, run.repo || opts.repo),
|
|
14172
15887
|
isAlive: pidAlive,
|
|
14173
15888
|
// Учёт cost/usage (TASWM-188): реальный аккаунтант поверх RUN_LOGS_DIR + db.
|
|
14174
|
-
accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db }),
|
|
15889
|
+
accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db, executor: EXECUTOR }),
|
|
14175
15890
|
// Классификация выхода прогона (TASWM-229): result-файл того же RUN_LOGS_DIR — «вышел сам»
|
|
14176
15891
|
// (graceful) vs «убит на полпути» (crash). Тот же каталог, что читает accountCost.
|
|
14177
|
-
exitOutcome: makeExitClassifier(RUN_LOGS_DIR),
|
|
15892
|
+
exitOutcome: makeExitClassifier(RUN_LOGS_DIR, EXECUTOR),
|
|
14178
15893
|
// Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
|
|
14179
15894
|
livePpids,
|
|
14180
15895
|
// Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
|
|
14181
15896
|
// POST /swarm/kill → stopped в проде → overmind гасит живой pid на реконсайле).
|
|
14182
|
-
|
|
15897
|
+
// TASWM-349: свой способ остановки — за адаптером; не задан (оба сегодняшних) → общий
|
|
15898
|
+
// групповой SIGTERM.
|
|
15899
|
+
kill: (pid) => EXECUTOR.stop ? EXECUTOR.stop(pid, "SIGTERM") : killProcessGroup(pid, "SIGTERM"),
|
|
14183
15900
|
// Реклейм worktree паузнутого/возвращаемого дрона (TASWM-228): тот же cwd-скан, что и
|
|
14184
15901
|
// очистка перед сносом worktree, но БЕЗ сноса каталога — worktree остаётся под
|
|
14185
15902
|
// --continue. Гасит тяжёлую работу дрона, ушедшую из его process group (Bash-тул
|
|
@@ -14218,7 +15935,7 @@ async function runDaemon(opts) {
|
|
|
14218
15935
|
lastCommitMs: (p) => worktreeLastCommitMs(p),
|
|
14219
15936
|
// mtime транскрипта claude-сессии как liveness (TASWM-136): точнее коммитов — обновляется
|
|
14220
15937
|
// на каждое действие дрона (Edit/Read/tool-use), не только на коммит.
|
|
14221
|
-
transcriptMtimeMs: (p) =>
|
|
15938
|
+
transcriptMtimeMs: (p) => EXECUTOR.session.mtimeMs(p),
|
|
14222
15939
|
// Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
|
|
14223
15940
|
resourcesOk: hostResourcesOk,
|
|
14224
15941
|
// Ресурс-гейт v2 (TASWM-151): жёсткий порог активной разгрузки — те же сырые показания
|
|
@@ -14249,6 +15966,29 @@ async function runDaemon(opts) {
|
|
|
14249
15966
|
...exclusiveProject ? { exclusiveProject } : {},
|
|
14250
15967
|
repoFallback: opts.repo,
|
|
14251
15968
|
exclusiveLogged: /* @__PURE__ */ new Map(),
|
|
15969
|
+
// Зона задачи из истории репо (TASWM-348) — предлагаем сами, если постановщик
|
|
15970
|
+
// её не проставил. Дедуп событий отдельной мапой, как у гейтов выше.
|
|
15971
|
+
suggestArea: async (run) => {
|
|
15972
|
+
const repo = normalizeRepo(run.repo || opts.repo || "");
|
|
15973
|
+
return repo ? suggestAreaForRun(await zoneModels.forRepo(repo), run) : null;
|
|
15974
|
+
},
|
|
15975
|
+
overlapLogged: /* @__PURE__ */ new Map(),
|
|
15976
|
+
// Ротация своего лога (TASWM-330): демон пишет в него неделями, а до этого ротация была
|
|
15977
|
+
// только при старте. Итог ротации печатаем в сам лог — первой строкой нового поколения
|
|
15978
|
+
// видно, что история не потерялась, а уехала в `.1`.
|
|
15979
|
+
logRotate: {
|
|
15980
|
+
run: () => {
|
|
15981
|
+
if (!stdoutGoesTo(DAEMON_LOG_PATH)) return;
|
|
15982
|
+
const outcome = rotateDaemonLog(DAEMON_LOG_PATH, { maxBytes: DAEMON_LOG_MAX_BYTES, keep: DAEMON_LOG_KEEP });
|
|
15983
|
+
if (outcome === "rotated") {
|
|
15984
|
+
console.log(
|
|
15985
|
+
`[overmind] \u043B\u043E\u0433 \u0434\u043E\u0440\u043E\u0441 \u0434\u043E ${Math.round(DAEMON_LOG_MAX_BYTES / 1024 / 1024)} \u041C\u0411 \u2014 \u043F\u0440\u043E\u0448\u043B\u043E\u0435 \u043F\u043E\u043A\u043E\u043B\u0435\u043D\u0438\u0435 \u0432 ${DAEMON_LOG_PATH}.1 (\u0434\u0435\u0440\u0436\u0438\u043C ${DAEMON_LOG_KEEP}; \u043F\u043E\u0442\u043E\u043B\u043E\u043A \u2014 SWARM_DAEMON_LOG_MAX_BYTES)`
|
|
15986
|
+
);
|
|
15987
|
+
}
|
|
15988
|
+
},
|
|
15989
|
+
intervalMs: DAEMON_LOG_CHECK_MS,
|
|
15990
|
+
lastAt: null
|
|
15991
|
+
},
|
|
14252
15992
|
// merge-main-in: review-PR конфликтует с main → влить main в ветку PR, не переделывая
|
|
14253
15993
|
// фичу. Реальные git-примитивы + launch; логика — в resolveConflict (тестируется).
|
|
14254
15994
|
resolveConflict: (reviewRun) => resolveConflict(
|
|
@@ -14257,7 +15997,9 @@ async function runDaemon(opts) {
|
|
|
14257
15997
|
repoFallback: opts.repo,
|
|
14258
15998
|
worktreesDir,
|
|
14259
15999
|
launch,
|
|
14260
|
-
git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree }
|
|
16000
|
+
git: { prepareDetachedWorktree, setWorktreeIdentity, remoteMainRef, remoteBaseRef, mergeRef, pushHeadTo, removeWorktree },
|
|
16001
|
+
prHeadBranch
|
|
16002
|
+
// TASWM-356: ветка PR для прогонов без записанного поля branch
|
|
14261
16003
|
},
|
|
14262
16004
|
reviewRun
|
|
14263
16005
|
).then(() => {
|
|
@@ -14288,8 +16030,8 @@ async function runDaemon(opts) {
|
|
|
14288
16030
|
mode,
|
|
14289
16031
|
nudge
|
|
14290
16032
|
),
|
|
14291
|
-
worktreeExists: (p) =>
|
|
14292
|
-
hasTranscript:
|
|
16033
|
+
worktreeExists: (p) => existsSync16(p),
|
|
16034
|
+
hasTranscript: executorHasSession,
|
|
14293
16035
|
// CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
|
|
14294
16036
|
// (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
|
|
14295
16037
|
// строится над failing/failingRunIds ИЗ checks — без повторного `gh pr checks`.
|
|
@@ -14300,7 +16042,7 @@ async function runDaemon(opts) {
|
|
|
14300
16042
|
// Штормовой режим (TASWM-23): health-probe на отбой (реальный claude) + внешний
|
|
14301
16043
|
// сигнал SWARM_STORM_URL (2-й слой, если задан). Логика/пороги — в manageStorm/storm.ts.
|
|
14302
16044
|
storm: {
|
|
14303
|
-
probe: () =>
|
|
16045
|
+
probe: () => executorHealthy({ executor: EXECUTOR, timeoutMs: resolveStormConfig().probeTimeoutMs }),
|
|
14304
16046
|
// externalDown опционален (exactOptionalPropertyTypes): ключ есть только при
|
|
14305
16047
|
// заданном SWARM_STORM_URL, иначе опускаем — не передаём undefined.
|
|
14306
16048
|
...SWARM_STORM_URL ? { externalDown: () => fetchExternalStorm(SWARM_STORM_URL) } : {}
|
|
@@ -14316,6 +16058,8 @@ async function runDaemon(opts) {
|
|
|
14316
16058
|
...heartbeat ? { heartbeat } : {},
|
|
14317
16059
|
// Сверка кода демона с main (TASWM-251): ключ есть только когда entryFile в git-репо.
|
|
14318
16060
|
...daemonVersion ? { daemonVersion } : {},
|
|
16061
|
+
// Карта репозиториев машины (TASWM-336): ключ есть только в client-режиме с непустым TTL.
|
|
16062
|
+
...repoMap ? { repoMap } : {},
|
|
14319
16063
|
// Встроенный надзор (client-режим, TASWM-194): ключ есть только когда сконфигурен.
|
|
14320
16064
|
...anomalies ? { anomalies } : {},
|
|
14321
16065
|
// Сторож прогресса роя (TASWM-270): в отличие от anomalies выше — заведён БЕЗУСЛОВНО
|
|
@@ -14444,6 +16188,17 @@ async function runDaemon(opts) {
|
|
|
14444
16188
|
);
|
|
14445
16189
|
return run ? { id: run.id } : null;
|
|
14446
16190
|
},
|
|
16191
|
+
// TASWM-351: «перезапустить с того же места» из продукта. Тот же вход, что у
|
|
16192
|
+
// `swarm wake <id>` и у POST /wake локальной морды, — сознательно, чтобы у
|
|
16193
|
+
// побудки была ОДНА реализация: она умеет отличать «сессию можно продолжить»
|
|
16194
|
+
// от «продолжать нечем» и не будит служебные прогоны вслепую (см. wakeRun).
|
|
16195
|
+
// Слаг → прогон резолвим тем же activeRunForSlug, что и остановка: у команды
|
|
16196
|
+
// из веба нет и не будет локального id прогона (он живёт только в этом процессе).
|
|
16197
|
+
wakeRun: async (slug) => {
|
|
16198
|
+
const run = db.activeRunForSlug(slug);
|
|
16199
|
+
if (!run) return null;
|
|
16200
|
+
return wakeRun({ db, isAlive: pidAlive, respawn: deps.respawn, narrate: narrator.narrate }, run.id);
|
|
16201
|
+
},
|
|
14447
16202
|
update: async (onStep) => {
|
|
14448
16203
|
if (!daemonStart) {
|
|
14449
16204
|
return { ok: false, step: "fetch", message: "\u043A\u043E\u0434 \u0434\u0435\u043C\u043E\u043D\u0430 \u043B\u0435\u0436\u0438\u0442 \u043D\u0435 \u0432 git-\u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u0442\u044C \u043D\u0435\u0447\u0435\u0433\u043E" };
|
|
@@ -14529,6 +16284,10 @@ async function runDaemon(opts) {
|
|
|
14529
16284
|
// Устаревший код демона (TASWM-251): тот же кэш, что читает heartbeat.push выше —
|
|
14530
16285
|
// локальный дашборд :7420 видит ту же сверку, что уходит в прод, без отдельного git-похода.
|
|
14531
16286
|
daemonVersion: () => daemonVersion?.get() ?? null,
|
|
16287
|
+
// Карта репозиториев машины (TASWM-336): то же состояние, что мутирует шаг тика, — его
|
|
16288
|
+
// печатает `swarm status`. Без него человек снова гадал бы, какие репозитории машина
|
|
16289
|
+
// считает своими и в каком дереве работает при двух копиях одного клона.
|
|
16290
|
+
repoMap: () => repoMapRows(repoMapState),
|
|
14532
16291
|
// Оператор-пауза + «Оживить» (TASWM-214 Фаза 2): тот же shared-флаг, что читает тик;
|
|
14533
16292
|
// narrate — операторские действия видны в ленте роя; revive — сброс залипшего шторма.
|
|
14534
16293
|
control,
|
|
@@ -14651,6 +16410,7 @@ export {
|
|
|
14651
16410
|
REVIEW_CYCLE_LIMIT,
|
|
14652
16411
|
REVIEW_ESCALATION_MARK,
|
|
14653
16412
|
REVIEW_SPAWN_LIMIT,
|
|
16413
|
+
REVIEW_UNRECOVERABLE_MARK,
|
|
14654
16414
|
SELF_REFERENCE_MARK,
|
|
14655
16415
|
STALL_FLOOR_MS,
|
|
14656
16416
|
STALL_MS,
|
|
@@ -14673,6 +16433,7 @@ export {
|
|
|
14673
16433
|
repoHasCiWorkflows,
|
|
14674
16434
|
resolveConflict,
|
|
14675
16435
|
respawnRun,
|
|
16436
|
+
reviewUnrecoverableMark,
|
|
14676
16437
|
reviveStuck,
|
|
14677
16438
|
runDaemon,
|
|
14678
16439
|
spawnReviewer,
|