@taskless-app/swarm 0.1.1 → 0.2.2
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 +15 -3
- package/dist/cli.js +2033 -301
- package/dist/overmind.js +2882 -514
- package/docs/HERO.md +3 -0
- package/package.json +23 -19
package/dist/overmind.js
CHANGED
|
@@ -8,20 +8,400 @@ var __export = (target, all) => {
|
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// ../swarm-core/src/types.ts
|
|
12
|
+
function isMilestoneKind(kind) {
|
|
13
|
+
return MILESTONE_KIND_SET.has(kind);
|
|
14
|
+
}
|
|
15
|
+
var ACTIVE_DRONE_STATUSES, MILESTONE_KINDS, MILESTONE_KIND_SET;
|
|
16
|
+
var init_types = __esm({
|
|
17
|
+
"../swarm-core/src/types.ts"() {
|
|
18
|
+
"use strict";
|
|
19
|
+
ACTIVE_DRONE_STATUSES = /* @__PURE__ */ new Set(["running", "blocked"]);
|
|
20
|
+
MILESTONE_KINDS = [
|
|
21
|
+
"spawned",
|
|
22
|
+
"pr_opened",
|
|
23
|
+
"merged",
|
|
24
|
+
"done",
|
|
25
|
+
"failed",
|
|
26
|
+
"stalled",
|
|
27
|
+
"conflict",
|
|
28
|
+
"ci_wake",
|
|
29
|
+
// CI-побудка (TASWM-102) — тоже реконсайл-решение, веха как conflict
|
|
30
|
+
"stopped",
|
|
31
|
+
"superseded",
|
|
32
|
+
"paused",
|
|
33
|
+
"resumed",
|
|
34
|
+
"storm_start",
|
|
35
|
+
"storm_end",
|
|
36
|
+
"storm_dead",
|
|
37
|
+
"anomaly"
|
|
38
|
+
// надзор overmind (TASWM-194) — та же нотификация, что улетела в инбокс, вехой в Истории
|
|
39
|
+
];
|
|
40
|
+
MILESTONE_KIND_SET = new Set(MILESTONE_KINDS);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ../swarm-core/src/timeline.ts
|
|
45
|
+
function buildSegments(created, events, laneEnd) {
|
|
46
|
+
const segs = [];
|
|
47
|
+
let cursor = created;
|
|
48
|
+
let phase = "queued";
|
|
49
|
+
const push = (to) => {
|
|
50
|
+
if (to > cursor) segs.push({ phase, from: cursor, to });
|
|
51
|
+
cursor = to;
|
|
52
|
+
};
|
|
53
|
+
for (const e of events) {
|
|
54
|
+
switch (e.kind) {
|
|
55
|
+
case "step":
|
|
56
|
+
if (phase === "queued") {
|
|
57
|
+
push(e.at);
|
|
58
|
+
phase = "running";
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
case "blocked":
|
|
62
|
+
push(e.at);
|
|
63
|
+
phase = "blocked";
|
|
64
|
+
break;
|
|
65
|
+
case "answered":
|
|
66
|
+
push(e.at);
|
|
67
|
+
phase = "running";
|
|
68
|
+
break;
|
|
69
|
+
case "pr_opened":
|
|
70
|
+
push(e.at);
|
|
71
|
+
phase = "review";
|
|
72
|
+
break;
|
|
73
|
+
case "merged":
|
|
74
|
+
case "done":
|
|
75
|
+
push(e.at);
|
|
76
|
+
phase = "done";
|
|
77
|
+
break;
|
|
78
|
+
case "failed":
|
|
79
|
+
push(e.at);
|
|
80
|
+
phase = "failed";
|
|
81
|
+
break;
|
|
82
|
+
case "stalled":
|
|
83
|
+
push(e.at);
|
|
84
|
+
phase = "stalled";
|
|
85
|
+
break;
|
|
86
|
+
case "paused":
|
|
87
|
+
case "storm_dead":
|
|
88
|
+
push(e.at);
|
|
89
|
+
phase = "paused";
|
|
90
|
+
break;
|
|
91
|
+
case "resumed":
|
|
92
|
+
push(e.at);
|
|
93
|
+
phase = "running";
|
|
94
|
+
break;
|
|
95
|
+
// TASWM-166: раньше эти два kind'а не двигали фазу — хвост дорожки
|
|
96
|
+
// дорисовывался ТЕКУЩЕЙ (до остановки) фазой, напр. "running" сплошным
|
|
97
|
+
// зелёным до самого конца — визуально неотличимо от реально идущего прогона.
|
|
98
|
+
case "stopped":
|
|
99
|
+
push(e.at);
|
|
100
|
+
phase = "stopped";
|
|
101
|
+
break;
|
|
102
|
+
case "superseded":
|
|
103
|
+
push(e.at);
|
|
104
|
+
phase = "superseded";
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
push(laneEnd);
|
|
109
|
+
return segs;
|
|
110
|
+
}
|
|
111
|
+
function computeBlocker(status, stopReason, lastStep, pr, ci, deps, openQ) {
|
|
112
|
+
switch (status) {
|
|
113
|
+
case "queued": {
|
|
114
|
+
const unmet = deps.filter((d) => !d.met);
|
|
115
|
+
if (unmet.length) {
|
|
116
|
+
const who = unmet.map((d) => d.name ? `${d.slug}\xB7${d.name}` : d.slug).join(", ");
|
|
117
|
+
return { icon: "\u26D4", text: `\u0436\u0434\u0451\u0442 ${who}`, tone: "dep" };
|
|
118
|
+
}
|
|
119
|
+
return { icon: "\u23F8", text: "\u0436\u0434\u0451\u0442 \u0441\u043B\u043E\u0442\u0430 / \u0441\u043F\u0430\u0432\u043D\u0430", tone: "queued" };
|
|
120
|
+
}
|
|
121
|
+
case "running":
|
|
122
|
+
return { icon: "\u25B6", text: lastStep ? `\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442: ${lastStep}` : "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", tone: "run" };
|
|
123
|
+
case "blocked":
|
|
124
|
+
return { icon: "\u2753", text: openQ ? `\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430: ${openQ}` : "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430", tone: "blocked" };
|
|
125
|
+
case "paused":
|
|
126
|
+
return { icon: "\u{1F4A4}", text: stopReason || "\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430", tone: "paused" };
|
|
127
|
+
case "storm_dead":
|
|
128
|
+
return { icon: "\u{1F4A4}", text: "\u0432 \u043A\u043E\u043A\u043E\u043D\u0435 \u2014 \u0448\u0442\u043E\u0440\u043C Anthropic, \u0436\u0434\u0451\u0442 \u043E\u0442\u0431\u043E\u044F", tone: "paused" };
|
|
129
|
+
case "review": {
|
|
130
|
+
if (!pr) return { icon: "\u{1F440}", text: "review \u0431\u0435\u0437 PR", tone: "review" };
|
|
131
|
+
if (!ci) return { icon: "\u23F3", text: "CI: \u0436\u0434\u0443 \u0441\u0442\u0430\u0442\u0443\u0441\u2026", tone: "review" };
|
|
132
|
+
if (ci.tone === "merge") return { icon: "\u{1F535}", text: "CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u043C\u0435\u0440\u0436\u0430", tone: "merge" };
|
|
133
|
+
return { icon: ci.tone === "fail" ? "\u274C" : "\u23F3", text: ci.text.replace(/^[^ ]+ /, ""), tone: ci.tone };
|
|
134
|
+
}
|
|
135
|
+
case "stalled":
|
|
136
|
+
return { icon: "\u26A0\uFE0F", text: stopReason || (lastStep ? `\u043C\u043E\u043B\u0447\u0438\u0442: ${lastStep}` : "\u043C\u043E\u043B\u0447\u0438\u0442"), tone: "stalled" };
|
|
137
|
+
case "failed":
|
|
138
|
+
return { icon: "\u274C", text: stopReason || "\u0443\u043F\u0430\u043B", tone: "fail" };
|
|
139
|
+
case "stopped":
|
|
140
|
+
return { icon: "\u23F9", text: stopReason || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", tone: "stopped" };
|
|
141
|
+
case "superseded":
|
|
142
|
+
return { icon: "\u21BA", text: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442", tone: "superseded" };
|
|
143
|
+
case "done":
|
|
144
|
+
return { icon: "\u2705", text: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", tone: "done" };
|
|
145
|
+
default:
|
|
146
|
+
return { icon: "\u2022", text: status, tone: "queued" };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function pickHeadline(lanes) {
|
|
150
|
+
const live = lanes.filter((l2) => l2.live && !l2.obsolete);
|
|
151
|
+
const by = (pred) => live.find(pred);
|
|
152
|
+
const fmt = (l2, extra) => `${l2.name} \xB7 ${l2.slug} \u2014 ${extra}`;
|
|
153
|
+
let l = by((x) => x.status === "review" && x.ci?.tone === "fail");
|
|
154
|
+
if (l) return { icon: "\u274C", text: fmt(l, "CI \u0443\u043F\u0430\u043B \u2014 \u0447\u0438\u043D\u0438\u0442\u044C"), tone: "fail" };
|
|
155
|
+
l = by((x) => x.status === "blocked");
|
|
156
|
+
if (l) return { icon: "\u2753", text: fmt(l, "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430"), tone: "blocked" };
|
|
157
|
+
l = by((x) => x.status === "stalled");
|
|
158
|
+
if (l) return { icon: "\u26A0\uFE0F", text: fmt(l, "stalled \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430"), tone: "stalled" };
|
|
159
|
+
l = by((x) => x.status === "review" && x.blocker.tone === "merge");
|
|
160
|
+
if (l) return { icon: "\u{1F535}", text: fmt(l, "CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u043C\u0435\u0440\u0436\u0430"), tone: "merge" };
|
|
161
|
+
l = by((x) => x.status === "queued" && x.dependsOn.some((d) => !d.met));
|
|
162
|
+
if (l) {
|
|
163
|
+
const dep = l.dependsOn.find((d) => !d.met);
|
|
164
|
+
return { icon: "\u26D4", text: fmt(l, `\u0436\u0434\u0451\u0442 ${dep.slug}${dep.name ? " \xB7 " + dep.name : ""}`), tone: "dep" };
|
|
165
|
+
}
|
|
166
|
+
l = by((x) => x.status === "review");
|
|
167
|
+
if (l) return { icon: "\u23F3", text: fmt(l, "review \u2014 CI \u0438\u0434\u0451\u0442"), tone: "review" };
|
|
168
|
+
l = by((x) => x.status === "running");
|
|
169
|
+
if (l) return { icon: "\u25B6", text: `${live.filter((x) => x.status === "running").length} \u0434\u0440\u043E\u043D\u0430 \u0432 \u0440\u0430\u0431\u043E\u0442\u0435 \u2014 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043E\u043A \u043D\u0435\u0442`, tone: "run" };
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
var DEFAULT_RUN_MS;
|
|
173
|
+
var init_timeline = __esm({
|
|
174
|
+
"../swarm-core/src/timeline.ts"() {
|
|
175
|
+
"use strict";
|
|
176
|
+
DEFAULT_RUN_MS = 25 * 6e4;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ../swarm-core/src/status.ts
|
|
181
|
+
function statusMeta(status) {
|
|
182
|
+
return STATUS_META[status] ?? FALLBACK;
|
|
183
|
+
}
|
|
184
|
+
function isTerminalStatus(status) {
|
|
185
|
+
return statusMeta(status).terminal;
|
|
186
|
+
}
|
|
187
|
+
function statusTuples() {
|
|
188
|
+
const out = {};
|
|
189
|
+
for (const [status, m] of Object.entries(STATUS_META)) {
|
|
190
|
+
out[status] = [m.pill, m.label, m.color];
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
var STATUS_META, FALLBACK, TERMINAL_STATUSES;
|
|
195
|
+
var init_status = __esm({
|
|
196
|
+
"../swarm-core/src/status.ts"() {
|
|
197
|
+
"use strict";
|
|
198
|
+
STATUS_META = {
|
|
199
|
+
queued: { pill: "queued", label: "\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438", color: "--muted-foreground", terminal: false },
|
|
200
|
+
running: { pill: "run", label: "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", color: "--primary", terminal: false },
|
|
201
|
+
blocked: { pill: "blocked", label: "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430", color: "--warning", terminal: false },
|
|
202
|
+
paused: { pill: "paused", label: "\u0437\u0430\u043C\u0435\u0440", color: "--warning", terminal: false },
|
|
203
|
+
// overmind был недоступен — ждёт пробуждения (TASWM-41)
|
|
204
|
+
review: { pill: "review", label: "\u043D\u0430 \u0440\u0435\u0432\u044C\u044E", color: "--info", terminal: false },
|
|
205
|
+
done: { pill: "done", label: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", color: "--success", terminal: true },
|
|
206
|
+
failed: { pill: "failed", label: "\u0443\u043F\u0430\u043B", color: "--destructive", terminal: true },
|
|
207
|
+
stalled: { pill: "stalled", label: "\u043F\u043E\u0433\u0438\u0431/\u0437\u0430\u0432\u0438\u0441", color: "--destructive", terminal: false },
|
|
208
|
+
// молчит/погиб, но ещё разруливается — не терминал
|
|
209
|
+
storm_dead: { pill: "storm", label: "\u0448\u0442\u043E\u0440\u043C \u2014 \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D", color: "--warning", terminal: false },
|
|
210
|
+
// умер в шторм Anthropic, поднимется после отбоя (TASWM-23)
|
|
211
|
+
stopped: { pill: "stopped", label: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", color: "--muted-foreground", terminal: true },
|
|
212
|
+
superseded: { pill: "superseded", label: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D", color: "--muted-foreground", terminal: true }
|
|
213
|
+
};
|
|
214
|
+
FALLBACK = STATUS_META.queued;
|
|
215
|
+
TERMINAL_STATUSES = Object.keys(STATUS_META).filter(
|
|
216
|
+
(s) => STATUS_META[s].terminal
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ../swarm-core/src/time.ts
|
|
222
|
+
function stamp(d = /* @__PURE__ */ new Date()) {
|
|
223
|
+
return d.toISOString().slice(0, 19).replace("T", " ");
|
|
224
|
+
}
|
|
225
|
+
function stampFromEpochSec(sec) {
|
|
226
|
+
return stamp(new Date(sec * 1e3));
|
|
227
|
+
}
|
|
228
|
+
function sqliteMs(ts) {
|
|
229
|
+
return Date.parse(ts.replace(" ", "T") + "Z");
|
|
230
|
+
}
|
|
231
|
+
function toStamp(v) {
|
|
232
|
+
if (typeof v !== "string" || !v) return null;
|
|
233
|
+
const d = new Date(v);
|
|
234
|
+
if (Number.isNaN(d.getTime())) return v.includes("T") ? v.slice(0, 19).replace("T", " ") : v;
|
|
235
|
+
return stamp(d);
|
|
236
|
+
}
|
|
237
|
+
var DAY_MS;
|
|
238
|
+
var init_time = __esm({
|
|
239
|
+
"../swarm-core/src/time.ts"() {
|
|
240
|
+
"use strict";
|
|
241
|
+
DAY_MS = 864e5;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ../swarm-core/src/windows.ts
|
|
246
|
+
function resolveRoyWindows(env = {}) {
|
|
247
|
+
return {
|
|
248
|
+
windowMs: Number(env.ROY_WINDOW_H || ROY_WINDOW_DEFAULTS.windowH) * 36e5,
|
|
249
|
+
maxTerminalLanes: Number(env.ROY_MAX_DONE || ROY_WINDOW_DEFAULTS.maxDone),
|
|
250
|
+
maxAxisMs: Number(env.ROY_AXIS_H || ROY_WINDOW_DEFAULTS.axisH) * 36e5
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
var ROY_WINDOW_DEFAULTS;
|
|
254
|
+
var init_windows = __esm({
|
|
255
|
+
"../swarm-core/src/windows.ts"() {
|
|
256
|
+
"use strict";
|
|
257
|
+
ROY_WINDOW_DEFAULTS = { windowH: 5, maxDone: 6, axisH: 6 };
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ../swarm-core/src/slug.ts
|
|
262
|
+
var init_slug = __esm({
|
|
263
|
+
"../swarm-core/src/slug.ts"() {
|
|
264
|
+
"use strict";
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ../swarm-core/src/text.ts
|
|
269
|
+
function extractText(d) {
|
|
270
|
+
if (typeof d === "string") return d;
|
|
271
|
+
const out = [];
|
|
272
|
+
const walk = (n) => {
|
|
273
|
+
if (!n || typeof n !== "object") return;
|
|
274
|
+
const node = n;
|
|
275
|
+
if (node.text) out.push(node.text);
|
|
276
|
+
(node.content ?? []).forEach(walk);
|
|
277
|
+
};
|
|
278
|
+
walk(d);
|
|
279
|
+
return out.join(" ");
|
|
280
|
+
}
|
|
281
|
+
var init_text = __esm({
|
|
282
|
+
"../swarm-core/src/text.ts"() {
|
|
283
|
+
"use strict";
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ../swarm-core/src/names.ts
|
|
288
|
+
function toRoman(n) {
|
|
289
|
+
if (n < 1 || n > 3999) return String(n);
|
|
290
|
+
const table = [
|
|
291
|
+
[1e3, "M"],
|
|
292
|
+
[900, "CM"],
|
|
293
|
+
[500, "D"],
|
|
294
|
+
[400, "CD"],
|
|
295
|
+
[100, "C"],
|
|
296
|
+
[90, "XC"],
|
|
297
|
+
[50, "L"],
|
|
298
|
+
[40, "XL"],
|
|
299
|
+
[10, "X"],
|
|
300
|
+
[9, "IX"],
|
|
301
|
+
[5, "V"],
|
|
302
|
+
[4, "IV"],
|
|
303
|
+
[1, "I"]
|
|
304
|
+
];
|
|
305
|
+
let out = "";
|
|
306
|
+
for (const [v, sym] of table) while (n >= v) out += sym, n -= v;
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
function genName(base2, g) {
|
|
310
|
+
return g <= 1 ? base2 : `${base2} ${toRoman(g)}`;
|
|
311
|
+
}
|
|
312
|
+
function baseName(name) {
|
|
313
|
+
return name.split(" ")[0];
|
|
314
|
+
}
|
|
315
|
+
var DRONE_POOL, ACTIVE_NAME_STATUSES;
|
|
316
|
+
var init_names = __esm({
|
|
317
|
+
"../swarm-core/src/names.ts"() {
|
|
318
|
+
"use strict";
|
|
319
|
+
DRONE_POOL = [
|
|
320
|
+
// Латинский бестиарий (15)
|
|
321
|
+
"SCARAB",
|
|
322
|
+
"VESPA",
|
|
323
|
+
"MANTIS",
|
|
324
|
+
"HORNET",
|
|
325
|
+
"CICADA",
|
|
326
|
+
"LOCUST",
|
|
327
|
+
"ATTA",
|
|
328
|
+
"FORMICA",
|
|
329
|
+
"MYRMEX",
|
|
330
|
+
"APIS",
|
|
331
|
+
"TERMES",
|
|
332
|
+
"GRYLLUS",
|
|
333
|
+
"LAMPYRIS",
|
|
334
|
+
"ARACHNE",
|
|
335
|
+
"SPHINX",
|
|
336
|
+
// Мифо-стаи (9)
|
|
337
|
+
"TIAMAT",
|
|
338
|
+
"JORMUNGAND",
|
|
339
|
+
"GRENDEL",
|
|
340
|
+
"TYPHON",
|
|
341
|
+
"FENRIS",
|
|
342
|
+
"GARM",
|
|
343
|
+
"KUKULKAN",
|
|
344
|
+
"LEVIATHAN",
|
|
345
|
+
"SURTUR"
|
|
346
|
+
];
|
|
347
|
+
ACTIVE_NAME_STATUSES = ["queued", "running", "blocked", "review"];
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// ../swarm-core/src/index.ts
|
|
352
|
+
var init_src = __esm({
|
|
353
|
+
"../swarm-core/src/index.ts"() {
|
|
354
|
+
"use strict";
|
|
355
|
+
init_types();
|
|
356
|
+
init_timeline();
|
|
357
|
+
init_status();
|
|
358
|
+
init_time();
|
|
359
|
+
init_windows();
|
|
360
|
+
init_slug();
|
|
361
|
+
init_text();
|
|
362
|
+
init_names();
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
11
366
|
// src/config.ts
|
|
12
367
|
var config_exports = {};
|
|
13
368
|
__export(config_exports, {
|
|
14
369
|
DB_PATH: () => DB_PATH,
|
|
15
370
|
HEARTBEAT_PATH: () => HEARTBEAT_PATH,
|
|
16
371
|
HEARTBEAT_STALE_MS: () => HEARTBEAT_STALE_MS,
|
|
372
|
+
REVIEW_CUTOFF_PATH: () => REVIEW_CUTOFF_PATH,
|
|
373
|
+
RUN_LOGS_DIR: () => RUN_LOGS_DIR,
|
|
374
|
+
SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
|
|
375
|
+
SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS: () => SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
|
|
376
|
+
SWARM_ANOMALY_OUTBOX_STUCK_MS: () => SWARM_ANOMALY_OUTBOX_STUCK_MS,
|
|
377
|
+
SWARM_ANOMALY_PR_NO_REVIEWER_MS: () => SWARM_ANOMALY_PR_NO_REVIEWER_MS,
|
|
378
|
+
SWARM_ANOMALY_PR_STUCK_MERGE_MS: () => SWARM_ANOMALY_PR_STUCK_MERGE_MS,
|
|
379
|
+
SWARM_ANOMALY_QUESTION_UNANSWERED_MS: () => SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
|
|
380
|
+
SWARM_ANOMALY_QUEUE_STALLED_MS: () => SWARM_ANOMALY_QUEUE_STALLED_MS,
|
|
381
|
+
SWARM_ANOMALY_RENOTIFY_MS: () => SWARM_ANOMALY_RENOTIFY_MS,
|
|
382
|
+
SWARM_AUTO_REVIEW: () => SWARM_AUTO_REVIEW,
|
|
17
383
|
SWARM_BACKEND: () => SWARM_BACKEND,
|
|
18
384
|
SWARM_CLAIM_INTERVAL_MS: () => SWARM_CLAIM_INTERVAL_MS,
|
|
19
385
|
SWARM_HOME: () => SWARM_HOME,
|
|
386
|
+
SWARM_MAX_FD_LOAD_FACTOR: () => SWARM_MAX_FD_LOAD_FACTOR,
|
|
20
387
|
SWARM_MAX_IN_FLIGHT: () => SWARM_MAX_IN_FLIGHT,
|
|
388
|
+
SWARM_MAX_LOAD_FACTOR: () => SWARM_MAX_LOAD_FACTOR,
|
|
389
|
+
SWARM_MIN_FREE_GB: () => SWARM_MIN_FREE_GB,
|
|
21
390
|
SWARM_OUTBOX: () => SWARM_OUTBOX,
|
|
391
|
+
SWARM_PROD_HEARTBEAT_MS: () => SWARM_PROD_HEARTBEAT_MS,
|
|
392
|
+
SWARM_REHYDRATE_MS: () => SWARM_REHYDRATE_MS,
|
|
393
|
+
SWARM_RELIEF_LOAD_FACTOR: () => SWARM_RELIEF_LOAD_FACTOR,
|
|
394
|
+
SWARM_RELIEF_MAX_FD_LOAD_FACTOR: () => SWARM_RELIEF_MAX_FD_LOAD_FACTOR,
|
|
395
|
+
SWARM_RELIEF_MIN_FREE_GB: () => SWARM_RELIEF_MIN_FREE_GB,
|
|
22
396
|
SWARM_REPO: () => SWARM_REPO,
|
|
397
|
+
SWARM_REPO_PATHS: () => SWARM_REPO_PATHS,
|
|
398
|
+
SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
|
|
399
|
+
SWARM_SHOT_TIMEOUT_MS: () => SWARM_SHOT_TIMEOUT_MS,
|
|
23
400
|
SWARM_STORM_URL: () => SWARM_STORM_URL,
|
|
401
|
+
SWARM_SWEEP_INTERVAL_MS: () => SWARM_SWEEP_INTERVAL_MS,
|
|
402
|
+
SWARM_VERIFY: () => SWARM_VERIFY,
|
|
24
403
|
TASKLESS_API: () => TASKLESS_API,
|
|
404
|
+
TASKLESS_MCP_PATH: () => TASKLESS_MCP_PATH,
|
|
25
405
|
TASKLESS_TASK_URL_BASE: () => TASKLESS_TASK_URL_BASE,
|
|
26
406
|
TASKLESS_TOKEN: () => TASKLESS_TOKEN,
|
|
27
407
|
TASKLESS_WORKSPACE: () => TASKLESS_WORKSPACE,
|
|
@@ -29,11 +409,14 @@ __export(config_exports, {
|
|
|
29
409
|
WEB_PORT: () => WEB_PORT,
|
|
30
410
|
WEB_REDIRECT_URI: () => WEB_REDIRECT_URI,
|
|
31
411
|
WORKTREES_DIR: () => WORKTREES_DIR,
|
|
32
|
-
|
|
412
|
+
droneWorktreePath: () => droneWorktreePath,
|
|
413
|
+
mcpEndpoint: () => mcpEndpoint,
|
|
414
|
+
normalizeRepo: () => normalizeRepo,
|
|
415
|
+
reviewCutoffAt: () => reviewCutoffAt
|
|
33
416
|
});
|
|
34
417
|
import { homedir } from "os";
|
|
35
|
-
import { join, resolve } from "path";
|
|
36
|
-
import { mkdirSync, readFileSync, realpathSync } from "fs";
|
|
418
|
+
import { basename, join, resolve } from "path";
|
|
419
|
+
import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "fs";
|
|
37
420
|
function normalizeRepo(repo) {
|
|
38
421
|
const r = (repo ?? "").trim();
|
|
39
422
|
if (!r) return "";
|
|
@@ -43,12 +426,20 @@ function normalizeRepo(repo) {
|
|
|
43
426
|
return resolve(r);
|
|
44
427
|
}
|
|
45
428
|
}
|
|
429
|
+
function droneWorktreePath(worktreesDir, repo, slug) {
|
|
430
|
+
return join(worktreesDir, basename(normalizeRepo(repo)), slug);
|
|
431
|
+
}
|
|
46
432
|
function envNum(name, def) {
|
|
47
433
|
const raw = process.env[name];
|
|
48
434
|
if (raw === void 0 || raw === "") return def;
|
|
49
435
|
const n = Number(raw);
|
|
50
436
|
return Number.isFinite(n) && n >= 0 ? n : def;
|
|
51
437
|
}
|
|
438
|
+
function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
|
|
439
|
+
const base2 = api.replace(/\/$/, "");
|
|
440
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
441
|
+
return `${base2}${p}`;
|
|
442
|
+
}
|
|
52
443
|
function tokenFromFile() {
|
|
53
444
|
try {
|
|
54
445
|
return readFileSync(join(SWARM_HOME, "token"), "utf8").trim();
|
|
@@ -56,60 +447,75 @@ function tokenFromFile() {
|
|
|
56
447
|
return "";
|
|
57
448
|
}
|
|
58
449
|
}
|
|
59
|
-
|
|
450
|
+
function reviewCutoffAt() {
|
|
451
|
+
try {
|
|
452
|
+
const raw = JSON.parse(readFileSync(REVIEW_CUTOFF_PATH, "utf8"));
|
|
453
|
+
if (raw.at) return raw.at;
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
const at = stamp();
|
|
457
|
+
try {
|
|
458
|
+
const tmp = `${REVIEW_CUTOFF_PATH}.tmp`;
|
|
459
|
+
writeFileSync(tmp, JSON.stringify({ at }));
|
|
460
|
+
renameSync(tmp, REVIEW_CUTOFF_PATH);
|
|
461
|
+
} catch {
|
|
462
|
+
}
|
|
463
|
+
return at;
|
|
464
|
+
}
|
|
465
|
+
var SWARM_HOME, DB_PATH, WORKTREES_DIR, RUN_LOGS_DIR, 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_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, SWARM_RELIEF_MAX_FD_LOAD_FACTOR, SWARM_REHYDRATE_MS, SWARM_PROD_HEARTBEAT_MS, SWARM_SWEEP_INTERVAL_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, SWARM_REPO_PATHS, 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_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;
|
|
60
466
|
var init_config = __esm({
|
|
61
467
|
"src/config.ts"() {
|
|
62
468
|
"use strict";
|
|
469
|
+
init_src();
|
|
63
470
|
SWARM_HOME = process.env.SWARM_HOME || join(homedir(), ".taskless-swarm");
|
|
64
471
|
mkdirSync(SWARM_HOME, { recursive: true });
|
|
65
472
|
DB_PATH = process.env.SWARM_DB || join(SWARM_HOME, "swarm.db");
|
|
66
473
|
WORKTREES_DIR = process.env.SWARM_WORKTREES || join(SWARM_HOME, "worktrees");
|
|
67
|
-
|
|
474
|
+
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join(SWARM_HOME, "run-logs");
|
|
475
|
+
DIST_BUILD = true;
|
|
476
|
+
SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
|
|
68
477
|
SWARM_OUTBOX = process.env.SWARM_OUTBOX || join(SWARM_HOME, "outbox.jsonl");
|
|
69
478
|
SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
|
|
70
479
|
SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
|
|
480
|
+
SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
|
|
481
|
+
SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
|
|
482
|
+
SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
|
|
483
|
+
SWARM_RELIEF_LOAD_FACTOR = envNum("SWARM_RELIEF_LOAD_FACTOR", 1.3);
|
|
484
|
+
SWARM_RELIEF_MIN_FREE_GB = envNum("SWARM_RELIEF_MIN_FREE_GB", 0.8);
|
|
485
|
+
SWARM_RELIEF_MAX_FD_LOAD_FACTOR = envNum("SWARM_RELIEF_MAX_FD_LOAD_FACTOR", 0.95);
|
|
486
|
+
SWARM_REHYDRATE_MS = envNum("SWARM_REHYDRATE_MS", 15e3);
|
|
487
|
+
SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
|
|
488
|
+
SWARM_SWEEP_INTERVAL_MS = envNum("SWARM_SWEEP_INTERVAL_MS", 10 * 6e4);
|
|
71
489
|
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join(SWARM_HOME, "overmind.heartbeat");
|
|
72
490
|
HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
73
491
|
SWARM_REPO = process.env.SWARM_REPO || "";
|
|
492
|
+
SWARM_REPO_PATHS = (process.env.SWARM_REPOS || SWARM_REPO).split(",").map((s) => s.trim()).filter(Boolean);
|
|
74
493
|
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
494
|
+
SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
495
|
+
TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
|
|
75
496
|
TASKLESS_TOKEN = process.env.TASKLESS_TOKEN || tokenFromFile();
|
|
76
497
|
TASKLESS_WORKSPACE = process.env.TASKLESS_WORKSPACE || "";
|
|
77
498
|
TASKLESS_WORKSPACE_SLUG = process.env.TASKLESS_WORKSPACE_SLUG || "";
|
|
78
499
|
TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
|
|
500
|
+
SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
|
|
501
|
+
SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
|
|
502
|
+
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join(SWARM_HOME, "review-cutoff.json");
|
|
503
|
+
SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
|
|
79
504
|
SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
|
|
80
505
|
WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
|
|
81
506
|
WEB_REDIRECT_URI = process.env.SWARM_REDIRECT_URI || `http://127.0.0.1:${WEB_PORT}/callback`;
|
|
507
|
+
SWARM_ANOMALY_RENOTIFY_MS = envNum("SWARM_ANOMALY_RENOTIFY_MS", 60 * 6e4);
|
|
508
|
+
SWARM_ANOMALY_FAILED_SPIKE_COUNT = Math.trunc(envNum("SWARM_ANOMALY_FAILED_SPIKE_COUNT", 3));
|
|
509
|
+
SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS = envNum("SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS", 30 * 6e4);
|
|
510
|
+
SWARM_ANOMALY_QUEUE_STALLED_MS = envNum("SWARM_ANOMALY_QUEUE_STALLED_MS", 10 * 6e4);
|
|
511
|
+
SWARM_ANOMALY_PR_STUCK_MERGE_MS = envNum("SWARM_ANOMALY_PR_STUCK_MERGE_MS", 30 * 6e4);
|
|
512
|
+
SWARM_ANOMALY_PR_NO_REVIEWER_MS = envNum("SWARM_ANOMALY_PR_NO_REVIEWER_MS", 20 * 6e4);
|
|
513
|
+
SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
|
|
514
|
+
SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
|
|
82
515
|
}
|
|
83
516
|
});
|
|
84
517
|
|
|
85
518
|
// src/names.ts
|
|
86
|
-
function toRoman(n) {
|
|
87
|
-
if (n < 1 || n > 3999) return String(n);
|
|
88
|
-
const table = [
|
|
89
|
-
[1e3, "M"],
|
|
90
|
-
[900, "CM"],
|
|
91
|
-
[500, "D"],
|
|
92
|
-
[400, "CD"],
|
|
93
|
-
[100, "C"],
|
|
94
|
-
[90, "XC"],
|
|
95
|
-
[50, "L"],
|
|
96
|
-
[40, "XL"],
|
|
97
|
-
[10, "X"],
|
|
98
|
-
[9, "IX"],
|
|
99
|
-
[5, "V"],
|
|
100
|
-
[4, "IV"],
|
|
101
|
-
[1, "I"]
|
|
102
|
-
];
|
|
103
|
-
let out = "";
|
|
104
|
-
for (const [v, sym] of table) while (n >= v) out += sym, n -= v;
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
function genName(base2, g) {
|
|
108
|
-
return g <= 1 ? base2 : `${base2} ${toRoman(g)}`;
|
|
109
|
-
}
|
|
110
|
-
function baseName(name) {
|
|
111
|
-
return name.split(" ")[0];
|
|
112
|
-
}
|
|
113
519
|
function pickName(rows) {
|
|
114
520
|
const live = /* @__PURE__ */ new Set();
|
|
115
521
|
const lastUse = /* @__PURE__ */ new Map();
|
|
@@ -129,39 +535,12 @@ function pickName(rows) {
|
|
|
129
535
|
}
|
|
130
536
|
}
|
|
131
537
|
}
|
|
132
|
-
var
|
|
133
|
-
var
|
|
538
|
+
var NAME_LIVE;
|
|
539
|
+
var init_names2 = __esm({
|
|
134
540
|
"src/names.ts"() {
|
|
135
541
|
"use strict";
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"SCARAB",
|
|
139
|
-
"VESPA",
|
|
140
|
-
"MANTIS",
|
|
141
|
-
"HORNET",
|
|
142
|
-
"CICADA",
|
|
143
|
-
"LOCUST",
|
|
144
|
-
"ATTA",
|
|
145
|
-
"FORMICA",
|
|
146
|
-
"MYRMEX",
|
|
147
|
-
"APIS",
|
|
148
|
-
"TERMES",
|
|
149
|
-
"GRYLLUS",
|
|
150
|
-
"LAMPYRIS",
|
|
151
|
-
"ARACHNE",
|
|
152
|
-
"SPHINX",
|
|
153
|
-
// Мифо-стаи (9)
|
|
154
|
-
"TIAMAT",
|
|
155
|
-
"JORMUNGAND",
|
|
156
|
-
"GRENDEL",
|
|
157
|
-
"TYPHON",
|
|
158
|
-
"FENRIS",
|
|
159
|
-
"GARM",
|
|
160
|
-
"KUKULKAN",
|
|
161
|
-
"LEVIATHAN",
|
|
162
|
-
"SURTUR"
|
|
163
|
-
];
|
|
164
|
-
NAME_LIVE = /* @__PURE__ */ new Set(["queued", "running", "blocked", "review"]);
|
|
542
|
+
init_src();
|
|
543
|
+
NAME_LIVE = new Set(ACTIVE_NAME_STATUSES);
|
|
165
544
|
}
|
|
166
545
|
});
|
|
167
546
|
|
|
@@ -169,9 +548,6 @@ var init_names = __esm({
|
|
|
169
548
|
function resolveStormConfig(cfg) {
|
|
170
549
|
return { ...DEFAULT_STORM_CONFIG, ...cfg ?? {} };
|
|
171
550
|
}
|
|
172
|
-
function sqliteMs(ts) {
|
|
173
|
-
return Date.parse(ts.replace(" ", "T") + "Z");
|
|
174
|
-
}
|
|
175
551
|
function encodeProbe(rec) {
|
|
176
552
|
return JSON.stringify(rec);
|
|
177
553
|
}
|
|
@@ -228,6 +604,7 @@ var numEnv, DEFAULT_STORM_CONFIG, STORM_START_TEXT, STORM_END_TEXT, STORM_DEAD_T
|
|
|
228
604
|
var init_storm = __esm({
|
|
229
605
|
"src/storm.ts"() {
|
|
230
606
|
"use strict";
|
|
607
|
+
init_src();
|
|
231
608
|
numEnv = (name, def) => {
|
|
232
609
|
const v = Number(process.env[name]);
|
|
233
610
|
return Number.isFinite(v) && v > 0 ? v : def;
|
|
@@ -253,7 +630,7 @@ var db_exports = {};
|
|
|
253
630
|
__export(db_exports, {
|
|
254
631
|
SwarmDb: () => SwarmDb
|
|
255
632
|
});
|
|
256
|
-
import
|
|
633
|
+
import { createRequire } from "module";
|
|
257
634
|
import { existsSync } from "fs";
|
|
258
635
|
import { resolve as resolve2 } from "path";
|
|
259
636
|
function rowToQuestion(row) {
|
|
@@ -295,6 +672,8 @@ function rowToRun(row) {
|
|
|
295
672
|
respawns: row.respawns ?? 0,
|
|
296
673
|
// старые прогоны без колонки → 0
|
|
297
674
|
resolves_run_id: row.resolves_run_id ?? null,
|
|
675
|
+
reviews_run_id: row.reviews_run_id ?? null,
|
|
676
|
+
review_verdict: row.review_verdict ?? null,
|
|
298
677
|
task_title: row.task_title ?? null,
|
|
299
678
|
task_desc: row.task_desc ?? null,
|
|
300
679
|
task_url: row.task_url ?? null,
|
|
@@ -302,13 +681,26 @@ function rowToRun(row) {
|
|
|
302
681
|
updated_at: row.updated_at
|
|
303
682
|
};
|
|
304
683
|
}
|
|
305
|
-
|
|
684
|
+
function loadBetterSqlite3() {
|
|
685
|
+
const require2 = createRequire(import.meta.url);
|
|
686
|
+
try {
|
|
687
|
+
return require2("better-sqlite3");
|
|
688
|
+
} catch (err) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`better-sqlite3 \u043D\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D. Sqlite-\u0440\u0435\u0436\u0438\u043C (SwarmDb) \u2014 \u0434\u0435\u0444\u043E\u043B\u0442 \u043C\u043E\u043D\u043E\u0440\u0435\u043F\u044B/\u0442\u0435\u0441\u0442\u043E\u0432; \u043E\u043F\u0443\u0431\u043B\u0438\u043A\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u0432 client-\u0440\u0435\u0436\u0438\u043C\u0435 \u0438 sqlite \u043D\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442. \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u0435 optional dependency (npm i, \u0431\u0435\u0437 --omit=optional) \u0438\u043B\u0438 \u0437\u0430\u0434\u0430\u0439\u0442\u0435 SWARM_BACKEND=client. \u0418\u0441\u0445\u043E\u0434\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430: ${err.message}`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
var TERMINAL_SQL, RESOLVER_TERMINAL_SQL, SCHEMA, SwarmDb;
|
|
306
695
|
var init_db = __esm({
|
|
307
696
|
"src/db.ts"() {
|
|
308
697
|
"use strict";
|
|
309
698
|
init_config();
|
|
310
|
-
|
|
699
|
+
init_names2();
|
|
311
700
|
init_storm();
|
|
701
|
+
init_src();
|
|
702
|
+
TERMINAL_SQL = TERMINAL_STATUSES.map((s) => `'${s}'`).join(",");
|
|
703
|
+
RESOLVER_TERMINAL_SQL = [...TERMINAL_STATUSES, "stalled"].map((s) => `'${s}'`).join(",");
|
|
312
704
|
SCHEMA = `
|
|
313
705
|
CREATE TABLE IF NOT EXISTS runs (
|
|
314
706
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -331,6 +723,8 @@ CREATE TABLE IF NOT EXISTS runs (
|
|
|
331
723
|
stop_reason TEXT,
|
|
332
724
|
respawns INTEGER NOT NULL DEFAULT 0,
|
|
333
725
|
resolves_run_id INTEGER,
|
|
726
|
+
reviews_run_id INTEGER,
|
|
727
|
+
review_verdict TEXT,
|
|
334
728
|
task_title TEXT,
|
|
335
729
|
task_desc TEXT,
|
|
336
730
|
task_url TEXT,
|
|
@@ -372,9 +766,12 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
372
766
|
SwarmDb = class {
|
|
373
767
|
db;
|
|
374
768
|
defaultRepo;
|
|
769
|
+
databaseCtor;
|
|
770
|
+
// для importDb (второй Database, source)
|
|
375
771
|
constructor(path = "swarm.db", defaultRepo = "") {
|
|
376
772
|
this.defaultRepo = normalizeRepo(defaultRepo);
|
|
377
|
-
this.
|
|
773
|
+
this.databaseCtor = loadBetterSqlite3();
|
|
774
|
+
this.db = new this.databaseCtor(path);
|
|
378
775
|
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
379
776
|
this.db.exec(SCHEMA);
|
|
380
777
|
for (const alter of [
|
|
@@ -395,7 +792,11 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
395
792
|
// Иерархия роя (TASWM-78): роль прогона (drone|hero) и ссылка на прогон-героя.
|
|
396
793
|
"ALTER TABLE runs ADD COLUMN kind TEXT NOT NULL DEFAULT 'drone'",
|
|
397
794
|
"ALTER TABLE runs ADD COLUMN hero_run_id INTEGER",
|
|
398
|
-
"ALTER TABLE events ADD COLUMN actor TEXT"
|
|
795
|
+
"ALTER TABLE events ADD COLUMN actor TEXT",
|
|
796
|
+
// Авто-ревью (TASWM-148): прогон-ревьюер (reviews_run_id → review-прогон) и
|
|
797
|
+
// вердикт (approve|changes), который ревьюер пишет на проверяемый прогон.
|
|
798
|
+
"ALTER TABLE runs ADD COLUMN reviews_run_id INTEGER",
|
|
799
|
+
"ALTER TABLE runs ADD COLUMN review_verdict TEXT"
|
|
399
800
|
]) {
|
|
400
801
|
try {
|
|
401
802
|
this.db.exec(alter);
|
|
@@ -415,7 +816,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
415
816
|
createRun(input) {
|
|
416
817
|
const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
|
|
417
818
|
const res = this.db.prepare(
|
|
418
|
-
"INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, decision, resolves_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
819
|
+
"INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, decision, resolves_run_id, reviews_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
419
820
|
).run(
|
|
420
821
|
input.task_slug,
|
|
421
822
|
name,
|
|
@@ -429,10 +830,11 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
429
830
|
input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
|
|
430
831
|
input.model ?? null,
|
|
431
832
|
input.decision ? JSON.stringify(input.decision) : null,
|
|
432
|
-
input.resolves_run_id ?? null
|
|
833
|
+
input.resolves_run_id ?? null,
|
|
834
|
+
input.reviews_run_id ?? null
|
|
433
835
|
);
|
|
434
836
|
const id = Number(res.lastInsertRowid);
|
|
435
|
-
const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
|
|
837
|
+
const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.reviews_run_id ? `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u043B\u044F #${input.reviews_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
|
|
436
838
|
this.addEvent(id, "spawned", spawnNote);
|
|
437
839
|
return this.getRun(id);
|
|
438
840
|
}
|
|
@@ -485,7 +887,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
485
887
|
*/
|
|
486
888
|
activeResolverFor(reviewRunId) {
|
|
487
889
|
const row = this.db.prepare(
|
|
488
|
-
|
|
890
|
+
`SELECT * FROM runs WHERE resolves_run_id = ? AND status NOT IN (${RESOLVER_TERMINAL_SQL}) ORDER BY id DESC LIMIT 1`
|
|
489
891
|
).get(reviewRunId);
|
|
490
892
|
return row ? rowToRun(row) : null;
|
|
491
893
|
}
|
|
@@ -494,10 +896,48 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
494
896
|
const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE resolves_run_id = ?").get(reviewRunId);
|
|
495
897
|
return row.n;
|
|
496
898
|
}
|
|
899
|
+
/**
|
|
900
|
+
* Сколько ЖИВЫХ/УСПЕШНЫХ ревьюеров уже поднимали на этот review-PR (авто-ревью, TASWM-148) —
|
|
901
|
+
* one-shot идемпотентность БЕЗ лимита попыток (в отличие от резолвера): >0 → повторно не
|
|
902
|
+
* спавним. failed/stalled ревьюеры (worktree/launch сорвались ДО вердикта) в счёт НЕ идут —
|
|
903
|
+
* иначе один транзиентный сбой спавна (диск/git/gh) навсегда блокировал бы ревью этого PR без
|
|
904
|
+
* единого шанса на повтор — тихо нарушая политику владельца «каждый PR ревьюится».
|
|
905
|
+
*/
|
|
906
|
+
reviewerCountFor(reviewRunId) {
|
|
907
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ? AND status NOT IN ('failed','stalled')").get(reviewRunId);
|
|
908
|
+
return row.n;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Жёсткий кап ревью-спавнов (TASWM-204, карусель мёртвых ревьюеров): сколько ревьюеров
|
|
912
|
+
* ВСЕГО (любой статус, включая failed/stalled — специально в отличие от reviewerCountFor
|
|
913
|
+
* выше) когда-либо поднималось на review-прогон #reviewRunId. Зеркалит resolverCountFor —
|
|
914
|
+
* плоский потолок попыток. Именно это ловит карусель: reviewerCountFor исключает
|
|
915
|
+
* failed/stalled (чтобы ОДИН транзиентный сбой не блокировал ревью навсегда), но тем
|
|
916
|
+
* самым допускает бесконечный ретрай, если ревьюер стабильно умирает без done().
|
|
917
|
+
*/
|
|
918
|
+
reviewerAttemptCountFor(reviewRunId) {
|
|
919
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ?").get(reviewRunId);
|
|
920
|
+
return row.n;
|
|
921
|
+
}
|
|
922
|
+
/** Последняя (любой статус) попытка ревьюера на review-прогон #reviewRunId (TASWM-196) —
|
|
923
|
+
* см. store.ts:lastReviewerAttemptFor. */
|
|
924
|
+
lastReviewerAttemptFor(reviewRunId) {
|
|
925
|
+
const row = this.db.prepare("SELECT * FROM runs WHERE reviews_run_id = ? ORDER BY id DESC LIMIT 1").get(reviewRunId);
|
|
926
|
+
return row ? rowToRun(row) : null;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Живые/ждущие ревьюер-прогоны review-прогона #reviewRunId (TASWM-204): для
|
|
930
|
+
* closeReviewersOnMerge — PR смержен раньше вердикта, зависшего ревьюера пора закрыть,
|
|
931
|
+
* не дожидаясь done(). Точечный WHERE, не полный скан listRuns() (code-review TASWM-204).
|
|
932
|
+
*/
|
|
933
|
+
activeReviewersFor(reviewRunId) {
|
|
934
|
+
const rows = this.db.prepare(`SELECT * FROM runs WHERE reviews_run_id = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC`).all(reviewRunId);
|
|
935
|
+
return rows.map(rowToRun);
|
|
936
|
+
}
|
|
497
937
|
/** Последний живой (не терминальный) прогон по slug — для swarm cancel. */
|
|
498
938
|
activeRunForSlug(slug) {
|
|
499
939
|
const row = this.db.prepare(
|
|
500
|
-
|
|
940
|
+
`SELECT * FROM runs WHERE task_slug = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC LIMIT 1`
|
|
501
941
|
).get(slug);
|
|
502
942
|
return row ? rowToRun(row) : null;
|
|
503
943
|
}
|
|
@@ -554,7 +994,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
554
994
|
const already = this.db.prepare("SELECT run_count FROM imports WHERE source = ?").get(abs);
|
|
555
995
|
if (already) return { imported: false, runs: already.run_count };
|
|
556
996
|
if (!existsSync(abs)) throw new Error(`import-db: \u0444\u0430\u0439\u043B\u0430 \u043D\u0435\u0442: ${abs}`);
|
|
557
|
-
const src = new
|
|
997
|
+
const src = new this.databaseCtor(abs);
|
|
558
998
|
try {
|
|
559
999
|
const runs = src.prepare("SELECT * FROM runs ORDER BY id").all();
|
|
560
1000
|
const events = src.prepare("SELECT * FROM events ORDER BY id").all();
|
|
@@ -729,11 +1169,10 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
729
1169
|
taskUrl: row.taskUrl ?? null
|
|
730
1170
|
});
|
|
731
1171
|
}
|
|
732
|
-
const MILESTONES2 = /* @__PURE__ */ new Set(["spawned", "pr_opened", "merged", "done", "failed", "stalled", "conflict", "stopped", "superseded", "paused", "resumed", "storm_start", "storm_end", "storm_dead"]);
|
|
733
1172
|
for (const row of this.db.prepare(
|
|
734
1173
|
"SELECT e.*, r.task_slug slug, r.name name, r.repo repo, r.task_title title, r.pr_url pr, r.task_url taskUrl FROM events e LEFT JOIN runs r ON r.id = e.run_id"
|
|
735
1174
|
).all()) {
|
|
736
|
-
if (!
|
|
1175
|
+
if (!isMilestoneKind(row.kind)) continue;
|
|
737
1176
|
const global = row.run_id == null;
|
|
738
1177
|
items.push({
|
|
739
1178
|
kind: "milestone",
|
|
@@ -794,12 +1233,13 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
794
1233
|
return row.n;
|
|
795
1234
|
}
|
|
796
1235
|
/**
|
|
797
|
-
* Спавн-фейлов (событий failed на
|
|
798
|
-
* Резолверные
|
|
1236
|
+
* Спавн-фейлов (событий failed на НЕ-резолверных/НЕ-ревьюерных прогонах) с момента
|
|
1237
|
+
* cutoff. Резолверные (merge-main-in) и ревьюерные (авто-ревью, TASWM-148) failed не
|
|
1238
|
+
* считаем штормовым сигналом.
|
|
799
1239
|
*/
|
|
800
1240
|
spawnFailCountSince(cutoffSeconds) {
|
|
801
1241
|
const row = this.db.prepare(
|
|
802
|
-
"SELECT COUNT(*) n FROM events e JOIN runs r ON r.id = e.run_id WHERE e.kind='failed' AND r.resolves_run_id IS NULL AND e.at >= datetime(?, 'unixepoch')"
|
|
1242
|
+
"SELECT COUNT(*) n FROM events e JOIN runs r ON r.id = e.run_id WHERE e.kind='failed' AND r.resolves_run_id IS NULL AND r.reviews_run_id IS NULL AND e.at >= datetime(?, 'unixepoch')"
|
|
803
1243
|
).get(cutoffSeconds);
|
|
804
1244
|
return row.n;
|
|
805
1245
|
}
|
|
@@ -823,7 +1263,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
823
1263
|
|
|
824
1264
|
// src/oauth.ts
|
|
825
1265
|
import { createHash, randomBytes } from "crypto";
|
|
826
|
-
import { readFileSync as
|
|
1266
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync } from "fs";
|
|
827
1267
|
import { join as join4 } from "path";
|
|
828
1268
|
function makePkce() {
|
|
829
1269
|
const verifier = randomBytes(48).toString("base64url");
|
|
@@ -908,13 +1348,13 @@ function toStore(clientId, t, now) {
|
|
|
908
1348
|
}
|
|
909
1349
|
function loadStore() {
|
|
910
1350
|
try {
|
|
911
|
-
return JSON.parse(
|
|
1351
|
+
return JSON.parse(readFileSync6(STORE_PATH, "utf8"));
|
|
912
1352
|
} catch {
|
|
913
1353
|
return null;
|
|
914
1354
|
}
|
|
915
1355
|
}
|
|
916
1356
|
function saveStore(s) {
|
|
917
|
-
|
|
1357
|
+
writeFileSync5(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
|
|
918
1358
|
}
|
|
919
1359
|
function clearStore() {
|
|
920
1360
|
try {
|
|
@@ -924,7 +1364,7 @@ function clearStore() {
|
|
|
924
1364
|
}
|
|
925
1365
|
function loadClientId() {
|
|
926
1366
|
try {
|
|
927
|
-
return JSON.parse(
|
|
1367
|
+
return JSON.parse(readFileSync6(CLIENT_PATH, "utf8")).clientId;
|
|
928
1368
|
} catch {
|
|
929
1369
|
return null;
|
|
930
1370
|
}
|
|
@@ -933,7 +1373,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
|
|
|
933
1373
|
const existing = loadClientId();
|
|
934
1374
|
if (existing) return existing;
|
|
935
1375
|
const clientId = await registerClient({ api, redirectUri }, fetchFn);
|
|
936
|
-
|
|
1376
|
+
writeFileSync5(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
|
|
937
1377
|
return clientId;
|
|
938
1378
|
}
|
|
939
1379
|
async function ensureAccess(store, opts = {}) {
|
|
@@ -966,11 +1406,11 @@ var init_oauth = __esm({
|
|
|
966
1406
|
});
|
|
967
1407
|
|
|
968
1408
|
// src/workspace.ts
|
|
969
|
-
import { readFileSync as
|
|
1409
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2 } from "fs";
|
|
970
1410
|
import { join as join5 } from "path";
|
|
971
1411
|
function loadWorkspace() {
|
|
972
1412
|
try {
|
|
973
|
-
const w = JSON.parse(
|
|
1413
|
+
const w = JSON.parse(readFileSync7(WORKSPACE_PATH, "utf8"));
|
|
974
1414
|
return w && typeof w.id === "string" && w.id ? w : null;
|
|
975
1415
|
} catch {
|
|
976
1416
|
return null;
|
|
@@ -991,9 +1431,14 @@ var init_workspace = __esm({
|
|
|
991
1431
|
// src/taskless.ts
|
|
992
1432
|
var taskless_exports = {};
|
|
993
1433
|
__export(taskless_exports, {
|
|
1434
|
+
TASK_CONTEXT_MAX_COMMENTS: () => TASK_CONTEXT_MAX_COMMENTS,
|
|
1435
|
+
TASK_CONTEXT_MAX_COMMENT_CHARS: () => TASK_CONTEXT_MAX_COMMENT_CHARS,
|
|
1436
|
+
TASK_CONTEXT_MAX_DOCS: () => TASK_CONTEXT_MAX_DOCS,
|
|
1437
|
+
TASK_CONTEXT_MAX_DOC_CHARS: () => TASK_CONTEXT_MAX_DOC_CHARS,
|
|
994
1438
|
connectSwarmMcp: () => connectSwarmMcp,
|
|
995
1439
|
connectTaskless: () => connectTaskless,
|
|
996
1440
|
extractText: () => extractText,
|
|
1441
|
+
fetchTaskContext: () => fetchTaskContext,
|
|
997
1442
|
makeTasklessClient: () => makeTasklessClient
|
|
998
1443
|
});
|
|
999
1444
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
@@ -1021,12 +1466,25 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
|
|
|
1021
1466
|
description: extractText(task.description),
|
|
1022
1467
|
url: taskUrlBase ? `${taskUrlBase}/tasks/${slug}` : null,
|
|
1023
1468
|
parentId: task.parentId ?? null,
|
|
1024
|
-
|
|
1469
|
+
projectId: task.projectId ?? null,
|
|
1470
|
+
statusCategory: task.status?.category ?? null,
|
|
1471
|
+
comments: (task.comments ?? []).map((c) => ({
|
|
1472
|
+
author: c.author?.displayName ?? "\u2014",
|
|
1473
|
+
createdAt: (c.createdAt ?? "").slice(0, 16).replace("T", " "),
|
|
1474
|
+
text: extractText(c.body)
|
|
1475
|
+
})),
|
|
1476
|
+
documents: (task.documents ?? []).filter((d) => !!d.id).map((d) => ({ id: d.id, title: d.title ?? d.id, url: d.url ?? null }))
|
|
1025
1477
|
};
|
|
1026
1478
|
idCache.set(slug, body.id);
|
|
1027
1479
|
idCache.set(body.id, body.id);
|
|
1028
1480
|
return body;
|
|
1029
1481
|
}
|
|
1482
|
+
async function getDocument(id) {
|
|
1483
|
+
const res = await safeCall("get_document", { id });
|
|
1484
|
+
const doc = res?.structuredContent?.document ?? null;
|
|
1485
|
+
if (!doc) return null;
|
|
1486
|
+
return { title: doc.title ?? "", body: extractText(doc.body) };
|
|
1487
|
+
}
|
|
1030
1488
|
async function resolveId(idOrSlug) {
|
|
1031
1489
|
const cached = idCache.get(idOrSlug);
|
|
1032
1490
|
if (cached) return cached;
|
|
@@ -1056,13 +1514,18 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
|
|
|
1056
1514
|
if (id) await safeCall("add_comment", { taskId: id, body });
|
|
1057
1515
|
},
|
|
1058
1516
|
backlogStatusId,
|
|
1517
|
+
getDocument,
|
|
1059
1518
|
createTask: async (input) => {
|
|
1519
|
+
if (!input.projectId) {
|
|
1520
|
+
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)");
|
|
1521
|
+
}
|
|
1060
1522
|
const res = await safeCall("create_task", {
|
|
1061
|
-
parentId: input.parentId,
|
|
1523
|
+
...input.parentId ? { parentId: input.parentId } : {},
|
|
1062
1524
|
title: input.title,
|
|
1063
1525
|
description: input.description,
|
|
1064
|
-
estimateHours: input.estimateHours,
|
|
1065
|
-
statusId: input.statusId,
|
|
1526
|
+
...input.estimateHours != null ? { estimateHours: input.estimateHours } : {},
|
|
1527
|
+
...input.statusId ? { statusId: input.statusId } : {},
|
|
1528
|
+
projectId: input.projectId,
|
|
1066
1529
|
force: true
|
|
1067
1530
|
});
|
|
1068
1531
|
const task = res?.structuredContent?.task ?? null;
|
|
@@ -1074,6 +1537,44 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
|
|
|
1074
1537
|
}
|
|
1075
1538
|
};
|
|
1076
1539
|
}
|
|
1540
|
+
async function fetchTaskContext(client, task) {
|
|
1541
|
+
const parts = [];
|
|
1542
|
+
const comments = task.comments ?? [];
|
|
1543
|
+
if (comments.length > 0) {
|
|
1544
|
+
const tail = comments.slice(-TASK_CONTEXT_MAX_COMMENTS);
|
|
1545
|
+
const skipped = comments.length - tail.length;
|
|
1546
|
+
const lines = tail.map((c) => {
|
|
1547
|
+
const cut = c.text.length > TASK_CONTEXT_MAX_COMMENT_CHARS;
|
|
1548
|
+
const text = cut ? `${c.text.slice(0, TASK_CONTEXT_MAX_COMMENT_CHARS)}\u2026 (\u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E)` : c.text;
|
|
1549
|
+
return `\u2014 [${c.createdAt} \xB7 ${c.author}] ${text}`;
|
|
1550
|
+
});
|
|
1551
|
+
const header = skipped > 0 ? `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}, \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 ${tail.length} \u2014 \u0441\u0430\u043C\u044B\u0435 \u0441\u0432\u0435\u0436\u0438\u0435):` : `\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 (${comments.length}):`;
|
|
1552
|
+
parts.push([header, ...lines].join("\n"));
|
|
1553
|
+
}
|
|
1554
|
+
const documents = task.documents ?? [];
|
|
1555
|
+
if (documents.length > 0) {
|
|
1556
|
+
const head = documents.slice(0, TASK_CONTEXT_MAX_DOCS);
|
|
1557
|
+
const rest = documents.slice(TASK_CONTEXT_MAX_DOCS);
|
|
1558
|
+
const renderedHead = await Promise.all(
|
|
1559
|
+
head.map(async (d) => {
|
|
1560
|
+
const doc = await client.getDocument(d.id);
|
|
1561
|
+
if (!doc) return `### ${d.title}${d.url ? ` (${d.url})` : ""}
|
|
1562
|
+
(\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442)`;
|
|
1563
|
+
const cut = doc.body.length > TASK_CONTEXT_MAX_DOC_CHARS;
|
|
1564
|
+
const text = cut ? `${doc.body.slice(0, TASK_CONTEXT_MAX_DOC_CHARS)}\u2026` : doc.body;
|
|
1565
|
+
return `### ${d.title}${d.url ? ` (${d.url})` : ""}${cut ? " \u2014 \u043E\u0431\u0440\u0435\u0437\u0430\u043D\u043E, \u043F\u043E\u043B\u043D\u044B\u0439 \u0442\u0435\u043A\u0441\u0442 \u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435/get_document" : ""}
|
|
1566
|
+
${text || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
|
1567
|
+
})
|
|
1568
|
+
);
|
|
1569
|
+
const restLines = rest.map((d) => `- ${d.title}${d.url ? ` (${d.url})` : ""} \u2014 \u0442\u0435\u043B\u043E \u043D\u0435 \u043F\u043E\u0434\u0442\u044F\u043D\u0443\u0442\u043E, \u0447\u0438\u0442\u0430\u0439 \u0447\u0435\u0440\u0435\u0437 get_document/read_task`);
|
|
1570
|
+
parts.push(
|
|
1571
|
+
[`\u041F\u0440\u0438\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0435 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B (${documents.length}):`, ...renderedHead, ...restLines.length ? [restLines.join("\n")] : []].join(
|
|
1572
|
+
"\n\n"
|
|
1573
|
+
)
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
return parts.join("\n\n");
|
|
1577
|
+
}
|
|
1077
1578
|
function parseJsonContent(res) {
|
|
1078
1579
|
const text = res?.content?.find((c) => c.type === "text")?.text;
|
|
1079
1580
|
if (!text) return null;
|
|
@@ -1100,13 +1601,13 @@ async function connectTaskless(cfg) {
|
|
|
1100
1601
|
}
|
|
1101
1602
|
if (!token) token = TASKLESS_TOKEN;
|
|
1102
1603
|
if (!token) return null;
|
|
1103
|
-
const url = new URL(
|
|
1604
|
+
const url = new URL(mcpEndpoint(api));
|
|
1104
1605
|
const transport = new StreamableHTTPClientTransport(url, {
|
|
1105
1606
|
requestInit: { headers: { Authorization: `Bearer ${token}` } }
|
|
1106
1607
|
});
|
|
1107
1608
|
const client = new Client({ name: "taskless-swarm", version: "0.1.0" }, { capabilities: {} });
|
|
1108
1609
|
try {
|
|
1109
|
-
await client.connect(transport);
|
|
1610
|
+
await client.connect(transport, { timeout: TASKLESS_CONNECT_TIMEOUT_MS });
|
|
1110
1611
|
} catch (e) {
|
|
1111
1612
|
console.warn(`Taskless: \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043D\u0435 \u0432\u044B\u0448\u043B\u043E (${e instanceof Error ? e.message : e}) \u2192 slug-only. \u0412\u043E\u0439\u0434\u0438 \u0437\u0430\u043D\u043E\u0432\u043E.`);
|
|
1112
1613
|
return null;
|
|
@@ -1131,7 +1632,7 @@ async function connectSwarmMcp(cfg) {
|
|
|
1131
1632
|
}
|
|
1132
1633
|
if (!token) token = TASKLESS_TOKEN;
|
|
1133
1634
|
if (!token) return null;
|
|
1134
|
-
const url = new URL(
|
|
1635
|
+
const url = new URL(mcpEndpoint(api));
|
|
1135
1636
|
const transport = new StreamableHTTPClientTransport(url, {
|
|
1136
1637
|
requestInit: { headers: { Authorization: `Bearer ${token}` } }
|
|
1137
1638
|
});
|
|
@@ -1144,71 +1645,158 @@ async function connectSwarmMcp(cfg) {
|
|
|
1144
1645
|
}
|
|
1145
1646
|
return async (name, args) => await client.callTool({ name, arguments: { ...args, ...ws } });
|
|
1146
1647
|
}
|
|
1147
|
-
|
|
1148
|
-
if (typeof d === "string") return d;
|
|
1149
|
-
const out = [];
|
|
1150
|
-
const walk = (n) => {
|
|
1151
|
-
if (!n || typeof n !== "object") return;
|
|
1152
|
-
const node = n;
|
|
1153
|
-
if (node.text) out.push(node.text);
|
|
1154
|
-
(node.content ?? []).forEach(walk);
|
|
1155
|
-
};
|
|
1156
|
-
walk(d);
|
|
1157
|
-
return out.join(" ");
|
|
1158
|
-
}
|
|
1159
|
-
var SLUG_RE;
|
|
1648
|
+
var SLUG_RE, TASK_CONTEXT_MAX_DOCS, TASK_CONTEXT_MAX_DOC_CHARS, TASK_CONTEXT_MAX_COMMENTS, TASK_CONTEXT_MAX_COMMENT_CHARS, TASKLESS_CONNECT_TIMEOUT_MS;
|
|
1160
1649
|
var init_taskless = __esm({
|
|
1161
1650
|
"src/taskless.ts"() {
|
|
1162
1651
|
"use strict";
|
|
1652
|
+
init_src();
|
|
1163
1653
|
init_config();
|
|
1164
1654
|
init_oauth();
|
|
1165
1655
|
init_workspace();
|
|
1166
1656
|
SLUG_RE = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
|
|
1657
|
+
TASK_CONTEXT_MAX_DOCS = 5;
|
|
1658
|
+
TASK_CONTEXT_MAX_DOC_CHARS = 4e3;
|
|
1659
|
+
TASK_CONTEXT_MAX_COMMENTS = 30;
|
|
1660
|
+
TASK_CONTEXT_MAX_COMMENT_CHARS = 2e3;
|
|
1661
|
+
TASKLESS_CONNECT_TIMEOUT_MS = 1e4;
|
|
1167
1662
|
}
|
|
1168
1663
|
});
|
|
1169
1664
|
|
|
1170
1665
|
// src/overmind.ts
|
|
1171
1666
|
init_db();
|
|
1172
|
-
import { existsSync as
|
|
1667
|
+
import { existsSync as existsSync4, readFileSync as readFileSync8 } from "fs";
|
|
1173
1668
|
import { basename as basename3, dirname as dirname2, join as join7 } from "path";
|
|
1174
1669
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
1670
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1671
|
+
|
|
1672
|
+
// src/swarmClient.ts
|
|
1673
|
+
init_config();
|
|
1674
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1675
|
+
import { dirname } from "path";
|
|
1676
|
+
import { randomUUID } from "crypto";
|
|
1677
|
+
|
|
1678
|
+
// src/resources.ts
|
|
1679
|
+
init_config();
|
|
1680
|
+
import os from "os";
|
|
1681
|
+
import { execFileSync } from "child_process";
|
|
1682
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1683
|
+
function isTransientResourceError(e) {
|
|
1684
|
+
const code = e?.code;
|
|
1685
|
+
return code === "ENFILE" || code === "EMFILE" || code === "EAGAIN";
|
|
1686
|
+
}
|
|
1687
|
+
function logTransientSpawnError(label, e) {
|
|
1688
|
+
if (isTransientResourceError(e)) {
|
|
1689
|
+
const code = e.code;
|
|
1690
|
+
console.warn(`[overmind] ${label}: \u041E\u0421 \u043D\u0435 \u0434\u0430\u0451\u0442 \u0441\u043F\u0430\u0432\u043D\u0438\u0442\u044C \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u044B (${code}) \u2014 \u043F\u0440\u043E\u043F\u0443\u0441\u043A\u0430\u044E \u0441\u0438\u0433\u043D\u0430\u043B \u044D\u0442\u043E\u0433\u043E \u0442\u0438\u043A\u0430, \u043D\u0435 \u043F\u0430\u0434\u0430\u044E`);
|
|
1691
|
+
} else {
|
|
1692
|
+
console.warn(`[overmind] ${label} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F (\u043D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445 \u0437\u0430 \u044D\u0442\u043E\u0442 \u0442\u0438\u043A): ${String(e)}`);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
function resourcesOk(load1, cpus, freeGb, maxLoadFactor = SWARM_MAX_LOAD_FACTOR, minFreeGb = SWARM_MIN_FREE_GB, fdLoadRatio = null, maxFdLoadFactor = SWARM_MAX_FD_LOAD_FACTOR) {
|
|
1696
|
+
const cores = cpus > 0 ? cpus : 1;
|
|
1697
|
+
const fdsOk = fdLoadRatio == null || fdLoadRatio <= maxFdLoadFactor;
|
|
1698
|
+
return load1 <= cores * maxLoadFactor && freeGb >= minFreeGb && fdsOk;
|
|
1699
|
+
}
|
|
1700
|
+
function parseVmStatAvailableBytes(vmStatOut) {
|
|
1701
|
+
const page = Number(vmStatOut.match(/page size of (\d+) bytes/)?.[1]) || 4096;
|
|
1702
|
+
const pages = (label) => {
|
|
1703
|
+
const m = vmStatOut.match(new RegExp(`Pages ${label}:\\s+(\\d+)`));
|
|
1704
|
+
return m ? Number(m[1]) : 0;
|
|
1705
|
+
};
|
|
1706
|
+
return (pages("free") + pages("inactive") + pages("speculative") + pages("purgeable")) * page;
|
|
1707
|
+
}
|
|
1708
|
+
function availableMemBytes() {
|
|
1709
|
+
if (process.platform === "darwin") {
|
|
1710
|
+
try {
|
|
1711
|
+
const bytes = parseVmStatAvailableBytes(execFileSync("vm_stat", { encoding: "utf8" }));
|
|
1712
|
+
if (bytes > 0) return bytes;
|
|
1713
|
+
} catch (e) {
|
|
1714
|
+
logTransientSpawnError("vm_stat", e);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return os.freemem();
|
|
1718
|
+
}
|
|
1719
|
+
function parseSysctlInt(out) {
|
|
1720
|
+
const n = Number(out.trim());
|
|
1721
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
1722
|
+
}
|
|
1723
|
+
function readSysctlPair(nameA, nameB) {
|
|
1724
|
+
try {
|
|
1725
|
+
const lines = execFileSync("sysctl", ["-n", nameA, nameB], { encoding: "utf8" }).trim().split("\n");
|
|
1726
|
+
return [parseSysctlInt(lines[0] ?? ""), parseSysctlInt(lines[1] ?? "")];
|
|
1727
|
+
} catch (e) {
|
|
1728
|
+
logTransientSpawnError(`sysctl ${nameA}/${nameB}`, e);
|
|
1729
|
+
return [null, null];
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
function fileDescriptorLoadRatio() {
|
|
1733
|
+
if (process.platform === "darwin") {
|
|
1734
|
+
const [max, used] = readSysctlPair("kern.maxfiles", "kern.num_files");
|
|
1735
|
+
return max != null && used != null ? used / max : null;
|
|
1736
|
+
}
|
|
1737
|
+
if (process.platform === "linux") {
|
|
1738
|
+
try {
|
|
1739
|
+
const [allocated, , max] = readFileSync2("/proc/sys/fs/file-nr", "utf8").trim().split(/\s+/).map(Number);
|
|
1740
|
+
if (Number.isFinite(allocated ?? NaN) && Number.isFinite(max ?? NaN) && max) return allocated / max;
|
|
1741
|
+
} catch {
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
return null;
|
|
1745
|
+
}
|
|
1746
|
+
var RESOURCES_CACHE_MS = 250;
|
|
1747
|
+
var readingCache = null;
|
|
1748
|
+
function hostReading() {
|
|
1749
|
+
const now = Date.now();
|
|
1750
|
+
if (readingCache && now - readingCache.at < RESOURCES_CACHE_MS) return readingCache.reading;
|
|
1751
|
+
const reading = {
|
|
1752
|
+
load1: os.loadavg()[0] ?? 0,
|
|
1753
|
+
cpus: os.cpus().length,
|
|
1754
|
+
freeGb: availableMemBytes() / 1e9,
|
|
1755
|
+
fdLoadRatio: fileDescriptorLoadRatio()
|
|
1756
|
+
};
|
|
1757
|
+
readingCache = { at: now, reading };
|
|
1758
|
+
return reading;
|
|
1759
|
+
}
|
|
1760
|
+
function hostResourcesOk() {
|
|
1761
|
+
const r = hostReading();
|
|
1762
|
+
return resourcesOk(r.load1, r.cpus, r.freeGb, void 0, void 0, r.fdLoadRatio);
|
|
1763
|
+
}
|
|
1764
|
+
function hostReliefNeeded() {
|
|
1765
|
+
const r = hostReading();
|
|
1766
|
+
return !resourcesOk(r.load1, r.cpus, r.freeGb, SWARM_RELIEF_LOAD_FACTOR, SWARM_RELIEF_MIN_FREE_GB, r.fdLoadRatio, SWARM_RELIEF_MAX_FD_LOAD_FACTOR);
|
|
1767
|
+
}
|
|
1175
1768
|
|
|
1176
1769
|
// src/swarmClient.ts
|
|
1177
|
-
|
|
1178
|
-
init_names();
|
|
1770
|
+
init_names2();
|
|
1179
1771
|
init_storm();
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
function
|
|
1184
|
-
return
|
|
1772
|
+
init_src();
|
|
1773
|
+
var RESOLVER_TERMINAL = /* @__PURE__ */ new Set([...TERMINAL_STATUSES, "stalled"]);
|
|
1774
|
+
var SLUG_TERMINAL = new Set(TERMINAL_STATUSES);
|
|
1775
|
+
function runRef(runKey) {
|
|
1776
|
+
return { __runRef: runKey };
|
|
1185
1777
|
}
|
|
1186
|
-
function
|
|
1187
|
-
return
|
|
1778
|
+
function isRunRef(v) {
|
|
1779
|
+
return typeof v === "object" && v !== null && "__runRef" in v;
|
|
1780
|
+
}
|
|
1781
|
+
function jsonFromTextContent(res) {
|
|
1782
|
+
const text = res?.content?.find((c) => c.type === "text")?.text;
|
|
1783
|
+
if (text) {
|
|
1784
|
+
try {
|
|
1785
|
+
return JSON.parse(text);
|
|
1786
|
+
} catch {
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return res?.structuredContent ?? null;
|
|
1188
1790
|
}
|
|
1189
|
-
var RESOLVER_TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "superseded", "stopped", "stalled"]);
|
|
1190
|
-
var SLUG_TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "superseded", "stopped"]);
|
|
1191
|
-
var MILESTONES = /* @__PURE__ */ new Set([
|
|
1192
|
-
"spawned",
|
|
1193
|
-
"pr_opened",
|
|
1194
|
-
"merged",
|
|
1195
|
-
"done",
|
|
1196
|
-
"failed",
|
|
1197
|
-
"stalled",
|
|
1198
|
-
"conflict",
|
|
1199
|
-
"stopped",
|
|
1200
|
-
"superseded",
|
|
1201
|
-
"paused",
|
|
1202
|
-
"resumed",
|
|
1203
|
-
"storm_start",
|
|
1204
|
-
"storm_end",
|
|
1205
|
-
"storm_dead"
|
|
1206
|
-
]);
|
|
1207
1791
|
var SwarmClient = class _SwarmClient {
|
|
1208
1792
|
call;
|
|
1209
1793
|
defaultRepo;
|
|
1210
1794
|
autoFlush;
|
|
1211
1795
|
listLimit;
|
|
1796
|
+
// TASWM-169: репо-реестр демона — repoFullName[] на claim/heartbeat + обратная
|
|
1797
|
+
// карта repoFullName→локальный путь для трансляции run.repo из прода (см. resolveRepo).
|
|
1798
|
+
repoFullNames;
|
|
1799
|
+
repoPathByFullName;
|
|
1212
1800
|
// Зеркало.
|
|
1213
1801
|
runs = /* @__PURE__ */ new Map();
|
|
1214
1802
|
runtime = /* @__PURE__ */ new Map();
|
|
@@ -1227,6 +1815,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
1227
1815
|
// Outbox / flush.
|
|
1228
1816
|
outboxPath;
|
|
1229
1817
|
mapPath;
|
|
1818
|
+
stormPath;
|
|
1819
|
+
// durable-сайдкар глобальных (штормовых) событий
|
|
1230
1820
|
pending = [];
|
|
1231
1821
|
nextSeq = 1;
|
|
1232
1822
|
flushing = false;
|
|
@@ -1239,8 +1829,12 @@ var SwarmClient = class _SwarmClient {
|
|
|
1239
1829
|
this.defaultRepo = normalizeRepo(opts.defaultRepo ?? "");
|
|
1240
1830
|
this.autoFlush = opts.autoFlush ?? true;
|
|
1241
1831
|
this.listLimit = opts.listLimit ?? 200;
|
|
1832
|
+
const registry = opts.repoRegistry ?? [];
|
|
1833
|
+
this.repoFullNames = registry.map((e) => e.repoFullName);
|
|
1834
|
+
this.repoPathByFullName = new Map(registry.map((e) => [e.repoFullName, normalizeRepo(e.path)]));
|
|
1242
1835
|
this.outboxPath = opts.outboxPath;
|
|
1243
1836
|
this.mapPath = `${opts.outboxPath}.prodmap.json`;
|
|
1837
|
+
this.stormPath = `${opts.outboxPath}.storm.jsonl`;
|
|
1244
1838
|
mkdirSync2(dirname(this.outboxPath), { recursive: true });
|
|
1245
1839
|
this.recover();
|
|
1246
1840
|
}
|
|
@@ -1248,13 +1842,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
1248
1842
|
recover() {
|
|
1249
1843
|
if (existsSync2(this.mapPath)) {
|
|
1250
1844
|
try {
|
|
1251
|
-
const obj = JSON.parse(
|
|
1845
|
+
const obj = JSON.parse(readFileSync3(this.mapPath, "utf8"));
|
|
1252
1846
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
1253
1847
|
} catch {
|
|
1254
1848
|
}
|
|
1255
1849
|
}
|
|
1256
1850
|
if (existsSync2(this.outboxPath)) {
|
|
1257
|
-
const txt =
|
|
1851
|
+
const txt = readFileSync3(this.outboxPath, "utf8");
|
|
1258
1852
|
for (const line of txt.split("\n")) {
|
|
1259
1853
|
const s = line.trim();
|
|
1260
1854
|
if (!s) continue;
|
|
@@ -1266,6 +1860,42 @@ var SwarmClient = class _SwarmClient {
|
|
|
1266
1860
|
this.nextSeq = this.pending.reduce((m, e) => Math.max(m, e.seq), 0) + 1;
|
|
1267
1861
|
if (this.autoFlush && this.pending.length) this.scheduleFlush();
|
|
1268
1862
|
}
|
|
1863
|
+
this.replayGlobalEvents();
|
|
1864
|
+
}
|
|
1865
|
+
// Реплей глобальных (штормовых) событий из сайдкара в зеркало на старте. Идут в
|
|
1866
|
+
// eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
|
|
1867
|
+
// переприсваиваем локально (id событий — только порядок, не корреляция с продом).
|
|
1868
|
+
replayGlobalEvents() {
|
|
1869
|
+
if (!existsSync2(this.stormPath)) return;
|
|
1870
|
+
let txt;
|
|
1871
|
+
try {
|
|
1872
|
+
txt = readFileSync3(this.stormPath, "utf8");
|
|
1873
|
+
} catch {
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
for (const line of txt.split("\n")) {
|
|
1877
|
+
const s = line.trim();
|
|
1878
|
+
if (!s) continue;
|
|
1879
|
+
try {
|
|
1880
|
+
const g = JSON.parse(s);
|
|
1881
|
+
this.eventsLog.push({
|
|
1882
|
+
id: this.nextEventId++,
|
|
1883
|
+
run_id: null,
|
|
1884
|
+
kind: g.kind,
|
|
1885
|
+
text: g.text ?? null,
|
|
1886
|
+
actor: g.actor ?? null,
|
|
1887
|
+
at: g.at
|
|
1888
|
+
});
|
|
1889
|
+
} catch {
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
// Дописать глобальное событие в durable-сайдкар (append-only, синхронно — как outbox).
|
|
1894
|
+
persistGlobalEvent(ev) {
|
|
1895
|
+
try {
|
|
1896
|
+
appendFileSync(this.stormPath, JSON.stringify(ev) + "\n");
|
|
1897
|
+
} catch {
|
|
1898
|
+
}
|
|
1269
1899
|
}
|
|
1270
1900
|
// ── зеркало: наложить рантайм на durable-прогон ──────────────────────────────
|
|
1271
1901
|
mat(d) {
|
|
@@ -1276,14 +1906,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
1276
1906
|
runKeyFor(localId) {
|
|
1277
1907
|
return this.keyByLocal.get(localId);
|
|
1278
1908
|
}
|
|
1279
|
-
//
|
|
1280
|
-
// Нужен для отправки ссылки на прогон-героя (hero_run_id → heroId в проде): в
|
|
1281
|
-
// зеркале hero_run_id — локальный number, а прод-схеме нужен cuid. TASWM-78.
|
|
1282
|
-
prodIdForLocal(localId) {
|
|
1283
|
-
const key = this.keyByLocal.get(localId);
|
|
1284
|
-
return key ? this.prodIdByKey.get(key) : void 0;
|
|
1285
|
-
}
|
|
1286
|
-
// Обратно: cuid прода → локальный id (по runKey гидрированного прогона `p:<cuid>`).
|
|
1909
|
+
// cuid прода → локальный id (по runKey гидрированного прогона `p:<cuid>`).
|
|
1287
1910
|
// Для маппинга прод heroId → локальный hero_run_id при гидрации. Резолвится, когда
|
|
1288
1911
|
// прогон-герой присутствует в зеркале; иначе null (уточнится следующим рефрешем).
|
|
1289
1912
|
localIdForProdId(prodId) {
|
|
@@ -1300,9 +1923,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
1300
1923
|
this.scheduleFlush();
|
|
1301
1924
|
}
|
|
1302
1925
|
// Прод-патч прогона: локальные поля Run → поля swarm_run_upsert (см. TASWM-74).
|
|
1303
|
-
// pid/worktree_path (рантайм),
|
|
1304
|
-
//
|
|
1305
|
-
//
|
|
1926
|
+
// pid/worktree_path (рантайм), respawns (нет в схеме тула) и снимок задачи (task_*,
|
|
1927
|
+
// прод берёт join'ом из Task) в прод НЕ уходят. kind → role (плоский enum). Self-ref
|
|
1928
|
+
// прогонов hero_run_id/resolves_run_id/superseded_by → heroId/resolvesRunId/
|
|
1929
|
+
// supersededById как ОТЛОЖЕННЫЕ ссылки (RunRef) — cuid резолвится на флаше.
|
|
1306
1930
|
runFields(patch) {
|
|
1307
1931
|
const out = {};
|
|
1308
1932
|
if ("status" in patch && patch.status !== void 0) out.status = patch.status;
|
|
@@ -1317,15 +1941,26 @@ var SwarmClient = class _SwarmClient {
|
|
|
1317
1941
|
if ("depends_on" in patch && patch.depends_on) out.dependsOn = patch.depends_on;
|
|
1318
1942
|
if ("decision" in patch) out.decision = patch.decision ? JSON.stringify(patch.decision) : null;
|
|
1319
1943
|
if ("kind" in patch && patch.kind !== void 0) out.role = patch.kind;
|
|
1320
|
-
if ("
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1944
|
+
if ("review_verdict" in patch) out.reviewVerdict = patch.review_verdict ?? null;
|
|
1945
|
+
this.setRunRefField(out, "heroId", patch, "hero_run_id");
|
|
1946
|
+
this.setRunRefField(out, "resolvesRunId", patch, "resolves_run_id");
|
|
1947
|
+
this.setRunRefField(out, "supersededById", patch, "superseded_by");
|
|
1948
|
+
this.setRunRefField(out, "reviewsRunId", patch, "reviews_run_id");
|
|
1327
1949
|
return out;
|
|
1328
1950
|
}
|
|
1951
|
+
// Положить self-ref прогона в прод-патч: локальный number → отложенная ссылка
|
|
1952
|
+
// {__runRef: runKey} (резолв в cuid на флаше, resolveArgs/materializeRefs). Явный
|
|
1953
|
+
// null → очистка поля. Локальный id без runKey (прогон не в зеркале) — пропускаем.
|
|
1954
|
+
setRunRefField(out, prodKey, patch, localKey) {
|
|
1955
|
+
if (!(localKey in patch)) return;
|
|
1956
|
+
const localId = patch[localKey];
|
|
1957
|
+
if (localId == null) {
|
|
1958
|
+
out[prodKey] = null;
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
const key = this.keyByLocal.get(localId);
|
|
1962
|
+
if (key) out[prodKey] = runRef(key);
|
|
1963
|
+
}
|
|
1329
1964
|
enqueueRun(run, fields) {
|
|
1330
1965
|
if (Object.keys(fields).length === 0) return;
|
|
1331
1966
|
this.enqueue({ t: "run", runKey: run.runKey, taskRef: run.task_slug, fields });
|
|
@@ -1358,6 +1993,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
1358
1993
|
stop_reason: null,
|
|
1359
1994
|
respawns: 0,
|
|
1360
1995
|
resolves_run_id: input.resolves_run_id ?? null,
|
|
1996
|
+
reviews_run_id: input.reviews_run_id ?? null,
|
|
1997
|
+
review_verdict: null,
|
|
1361
1998
|
task_title: null,
|
|
1362
1999
|
task_desc: null,
|
|
1363
2000
|
task_url: null,
|
|
@@ -1375,9 +2012,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
1375
2012
|
depends_on: d.depends_on,
|
|
1376
2013
|
decision: d.decision,
|
|
1377
2014
|
kind: d.kind,
|
|
1378
|
-
hero_run_id: d.hero_run_id
|
|
2015
|
+
hero_run_id: d.hero_run_id,
|
|
2016
|
+
resolves_run_id: d.resolves_run_id,
|
|
2017
|
+
// резолвер конфликта → связь на review-прогон
|
|
2018
|
+
reviews_run_id: d.reviews_run_id
|
|
2019
|
+
// ревьюер (авто-ревью, TASWM-148) → связь на review-прогон
|
|
1379
2020
|
}));
|
|
1380
|
-
const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
|
|
2021
|
+
const spawnNote = input.resolves_run_id ? `\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0430 \u0434\u043B\u044F #${input.resolves_run_id}` : input.reviews_run_id ? `\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u043B\u044F #${input.reviews_run_id}` : input.decision ? `\u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A \u0441 \u0440\u0435\u0448\u0435\u043D\u0438\u0435\u043C \xAB${input.decision.chosen}\xBB` : `run for ${input.task_slug}`;
|
|
1381
2022
|
this.addEvent(id, "spawned", spawnNote);
|
|
1382
2023
|
return this.mat(d);
|
|
1383
2024
|
}
|
|
@@ -1431,6 +2072,25 @@ var SwarmClient = class _SwarmClient {
|
|
|
1431
2072
|
resolverCountFor(reviewRunId) {
|
|
1432
2073
|
return [...this.runs.values()].filter((r) => r.resolves_run_id === reviewRunId).length;
|
|
1433
2074
|
}
|
|
2075
|
+
// failed/stalled ревьюеры (сорвался спавн ДО вердикта) в счёт НЕ идут — см. db.ts:reviewerCountFor.
|
|
2076
|
+
reviewerCountFor(reviewRunId) {
|
|
2077
|
+
return [...this.runs.values()].filter(
|
|
2078
|
+
(r) => r.reviews_run_id === reviewRunId && r.status !== "failed" && r.status !== "stalled"
|
|
2079
|
+
).length;
|
|
2080
|
+
}
|
|
2081
|
+
// Жёсткий кап ревью-спавнов (TASWM-204) — см. db.ts:reviewerAttemptCountFor.
|
|
2082
|
+
reviewerAttemptCountFor(reviewRunId) {
|
|
2083
|
+
return [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId).length;
|
|
2084
|
+
}
|
|
2085
|
+
// Последняя попытка ревьюера (TASWM-196) — см. db.ts:lastReviewerAttemptFor.
|
|
2086
|
+
lastReviewerAttemptFor(reviewRunId) {
|
|
2087
|
+
const cands = [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId).sort((a, b) => b.id - a.id);
|
|
2088
|
+
return cands.length ? this.mat(cands[0]) : null;
|
|
2089
|
+
}
|
|
2090
|
+
// Живые/ждущие ревьюер-прогоны (TASWM-204) — см. db.ts:activeReviewersFor.
|
|
2091
|
+
activeReviewersFor(reviewRunId) {
|
|
2092
|
+
return [...this.runs.values()].filter((r) => r.reviews_run_id === reviewRunId && !TERMINAL_STATUSES.includes(r.status)).sort((a, b) => b.id - a.id).map((d) => this.mat(d));
|
|
2093
|
+
}
|
|
1434
2094
|
activeRunForSlug(slug) {
|
|
1435
2095
|
const cands = [...this.runs.values()].filter((r) => r.task_slug === slug && !SLUG_TERMINAL.has(r.status)).sort((a, b) => b.id - a.id);
|
|
1436
2096
|
return cands.length ? this.mat(cands[0]) : null;
|
|
@@ -1528,7 +2188,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
1528
2188
|
addEvent(run_id, kind, text, actor = null) {
|
|
1529
2189
|
const ev = { id: this.nextEventId++, run_id, kind, text: text ?? null, actor: actor ?? null, at: stamp() };
|
|
1530
2190
|
this.eventsLog.push(ev);
|
|
1531
|
-
if (run_id == null)
|
|
2191
|
+
if (run_id == null) {
|
|
2192
|
+
this.persistGlobalEvent({ kind, text: ev.text, actor: ev.actor, at: ev.at });
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
1532
2195
|
const key = this.runKeyFor(run_id);
|
|
1533
2196
|
if (key) this.enqueue({ t: "event", runKey: key, kind, text: text ?? null, actor: actor ?? null });
|
|
1534
2197
|
}
|
|
@@ -1561,7 +2224,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
1561
2224
|
});
|
|
1562
2225
|
}
|
|
1563
2226
|
for (const e of this.eventsLog) {
|
|
1564
|
-
if (!
|
|
2227
|
+
if (!isMilestoneKind(e.kind)) continue;
|
|
1565
2228
|
const global = e.run_id == null;
|
|
1566
2229
|
const r = runById(e.run_id);
|
|
1567
2230
|
items.push({
|
|
@@ -1618,7 +2281,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
1618
2281
|
return this.eventsLog.filter((e) => {
|
|
1619
2282
|
if (e.kind !== "failed" || e.run_id == null || e.at < cutoff) return false;
|
|
1620
2283
|
const r = this.runs.get(e.run_id);
|
|
1621
|
-
return !!r && r.resolves_run_id == null;
|
|
2284
|
+
return !!r && r.resolves_run_id == null && r.reviews_run_id == null;
|
|
1622
2285
|
}).length;
|
|
1623
2286
|
}
|
|
1624
2287
|
recordStormProbe(probe, ext) {
|
|
@@ -1642,6 +2305,100 @@ var SwarmClient = class _SwarmClient {
|
|
|
1642
2305
|
if (res?.isError) return;
|
|
1643
2306
|
const runs = res.structuredContent?.runs ?? [];
|
|
1644
2307
|
for (const pr of runs) this.hydrateRun(pr);
|
|
2308
|
+
for (const pr of runs) this.linkRefs(pr);
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Реконсайл рантайма после рестарта демона (client-режим). pid/worktree_path живут в
|
|
2312
|
+
* ОТДЕЛЬНОЙ Map (машинное — в прод не уходят) и рестарт демона НЕ переживают: гидрация
|
|
2313
|
+
* из прода приносит running/blocked-прогоны с pid=null. Но демон и дроны — РАЗНЫЕ
|
|
2314
|
+
* detached-процессы (launcher: detached+unref), поэтому рестарт демона дронов НЕ убивает
|
|
2315
|
+
* — часть их ещё жива. Сопоставляем каждый running/blocked-прогон зеркала с живым
|
|
2316
|
+
* процессом (ps, injected listProcesses) по детерминированному worktree
|
|
2317
|
+
* (<worktreesDir>/<repo-basename>/<slug>):
|
|
2318
|
+
* • нашёлся живой процесс → восстанавливаем pid+worktree_path (дрон жив — overmind его
|
|
2319
|
+
* НЕ похоронит и не заспавнит второго поверх);
|
|
2320
|
+
* • не нашёлся → дрон не пережил рестарт: восстанавливаем worktree_path, если каталог
|
|
2321
|
+
* цел (для respawn --continue), pid оставляем null. overmind (tickOnce шаг 3) ловит
|
|
2322
|
+
* running/blocked с pid=null как смерть и оживляет/усыновляет/стопорит по общей ветке.
|
|
2323
|
+
* Рантайм пишем ПРЯМО в runtime-Map (в обход updateRun) — чисто локально, без outbox и
|
|
2324
|
+
* без прод-раунд-трипа. Идемпотентно; вызывается один раз на старте после hydrate().
|
|
2325
|
+
* Возвращает счётчики (для лога демона) + reconciled (TASWM-145): false → listProcesses()
|
|
2326
|
+
* бросил (ENFILE и т.п.), реконсайл этого запуска ПРОПУЩЕН целиком (не «confirmed пусто» —
|
|
2327
|
+
* см. коммент в catch ниже). Вызывающий (runDaemon) ОБЯЗАН пропустить startup-sync
|
|
2328
|
+
* (syncCrashedOnStartup) при reconciled=false: та трактует pid==null как «не пережил
|
|
2329
|
+
* рестарт» и безусловно хоронит (stalled/done) — при пропущенном реконсайле это massively
|
|
2330
|
+
* похоронило бы ВСЕХ running/blocked, включая реально живых detached-дронов, тем же
|
|
2331
|
+
* транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
|
|
2332
|
+
*/
|
|
2333
|
+
reconcileRuntime(opts) {
|
|
2334
|
+
const wtExists = opts.worktreeExists ?? existsSync2;
|
|
2335
|
+
const pidByWorktree = /* @__PURE__ */ new Map();
|
|
2336
|
+
try {
|
|
2337
|
+
for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
|
|
2338
|
+
} catch (e) {
|
|
2339
|
+
logTransientSpawnError("ps (reconcileRuntime)", e);
|
|
2340
|
+
return { alive: 0, dead: 0, reconciled: false };
|
|
2341
|
+
}
|
|
2342
|
+
let alive = 0;
|
|
2343
|
+
let dead = 0;
|
|
2344
|
+
for (const d of this.runs.values()) {
|
|
2345
|
+
if (d.status !== "running" && d.status !== "blocked") continue;
|
|
2346
|
+
if (this.runtime.get(d.id)?.pid != null) continue;
|
|
2347
|
+
const workerWt = droneWorktreePath(opts.worktreesDir, d.repo || this.defaultRepo, d.task_slug);
|
|
2348
|
+
let matchWt = null;
|
|
2349
|
+
let matchPid = null;
|
|
2350
|
+
if (d.resolves_run_id == null && d.reviews_run_id == null) {
|
|
2351
|
+
const p = pidByWorktree.get(workerWt);
|
|
2352
|
+
if (p != null) {
|
|
2353
|
+
matchWt = workerWt;
|
|
2354
|
+
matchPid = p;
|
|
2355
|
+
}
|
|
2356
|
+
} else if (d.resolves_run_id != null) {
|
|
2357
|
+
const prefix = `${workerWt}-resolve-`;
|
|
2358
|
+
for (const [wt, p] of pidByWorktree) {
|
|
2359
|
+
if (wt.startsWith(prefix)) {
|
|
2360
|
+
matchWt = wt;
|
|
2361
|
+
matchPid = p;
|
|
2362
|
+
break;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
} else {
|
|
2366
|
+
const reviewed = this.runs.get(d.reviews_run_id);
|
|
2367
|
+
const reviewedWt = reviewed ? droneWorktreePath(opts.worktreesDir, reviewed.repo || this.defaultRepo, reviewed.task_slug) : workerWt;
|
|
2368
|
+
const prefix = `${reviewedWt}-review-`;
|
|
2369
|
+
for (const [wt, p] of pidByWorktree) {
|
|
2370
|
+
if (wt.startsWith(prefix)) {
|
|
2371
|
+
matchWt = wt;
|
|
2372
|
+
matchPid = p;
|
|
2373
|
+
break;
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
if (matchPid != null && matchWt) {
|
|
2378
|
+
this.runtime.set(d.id, { pid: matchPid, worktree_path: matchWt });
|
|
2379
|
+
alive++;
|
|
2380
|
+
} else {
|
|
2381
|
+
const wt = d.resolves_run_id == null && d.reviews_run_id == null && wtExists(workerWt) ? workerWt : null;
|
|
2382
|
+
this.runtime.set(d.id, { pid: null, worktree_path: wt });
|
|
2383
|
+
dead++;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
return { alive, dead, reconciled: true };
|
|
2387
|
+
}
|
|
2388
|
+
// Долинковать self-ref прогона по прод-cuid → локальному id (после того как все
|
|
2389
|
+
// прогоны гидрированы). Идемпотентно: ставит найденное, иначе не трогает.
|
|
2390
|
+
linkRefs(pr) {
|
|
2391
|
+
const localId = this.localIdForProdId(String(pr.id ?? ""));
|
|
2392
|
+
const local = localId === void 0 ? void 0 : this.runs.get(localId);
|
|
2393
|
+
if (!local) return;
|
|
2394
|
+
const hero = pr.heroId ?? null;
|
|
2395
|
+
if (hero) local.hero_run_id = this.localIdForProdId(hero) ?? local.hero_run_id;
|
|
2396
|
+
const res = pr.resolvesRunId ?? null;
|
|
2397
|
+
if (res) local.resolves_run_id = this.localIdForProdId(res) ?? local.resolves_run_id;
|
|
2398
|
+
const sup = pr.supersededById ?? null;
|
|
2399
|
+
if (sup) local.superseded_by = this.localIdForProdId(sup) ?? local.superseded_by;
|
|
2400
|
+
const rev = pr.reviewsRunId ?? null;
|
|
2401
|
+
if (rev) local.reviews_run_id = this.localIdForProdId(rev) ?? local.reviews_run_id;
|
|
1645
2402
|
}
|
|
1646
2403
|
/**
|
|
1647
2404
|
* Авто-интейк (TASWM-100): взять следующую claimable-задачу владельца токена через
|
|
@@ -1654,11 +2411,17 @@ var SwarmClient = class _SwarmClient {
|
|
|
1654
2411
|
* cuid известен сразу, никакого create-flush. Возвращает локальный Run (queued; overmind
|
|
1655
2412
|
* заспавнит его следующим тиком) либо null: нет claimable-задачи / прод недоступен / тул
|
|
1656
2413
|
* отверг — во всех случаях тик продолжается, следующий ретраит.
|
|
2414
|
+
*
|
|
2415
|
+
* TASWM-169: repoFullNames — ЖИВОЙ реестр демона на момент ЭТОГО вызова, а не
|
|
2416
|
+
* последняя heartbeat-декларация (heartbeat — своя кадансность, ~15с; claim — ~60с;
|
|
2417
|
+
* плюс окно до первого успешного heartbeat при рестарте) — свежий аргумент здесь
|
|
2418
|
+
* убирает то окно рассинхрона целиком. Пусто (демон без SWARM_REPOS) → ключ не
|
|
2419
|
+
* шлём — прод фолбэкается на последнюю heartbeat-декларацию (или без фильтра).
|
|
1657
2420
|
*/
|
|
1658
2421
|
async claim() {
|
|
1659
2422
|
let res;
|
|
1660
2423
|
try {
|
|
1661
|
-
res = await this.call("swarm_claim_work", {});
|
|
2424
|
+
res = await this.call("swarm_claim_work", this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {});
|
|
1662
2425
|
} catch {
|
|
1663
2426
|
return null;
|
|
1664
2427
|
}
|
|
@@ -1668,12 +2431,114 @@ var SwarmClient = class _SwarmClient {
|
|
|
1668
2431
|
const local = this.hydrateRun(sc.run);
|
|
1669
2432
|
return local ? this.mat(local) : null;
|
|
1670
2433
|
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Живой потолок дронов владельца из Taskless (TASWM-126): читает who_am_i.maxDrones
|
|
2436
|
+
* (личная настройка uiPrefs.swarm.maxDrones, дефолт 3). overmind капит им ВСЕ спавны —
|
|
2437
|
+
* очередь (шаг 1), резолвер (шаг 2), авто-claim (шаг 5) — чтобы активных дронов не было
|
|
2438
|
+
* больше числа из UI (инцидент 15.07: 4-6 дронов разом уронили ноут). who_am_i отдаёт
|
|
2439
|
+
* плоский JSON в text-контенте (не {display,data}). Прод недоступен / нет валидного числа
|
|
2440
|
+
* → null: ридер (makeMaxDronesReader) берёт env-фолбэк.
|
|
2441
|
+
*/
|
|
2442
|
+
async ownerMaxDrones() {
|
|
2443
|
+
let res;
|
|
2444
|
+
try {
|
|
2445
|
+
res = await this.call("who_am_i", {});
|
|
2446
|
+
} catch {
|
|
2447
|
+
return null;
|
|
2448
|
+
}
|
|
2449
|
+
if (res?.isError) return null;
|
|
2450
|
+
const obj = jsonFromTextContent(res);
|
|
2451
|
+
const n = obj?.maxDrones;
|
|
2452
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
|
|
2453
|
+
}
|
|
2454
|
+
/**
|
|
2455
|
+
* Live verifyMode проекта из Taskless (TASWM-171): читает swarm_repo_config по
|
|
2456
|
+
* repoFullName (owner/repo из origin — см. git.remoteRepoFullName) — Project.swarmConfig.
|
|
2457
|
+
* verifyMode ("local"/"ci"). Прод недоступен / repo не смэплен на swarm-enabled проект
|
|
2458
|
+
* владельца токена (config=null в ответе) → null: ридер (makeVerifyModeReader) берёт
|
|
2459
|
+
* env-фолбэк SWARM_VERIFY.
|
|
2460
|
+
*/
|
|
2461
|
+
async repoVerifyMode(repoFullName) {
|
|
2462
|
+
let res;
|
|
2463
|
+
try {
|
|
2464
|
+
res = await this.call("swarm_repo_config", { repoFullName });
|
|
2465
|
+
} catch {
|
|
2466
|
+
return null;
|
|
2467
|
+
}
|
|
2468
|
+
if (res?.isError) return null;
|
|
2469
|
+
const sc = res.structuredContent ?? {};
|
|
2470
|
+
const v = sc.config?.verifyMode;
|
|
2471
|
+
return v === "local" || v === "ci" ? v : null;
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Пульс overmind в прод (TASWM-129): swarm_heartbeat обновляет User.swarmHeartbeatAt —
|
|
2475
|
+
* дашборд /swarm сравнивает свежесть со своим порогом устаревания («нет связи с
|
|
2476
|
+
* овермайндом» вместо устаревшего «живого» роя). Best-effort, как ownerMaxDrones/claim:
|
|
2477
|
+
* прод недоступен/тул отверг → просто не обновили в этом тике, следующий ретраит —
|
|
2478
|
+
* не роняем тик демона из-за упавшего пульса.
|
|
2479
|
+
*
|
|
2480
|
+
* TASWM-169: заодно декларирует repoFullNames (репо-реестр демона) — та же
|
|
2481
|
+
* кадансность, что и пульс, естественная точка «сказать серверу, что обслуживаю».
|
|
2482
|
+
* Пустой реестр (демон не сконфигурирован на multi-repo/SWARM_REPOS) → ключ не
|
|
2483
|
+
* шлём вовсе (undefined) — прод НЕ трогает прошлую декларацию (см. swarm.ts), не
|
|
2484
|
+
* стирает её ложным «репо больше нет» из-за демона без SWARM_REPOS в env.
|
|
2485
|
+
*/
|
|
2486
|
+
async heartbeat() {
|
|
2487
|
+
try {
|
|
2488
|
+
await this.call("swarm_heartbeat", this.repoFullNames.length ? { repoFullNames: this.repoFullNames } : {});
|
|
2489
|
+
} catch {
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Встроенный надзор (TASWM-194): нотификация аномалии в инбокс владельца — прод-MCP
|
|
2494
|
+
* `swarm_notify`, ПРЯМОЙ вызов (НЕ outbox): аномалия — «сообщи СЕЙЧАС», не durable-факт,
|
|
2495
|
+
* который обязан пережить рестарт демона в неизменном порядке (как run-апдейты/события) —
|
|
2496
|
+
* задержка на цикл outbox-флаша (обычно доли секунды, но в худшем случае — до
|
|
2497
|
+
* восстановления сети) тут не оправдана.
|
|
2498
|
+
* runId (локальный numeric) резолвим в прод-cuid по той же карте, что flush аутбокса
|
|
2499
|
+
* (runKeyFor→prodIdByKey); прогон ещё не успел долететь до прода (cuid неизвестен) →
|
|
2500
|
+
* шлём без runId (нотификация всё равно уйдёт, просто без SwarmEvent-вехи на прогоне).
|
|
2501
|
+
*
|
|
2502
|
+
* БРОСАЕТ при сбое (транспорт ИЛИ isError:true от тула) — НЕ глотает, в отличие от
|
|
2503
|
+
* heartbeat() выше (code-review TASWM-194): вызывающий (overmind.ts tickOnce) коммитит
|
|
2504
|
+
* AnomalyTracker ТОЛЬКО после успешного return — молча проглоченная ошибка означала бы,
|
|
2505
|
+
* что трекер считает недоставленную нотификацию отправленной и молчит renotifyCooldownMs
|
|
2506
|
+
* (по умолчанию час) без единой реальной попытки. `res?.isError` — тот же паттерн, что
|
|
2507
|
+
* ownerMaxDrones() чуть выше: registerTool ловит throw внутри run() и превращает его в
|
|
2508
|
+
* `{isError:true}`, а не в отклонённый промис — без этой проверки схемная/бизнес-ошибка
|
|
2509
|
+
* тула (напр. TASWM-194 review: body > zod max) молча резолвилась бы успехом.
|
|
2510
|
+
*/
|
|
2511
|
+
async notifyAnomaly(signal) {
|
|
2512
|
+
const prodRunId = signal.runId != null ? this.prodIdByKey.get(this.runKeyFor(signal.runId) ?? "") : void 0;
|
|
2513
|
+
const res = await this.call("swarm_notify", {
|
|
2514
|
+
tag: signal.key,
|
|
2515
|
+
title: signal.title,
|
|
2516
|
+
body: signal.body,
|
|
2517
|
+
...signal.href ? { href: signal.href } : {},
|
|
2518
|
+
...prodRunId ? { runId: prodRunId } : {}
|
|
2519
|
+
});
|
|
2520
|
+
if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
|
|
2521
|
+
}
|
|
2522
|
+
/**
|
|
2523
|
+
* TASWM-169: прод присылает run.repo как GitHub `owner/repo` для задач, взятых
|
|
2524
|
+
* через claim-фильтр (matchedRepo в swarm_claim_work, см. api/mcp/tools/swarm.ts) —
|
|
2525
|
+
* сервер не знает локальных fs-путей. Транслируем в путь по СВОЕМУ репо-реестру
|
|
2526
|
+
* (см. repoRegistry.ts), т.к. git-операции (addWorktree/droneWorktreePath) ждут
|
|
2527
|
+
* именно путь. Не нашли в реестре (репо не наш / ручной `--repo <path>` спавн, где
|
|
2528
|
+
* repo — ВСЕГДА уже путь, старый формат) → возвращаем как есть без изменений
|
|
2529
|
+
* (обратная совместимость: раньше pr.repo ВСЕГДА был локальным путём).
|
|
2530
|
+
*/
|
|
2531
|
+
resolveRepo(raw) {
|
|
2532
|
+
if (!raw) return "";
|
|
2533
|
+
return this.repoPathByFullName.get(raw) ?? raw;
|
|
2534
|
+
}
|
|
1671
2535
|
hydrateRun(pr) {
|
|
1672
2536
|
const prodId = String(pr.id ?? "");
|
|
1673
2537
|
if (!prodId) return null;
|
|
1674
2538
|
const runKey = `p:${prodId}`;
|
|
1675
2539
|
this.prodIdByKey.set(runKey, prodId);
|
|
1676
2540
|
let local = [...this.runs.values()].find((r) => r.runKey === runKey);
|
|
2541
|
+
if (!local) local = [...this.runs.values()].find((r) => this.prodIdByKey.get(r.runKey) === prodId);
|
|
1677
2542
|
if (!local) {
|
|
1678
2543
|
const id = this.nextRunId++;
|
|
1679
2544
|
local = {
|
|
@@ -1696,6 +2561,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
1696
2561
|
stop_reason: null,
|
|
1697
2562
|
respawns: 0,
|
|
1698
2563
|
resolves_run_id: null,
|
|
2564
|
+
reviews_run_id: null,
|
|
2565
|
+
review_verdict: null,
|
|
1699
2566
|
task_title: null,
|
|
1700
2567
|
task_desc: null,
|
|
1701
2568
|
task_url: null,
|
|
@@ -1707,11 +2574,20 @@ var SwarmClient = class _SwarmClient {
|
|
|
1707
2574
|
}
|
|
1708
2575
|
const slug = pr.taskSlug ?? pr.task?.slug ?? pr.taskId ?? local.task_slug;
|
|
1709
2576
|
local.task_slug = slug || local.task_slug;
|
|
2577
|
+
local.task_title = pr.taskTitle ?? local.task_title;
|
|
2578
|
+
local.task_desc = pr.taskDesc ?? local.task_desc;
|
|
1710
2579
|
local.name = pr.droneName ?? local.name;
|
|
1711
2580
|
local.kind = pr.role ?? local.kind;
|
|
1712
2581
|
const heroProd = pr.heroId ?? null;
|
|
1713
|
-
local.hero_run_id =
|
|
1714
|
-
|
|
2582
|
+
if (heroProd) local.hero_run_id = this.localIdForProdId(heroProd) ?? local.hero_run_id;
|
|
2583
|
+
const resProd = pr.resolvesRunId ?? null;
|
|
2584
|
+
if (resProd) local.resolves_run_id = this.localIdForProdId(resProd) ?? local.resolves_run_id;
|
|
2585
|
+
const supProd = pr.supersededById ?? null;
|
|
2586
|
+
if (supProd) local.superseded_by = this.localIdForProdId(supProd) ?? local.superseded_by;
|
|
2587
|
+
const revProd = pr.reviewsRunId ?? null;
|
|
2588
|
+
if (revProd) local.reviews_run_id = this.localIdForProdId(revProd) ?? local.reviews_run_id;
|
|
2589
|
+
local.review_verdict = pr.reviewVerdict ?? local.review_verdict;
|
|
2590
|
+
local.repo = this.resolveRepo(pr.repo ?? null);
|
|
1715
2591
|
local.branch = pr.branch ?? null;
|
|
1716
2592
|
local.status = pr.status ?? local.status;
|
|
1717
2593
|
local.area = pr.area ?? null;
|
|
@@ -1777,16 +2653,88 @@ var SwarmClient = class _SwarmClient {
|
|
|
1777
2653
|
}, this.backoffMs);
|
|
1778
2654
|
if (typeof this.flushTimer.unref === "function") this.flushTimer.unref();
|
|
1779
2655
|
}
|
|
1780
|
-
|
|
2656
|
+
// TASWM-202: create-флаш без прод-id слал taskId=e.taskRef НАПРЯМУЮ — а taskRef это
|
|
2657
|
+
// человеческий слаг (task_slug, напр. «TASWM-171»), не cuid. Резолверы конфликта
|
|
2658
|
+
// рождаются ЛОКАЛЬНО (createRun с uuid-runKey) и попадают в outbox БЕЗ прод-id —
|
|
2659
|
+
// обычные прогоны рождаются в проде (claim) и гидрируются сразу с cuid, эта ветка
|
|
2660
|
+
// их не касается. Прод create-схема ждёт cuid → отбивает слаг → строго
|
|
2661
|
+
// упорядоченный flush стоит на этой голове НАВЕЧНО (ядовитая голова, инцидент
|
|
2662
|
+
// 14.07/18.07, TASWM-201 — общий карантин; здесь — конкретный источник). Резолвим
|
|
2663
|
+
// слаг→cuid через get_task (кэш — taskIdCache) ПЕРЕД тем как слать create.
|
|
2664
|
+
taskIdCache = /* @__PURE__ */ new Map();
|
|
2665
|
+
// task_slug → cuid прода
|
|
2666
|
+
// Create для этого runKey резолвился окончательно неудачно (задача не найдена/
|
|
2667
|
+
// недоступна) — событие/вопросы того же прогона тоже никогда не получат prod-id,
|
|
2668
|
+
// без карантина они бы бросали «нет prod-id» на каждом флаше и СНОВА вставали
|
|
2669
|
+
// ядовитой головой (просто на одну запись ниже по очереди).
|
|
2670
|
+
poisonedRunKeys = /* @__PURE__ */ new Set();
|
|
2671
|
+
// "transient" — прод/сеть недоступны, ответа нет или ошибка НЕ про отсутствие задачи
|
|
2672
|
+
// (offline-стаб CLI, auth-протух, 5xx): НЕ терминально — запись остаётся в outbox и
|
|
2673
|
+
// повторится на следующем флаше (иначе сетевой сбой = потеря прогона). null —
|
|
2674
|
+
// терминально: прод ЯВНО ответил «задачи нет» (task_not_found / not_found в тексте
|
|
2675
|
+
// tool-ошибки) либо успешный ответ без id — только это карантиним.
|
|
2676
|
+
async resolveTaskCuid(taskRef) {
|
|
2677
|
+
const cached = this.taskIdCache.get(taskRef);
|
|
2678
|
+
if (cached) return cached;
|
|
2679
|
+
let res;
|
|
2680
|
+
try {
|
|
2681
|
+
res = await this.call("get_task", { id: taskRef });
|
|
2682
|
+
} catch {
|
|
2683
|
+
return "transient";
|
|
2684
|
+
}
|
|
2685
|
+
if (!res) return "transient";
|
|
2686
|
+
if (res.isError) {
|
|
2687
|
+
const text = (res.content ?? []).map((c) => c.text ?? "").join(" ");
|
|
2688
|
+
return /not_found/i.test(text) ? null : "transient";
|
|
2689
|
+
}
|
|
2690
|
+
const id = res.structuredContent?.task?.id;
|
|
2691
|
+
if (!id) return null;
|
|
2692
|
+
this.taskIdCache.set(taskRef, id);
|
|
2693
|
+
return id;
|
|
2694
|
+
}
|
|
2695
|
+
async resolveArgs(e) {
|
|
1781
2696
|
if (e.t === "run") {
|
|
2697
|
+
const fields = this.materializeRefs(e.fields);
|
|
1782
2698
|
const prodId2 = this.prodIdByKey.get(e.runKey);
|
|
1783
|
-
|
|
2699
|
+
if (prodId2) return { tool: "swarm_run_upsert", args: { runId: prodId2, ...fields } };
|
|
2700
|
+
const taskId = await this.resolveTaskCuid(e.taskRef);
|
|
2701
|
+
if (taskId === "transient") {
|
|
2702
|
+
throw new Error(`slug\u2192cuid \u0434\u043B\u044F \xAB${e.taskRef}\xBB \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (\u043F\u0440\u043E\u0434 \u043C\u043E\u0440\u0433\u043D\u0443\u043B) \u2014 retry`);
|
|
2703
|
+
}
|
|
2704
|
+
if (!taskId) {
|
|
2705
|
+
console.error(
|
|
2706
|
+
`[swarm-client] create-\u0444\u043B\u0430\u0448 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: \u043F\u0440\u043E\u0434 \u043D\u0435 \u0437\u043D\u0430\u0435\u0442 \u0437\u0430\u0434\u0430\u0447\u0443 \xAB${e.taskRef}\xBB (runKey=${e.runKey}) \u2014 \u0437\u0430\u043F\u0438\u0441\u044C \u0432\u044B\u0431\u0440\u043E\u0448\u0435\u043D\u0430 \u0438\u0437 outbox`
|
|
2707
|
+
);
|
|
2708
|
+
this.poisonedRunKeys.add(e.runKey);
|
|
2709
|
+
return "skip";
|
|
2710
|
+
}
|
|
2711
|
+
return { tool: "swarm_run_upsert", args: { taskId, ...fields } };
|
|
2712
|
+
}
|
|
2713
|
+
if (this.poisonedRunKeys.has(e.runKey)) {
|
|
2714
|
+
console.error(`[swarm-client] ${e.t}-\u0444\u043B\u0430\u0448 \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D: create \u043F\u0440\u043E\u0433\u043E\u043D\u0430 runKey=${e.runKey} \u043D\u0435 \u0440\u0435\u0437\u043E\u043B\u0432\u0438\u043B\u0441\u044F \u0440\u0430\u043D\u0435\u0435 \u2014 \u0437\u0430\u043F\u0438\u0441\u044C \u0432\u044B\u0431\u0440\u043E\u0448\u0435\u043D\u0430 \u0438\u0437 outbox`);
|
|
2715
|
+
return "skip";
|
|
1784
2716
|
}
|
|
1785
2717
|
const prodId = this.prodIdByKey.get(e.runKey);
|
|
1786
2718
|
if (!prodId) throw new Error(`\u043D\u0435\u0442 prod-id \u0434\u043B\u044F ${e.runKey} \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0435\u0449\u0451 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D \u0432 \u043F\u0440\u043E\u0434\u0435`);
|
|
1787
2719
|
if (e.t === "event") return { tool: "swarm_event_append", args: { runId: prodId, kind: e.kind, text: e.text, actor: e.actor } };
|
|
1788
2720
|
return { tool: "swarm_ask", args: { runId: prodId, text: e.text } };
|
|
1789
2721
|
}
|
|
2722
|
+
// Резолв отложенных self-ref ({__runRef: runKey}) в прод-cuid на флаше. Цель уже
|
|
2723
|
+
// создана в проде (её create-запись с меньшим seq флашится раньше) → cuid известен.
|
|
2724
|
+
// Не резолвится (ещё не создан) — поле выкидываем: лучше не слать, чем слать мусор;
|
|
2725
|
+
// догонит следующий апдейт/реконсайл. Обычные значения (null/строки) — как есть.
|
|
2726
|
+
materializeRefs(fields) {
|
|
2727
|
+
const out = {};
|
|
2728
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
2729
|
+
if (isRunRef(v)) {
|
|
2730
|
+
const cuid = this.prodIdByKey.get(v.__runRef);
|
|
2731
|
+
if (cuid) out[k] = cuid;
|
|
2732
|
+
} else {
|
|
2733
|
+
out[k] = v;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
return out;
|
|
2737
|
+
}
|
|
1790
2738
|
/** Слить накопленное в прод. Возвращает число успешно отправленных операций.
|
|
1791
2739
|
* Порядок сохраняется; ошибка/недоступность → стоп, буфер остаётся, retry-backoff. */
|
|
1792
2740
|
async flush() {
|
|
@@ -1798,10 +2746,14 @@ var SwarmClient = class _SwarmClient {
|
|
|
1798
2746
|
const e = this.pending[0];
|
|
1799
2747
|
let call;
|
|
1800
2748
|
try {
|
|
1801
|
-
call = this.resolveArgs(e);
|
|
2749
|
+
call = await this.resolveArgs(e);
|
|
1802
2750
|
} catch {
|
|
1803
2751
|
break;
|
|
1804
2752
|
}
|
|
2753
|
+
if (call === "skip") {
|
|
2754
|
+
this.pending.shift();
|
|
2755
|
+
continue;
|
|
2756
|
+
}
|
|
1805
2757
|
let res;
|
|
1806
2758
|
try {
|
|
1807
2759
|
res = await this.call(call.tool, call.args);
|
|
@@ -1815,6 +2767,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
1815
2767
|
this.prodIdByKey.set(e.runKey, run.id);
|
|
1816
2768
|
this.persistMap();
|
|
1817
2769
|
}
|
|
2770
|
+
} else {
|
|
2771
|
+
const evId = res.structuredContent?.event?.id;
|
|
2772
|
+
if (evId) this.seenProdEventIds.add(evId);
|
|
1818
2773
|
}
|
|
1819
2774
|
this.pending.shift();
|
|
1820
2775
|
sent++;
|
|
@@ -1833,13 +2788,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
1833
2788
|
}
|
|
1834
2789
|
persistPending() {
|
|
1835
2790
|
try {
|
|
1836
|
-
|
|
2791
|
+
writeFileSync2(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
|
|
1837
2792
|
} catch {
|
|
1838
2793
|
}
|
|
1839
2794
|
}
|
|
1840
2795
|
persistMap() {
|
|
1841
2796
|
try {
|
|
1842
|
-
|
|
2797
|
+
writeFileSync2(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
|
|
1843
2798
|
} catch {
|
|
1844
2799
|
}
|
|
1845
2800
|
}
|
|
@@ -1863,26 +2818,20 @@ function parseDecision(v) {
|
|
|
1863
2818
|
return null;
|
|
1864
2819
|
}
|
|
1865
2820
|
}
|
|
1866
|
-
function toStamp(v) {
|
|
1867
|
-
if (typeof v !== "string" || !v) return null;
|
|
1868
|
-
const d = new Date(v);
|
|
1869
|
-
if (Number.isNaN(d.getTime())) return v.includes("T") ? v.slice(0, 19).replace("T", " ") : v;
|
|
1870
|
-
return stamp(d);
|
|
1871
|
-
}
|
|
1872
2821
|
|
|
1873
2822
|
// src/overmind.ts
|
|
1874
2823
|
init_config();
|
|
1875
2824
|
|
|
1876
2825
|
// src/heartbeat.ts
|
|
1877
|
-
import { readFileSync as
|
|
2826
|
+
import { readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1878
2827
|
function writeHeartbeat(path, now) {
|
|
1879
2828
|
const tmp = `${path}.tmp`;
|
|
1880
|
-
|
|
1881
|
-
|
|
2829
|
+
writeFileSync3(tmp, String(now));
|
|
2830
|
+
renameSync2(tmp, path);
|
|
1882
2831
|
}
|
|
1883
2832
|
function readHeartbeat(path) {
|
|
1884
2833
|
try {
|
|
1885
|
-
const ms = Number(
|
|
2834
|
+
const ms = Number(readFileSync4(path, "utf8").trim());
|
|
1886
2835
|
return Number.isFinite(ms) ? ms : null;
|
|
1887
2836
|
} catch {
|
|
1888
2837
|
return null;
|
|
@@ -1894,52 +2843,439 @@ function overmindAlive(path, now, staleMs) {
|
|
|
1894
2843
|
return now - ts < staleMs;
|
|
1895
2844
|
}
|
|
1896
2845
|
|
|
2846
|
+
// src/repoRegistry.ts
|
|
2847
|
+
init_config();
|
|
2848
|
+
import { $ } from "execa";
|
|
2849
|
+
function parseRepoFullName(remoteUrl) {
|
|
2850
|
+
const m = remoteUrl.trim().match(/^(?:https?:\/\/(?:[^@/]+@)?github\.com\/|git@github\.com:)([^/]+)\/([^/]+?)(\.git)?\/?$/i);
|
|
2851
|
+
if (!m) return null;
|
|
2852
|
+
return `${m[1]}/${m[2]}`.toLowerCase();
|
|
2853
|
+
}
|
|
2854
|
+
async function resolveRepoFullName(path) {
|
|
2855
|
+
const res = await $({ reject: false })`git -C ${path} remote get-url origin`;
|
|
2856
|
+
if (res.exitCode !== 0) return null;
|
|
2857
|
+
return parseRepoFullName(res.stdout);
|
|
2858
|
+
}
|
|
2859
|
+
async function buildRepoRegistry(paths) {
|
|
2860
|
+
const resolved = await Promise.all(
|
|
2861
|
+
paths.map(async (raw) => {
|
|
2862
|
+
const path = normalizeRepo(raw);
|
|
2863
|
+
if (!path) return null;
|
|
2864
|
+
const repoFullName = await resolveRepoFullName(path);
|
|
2865
|
+
return repoFullName ? { repoFullName, path } : null;
|
|
2866
|
+
})
|
|
2867
|
+
);
|
|
2868
|
+
return resolved.filter((e) => e !== null);
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// src/relief.ts
|
|
2872
|
+
var numEnv2 = (name, def) => {
|
|
2873
|
+
const v = Number(process.env[name]);
|
|
2874
|
+
return Number.isFinite(v) && v > 0 ? v : def;
|
|
2875
|
+
};
|
|
2876
|
+
var DEFAULT_RELIEF_CONFIG = {
|
|
2877
|
+
rampWindowMs: numEnv2("SWARM_RELIEF_RAMP_WINDOW_MS", 2e4),
|
|
2878
|
+
pauseWindowMs: numEnv2("SWARM_RELIEF_PAUSE_WINDOW_MS", 3e4),
|
|
2879
|
+
sustainMs: numEnv2("SWARM_RELIEF_SUSTAIN_MS", 15e3)
|
|
2880
|
+
};
|
|
2881
|
+
function resolveReliefConfig(cfg) {
|
|
2882
|
+
return { ...DEFAULT_RELIEF_CONFIG, ...cfg ?? {} };
|
|
2883
|
+
}
|
|
2884
|
+
var RELIEF_PAUSE_REASON = "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430: \u0445\u043E\u0441\u0442 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D \u2014 \u0434\u0440\u043E\u043D \u043F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D (SIGTERM, \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u0447\u0435\u0440\u0435\u0437 --continue), \u0432\u0435\u0440\u043D\u0451\u0442\u0441\u044F \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 \u043F\u043E\u0441\u043B\u0435 \u043D\u043E\u0440\u043C\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438 (TASWM-151)";
|
|
2885
|
+
function rampDue(lastRampAt, now, cfg) {
|
|
2886
|
+
return lastRampAt == null || now - lastRampAt >= cfg.rampWindowMs;
|
|
2887
|
+
}
|
|
2888
|
+
function pauseDue(reliefSince, lastPauseAt, now, cfg) {
|
|
2889
|
+
if (reliefSince == null || now - reliefSince < cfg.sustainMs) return false;
|
|
2890
|
+
return lastPauseAt == null || now - lastPauseAt >= cfg.pauseWindowMs;
|
|
2891
|
+
}
|
|
2892
|
+
function pickPauseCandidate(runs) {
|
|
2893
|
+
const candidates = runs.filter(
|
|
2894
|
+
(r) => r.kind === "drone" && r.status === "running" && r.pid != null && r.resolves_run_id == null && r.reviews_run_id == null
|
|
2895
|
+
);
|
|
2896
|
+
if (!candidates.length) return null;
|
|
2897
|
+
return candidates.reduce((youngest, r) => r.created_at > youngest.created_at ? r : youngest);
|
|
2898
|
+
}
|
|
2899
|
+
function pickResumeCandidate(pausedForRelief) {
|
|
2900
|
+
const candidates = pausedForRelief.filter((r) => r.kind === "drone");
|
|
2901
|
+
if (!candidates.length) return null;
|
|
2902
|
+
return candidates.reduce((oldest, r) => r.updated_at < oldest.updated_at ? r : oldest);
|
|
2903
|
+
}
|
|
2904
|
+
|
|
1897
2905
|
// src/launcher.ts
|
|
1898
2906
|
import { spawn } from "child_process";
|
|
1899
|
-
import { readdirSync, writeFileSync as
|
|
2907
|
+
import { closeSync, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync, readdirSync, statSync, writeFileSync as writeFileSync4 } from "fs";
|
|
1900
2908
|
import { homedir as homedir2 } from "os";
|
|
1901
2909
|
import { join as join2 } from "path";
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2910
|
+
var ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
|
|
2911
|
+
function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = []) {
|
|
2912
|
+
const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
|
|
2913
|
+
const base2 = [
|
|
2914
|
+
"--dangerously-skip-permissions",
|
|
2915
|
+
...ISOLATION_FLAGS,
|
|
2916
|
+
"--mcp-config",
|
|
2917
|
+
cfgPath,
|
|
2918
|
+
...pluginFlags,
|
|
2919
|
+
"--output-format",
|
|
2920
|
+
"json",
|
|
2921
|
+
"-p",
|
|
2922
|
+
prompt
|
|
2923
|
+
];
|
|
2924
|
+
const withModel = model ? ["--model", model, ...base2] : base2;
|
|
2925
|
+
return continueSession ? ["--continue", ...withModel] : withModel;
|
|
2926
|
+
}
|
|
2927
|
+
function roleDir(worktree, kind) {
|
|
2928
|
+
return join2(worktree, ".claude", "roles", kind);
|
|
2929
|
+
}
|
|
2930
|
+
function transcriptDir(worktree, projectsDir) {
|
|
2931
|
+
return join2(projectsDir, worktree.replace(/[/.]/g, "-"));
|
|
2932
|
+
}
|
|
2933
|
+
function sessionTranscriptExists(worktree, projectsDir = join2(homedir2(), ".claude", "projects")) {
|
|
2934
|
+
try {
|
|
2935
|
+
return readdirSync(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
|
|
2936
|
+
} catch {
|
|
2937
|
+
return false;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
function transcriptMtimeMs(worktree, projectsDir = join2(homedir2(), ".claude", "projects")) {
|
|
2941
|
+
const dir = transcriptDir(worktree, projectsDir);
|
|
2942
|
+
let files;
|
|
2943
|
+
try {
|
|
2944
|
+
files = readdirSync(dir);
|
|
2945
|
+
} catch {
|
|
2946
|
+
return null;
|
|
2947
|
+
}
|
|
2948
|
+
let latest = null;
|
|
2949
|
+
for (const f of files) {
|
|
2950
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
2951
|
+
try {
|
|
2952
|
+
const mtime2 = statSync(join2(dir, f)).mtimeMs;
|
|
2953
|
+
if (latest === null || mtime2 > latest) latest = mtime2;
|
|
2954
|
+
} catch {
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
return latest;
|
|
2958
|
+
}
|
|
2959
|
+
function nextRunLogPath(runLogsDir, runId) {
|
|
2960
|
+
const dir = join2(runLogsDir, String(runId));
|
|
2961
|
+
mkdirSync3(dir, { recursive: true });
|
|
2962
|
+
let files = [];
|
|
2963
|
+
try {
|
|
2964
|
+
files = readdirSync(dir);
|
|
2965
|
+
} catch {
|
|
2966
|
+
}
|
|
2967
|
+
let next = 0;
|
|
2968
|
+
for (const f of files) {
|
|
2969
|
+
const m = /^(\d+)\.json/.exec(f);
|
|
2970
|
+
if (m) next = Math.max(next, Number(m[1]) + 1);
|
|
2971
|
+
}
|
|
2972
|
+
return join2(dir, `${next}.json`);
|
|
2973
|
+
}
|
|
2974
|
+
function openNextRunLog(runLogsDir, runId) {
|
|
2975
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
2976
|
+
try {
|
|
2977
|
+
return openSync(nextRunLogPath(runLogsDir, runId), "wx");
|
|
2978
|
+
} catch (e) {
|
|
2979
|
+
if (e.code !== "EEXIST") throw e;
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
throw new Error(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0439 run-log \u0444\u0430\u0439\u043B \u0434\u043B\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${runId} (\u0433\u043E\u043D\u043A\u0430?)`);
|
|
2983
|
+
}
|
|
2984
|
+
function makeLauncher(cfg) {
|
|
2985
|
+
return ({ worktree, run, prompt, continueSession }) => {
|
|
2986
|
+
const isCompiled = cfg.overmindEntry.endsWith(".js");
|
|
2987
|
+
if (!isCompiled && !cfg.overmindEntry.endsWith(".ts")) {
|
|
2988
|
+
throw new Error(`overmindEntry \u043D\u0438 .ts, \u043D\u0438 .js: ${cfg.overmindEntry}`);
|
|
2989
|
+
}
|
|
2990
|
+
const mcpConfig = {
|
|
2991
|
+
mcpServers: {
|
|
2992
|
+
overmind: {
|
|
2993
|
+
command: isCompiled ? process.execPath : "bun",
|
|
2994
|
+
args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["run", cfg.overmindEntry, "--drone", "--run", String(run.id)],
|
|
2995
|
+
// SWARM_TASK_SLUG (TASWM-211): task_slug зафиксирован ЗДЕСЬ, на спавне — устойчив к
|
|
2996
|
+
// пересозданию локального стора между спавном и вызовами read_task (droneServer.ts
|
|
2997
|
+
// доверяет ему, а не db.getRun(run.id).task_slug, который может протухнуть/переехать).
|
|
2998
|
+
env: { SWARM_DB: cfg.dbPath, SWARM_TASK_SLUG: run.task_slug }
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
3002
|
+
const cfgPath = join2(worktree, ".swarm-mcp.json");
|
|
3003
|
+
writeFileSync4(cfgPath, JSON.stringify(mcpConfig, null, 2));
|
|
3004
|
+
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
|
|
3005
|
+
const dir = roleDir(worktree, run.kind);
|
|
3006
|
+
const pluginDirs = existsSync3(dir) ? [dir] : [];
|
|
3007
|
+
let stdio = "ignore";
|
|
3008
|
+
let outFd;
|
|
3009
|
+
if (cfg.runLogsDir) {
|
|
3010
|
+
outFd = openNextRunLog(cfg.runLogsDir, run.id);
|
|
3011
|
+
stdio = ["ignore", outFd, "ignore"];
|
|
3012
|
+
}
|
|
3013
|
+
const child = spawn(cfg.claudeBin ?? "claude", claudeArgs(cfgPath, prompt, continueSession, model, pluginDirs), {
|
|
3014
|
+
cwd: worktree,
|
|
3015
|
+
detached: true,
|
|
3016
|
+
stdio
|
|
3017
|
+
});
|
|
3018
|
+
if (outFd !== void 0) closeSync(outFd);
|
|
3019
|
+
child.unref();
|
|
3020
|
+
if (child.pid == null) throw new Error("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043D\u0435\u0442 pid)");
|
|
3021
|
+
return child.pid;
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
// src/procCleanup.ts
|
|
3026
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
3027
|
+
function sleep(ms) {
|
|
3028
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
3029
|
+
}
|
|
3030
|
+
function parseLsofCwd(output) {
|
|
3031
|
+
const rows = [];
|
|
3032
|
+
let pid = null;
|
|
3033
|
+
for (const line of output.split("\n")) {
|
|
3034
|
+
const pMatch = /^p(\d+)$/.exec(line);
|
|
3035
|
+
if (pMatch) {
|
|
3036
|
+
pid = Number(pMatch[1]);
|
|
3037
|
+
} else if (line.startsWith("n") && pid != null) {
|
|
3038
|
+
rows.push({ pid, cwd: line.slice(1) });
|
|
3039
|
+
pid = null;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
return rows;
|
|
3043
|
+
}
|
|
3044
|
+
function cwdUnder(cwd, dir) {
|
|
3045
|
+
return cwd === dir || cwd.startsWith(`${dir}/`);
|
|
3046
|
+
}
|
|
3047
|
+
function pidsWithCwdUnder(dir) {
|
|
3048
|
+
let out;
|
|
3049
|
+
try {
|
|
3050
|
+
out = execFileSync2("lsof", ["-a", "-d", "cwd", "-F", "pn"], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
|
|
3051
|
+
} catch (e) {
|
|
3052
|
+
logTransientSpawnError("lsof", e);
|
|
3053
|
+
return [];
|
|
3054
|
+
}
|
|
3055
|
+
return parseLsofCwd(out).filter((r) => cwdUnder(r.cwd, dir));
|
|
3056
|
+
}
|
|
3057
|
+
function pidAlive(pid) {
|
|
3058
|
+
try {
|
|
3059
|
+
process.kill(pid, 0);
|
|
3060
|
+
return true;
|
|
3061
|
+
} catch (e) {
|
|
3062
|
+
return e.code === "EPERM";
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
async function killPidsGraceful(pids, opts = {}) {
|
|
3066
|
+
const graceMs = opts.graceMs ?? 3e3;
|
|
3067
|
+
const pollMs = opts.pollMs ?? 150;
|
|
3068
|
+
const kill = opts.kill ?? ((pid, signal) => process.kill(pid, signal));
|
|
3069
|
+
const isAlive = opts.isAlive ?? pidAlive;
|
|
3070
|
+
const sleepFn = opts.sleepFn ?? sleep;
|
|
3071
|
+
const signalled = [];
|
|
3072
|
+
for (const pid of pids) {
|
|
3073
|
+
try {
|
|
3074
|
+
kill(pid, "SIGTERM");
|
|
3075
|
+
signalled.push(pid);
|
|
3076
|
+
} catch {
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
if (!signalled.length) return signalled;
|
|
3080
|
+
const deadline = Date.now() + graceMs;
|
|
3081
|
+
while (Date.now() < deadline && signalled.some((pid) => isAlive(pid))) {
|
|
3082
|
+
await sleepFn(pollMs);
|
|
3083
|
+
}
|
|
3084
|
+
for (const pid of signalled) {
|
|
3085
|
+
try {
|
|
3086
|
+
if (isAlive(pid)) kill(pid, "SIGKILL");
|
|
3087
|
+
} catch {
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
return signalled;
|
|
3091
|
+
}
|
|
3092
|
+
function killProcessGroup(pid, signal) {
|
|
3093
|
+
try {
|
|
3094
|
+
process.kill(-pid, signal);
|
|
3095
|
+
} catch {
|
|
3096
|
+
process.kill(pid, signal);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
function composeProjectPrefix(worktreeDirName) {
|
|
3100
|
+
return worktreeDirName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3101
|
+
}
|
|
3102
|
+
function containerBelongsToPrefix(name, prefix) {
|
|
3103
|
+
return name === prefix || name.startsWith(`${prefix}-`);
|
|
3104
|
+
}
|
|
3105
|
+
function findOrphanContainers(containerNames, runs) {
|
|
3106
|
+
const orphans = [];
|
|
3107
|
+
for (const name of containerNames) {
|
|
3108
|
+
let best = null;
|
|
3109
|
+
for (const r of runs) {
|
|
3110
|
+
if (!containerBelongsToPrefix(name, r.prefix)) continue;
|
|
3111
|
+
if (!best || r.prefix.length > best.prefix.length) best = r;
|
|
3112
|
+
}
|
|
3113
|
+
if (best && !best.active) orphans.push(name);
|
|
3114
|
+
}
|
|
3115
|
+
return orphans;
|
|
3116
|
+
}
|
|
3117
|
+
function dockerContainerNames() {
|
|
3118
|
+
try {
|
|
3119
|
+
return execFileSync2("docker", ["ps", "-a", "--format", "{{.Names}}"], { encoding: "utf8" }).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3120
|
+
} catch (e) {
|
|
3121
|
+
logTransientSpawnError("docker ps", e);
|
|
3122
|
+
return [];
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
function removeContainers(names) {
|
|
3126
|
+
const removed = [];
|
|
3127
|
+
for (const name of names) {
|
|
3128
|
+
try {
|
|
3129
|
+
execFileSync2("docker", ["rm", "-f", name], { encoding: "utf8" });
|
|
3130
|
+
removed.push(name);
|
|
3131
|
+
} catch (e) {
|
|
3132
|
+
console.warn(`[procCleanup] docker rm -f ${name} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
return removed;
|
|
3136
|
+
}
|
|
3137
|
+
async function cleanupWorktreeProcesses(worktree, opts = {}) {
|
|
3138
|
+
try {
|
|
3139
|
+
const found = pidsWithCwdUnder(worktree);
|
|
3140
|
+
if (found.length) {
|
|
3141
|
+
const killed = await killPidsGraceful(
|
|
3142
|
+
found.map((f) => f.pid),
|
|
3143
|
+
opts
|
|
3144
|
+
);
|
|
3145
|
+
if (killed.length) console.log(`[procCleanup] worktree ${worktree}: \u043F\u043E\u0433\u0430\u0448\u0435\u043D\u043E \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432 ${killed.length} (${killed.join(", ")})`);
|
|
3146
|
+
}
|
|
3147
|
+
} catch (e) {
|
|
3148
|
+
console.error(`[procCleanup] \u0433\u0430\u0448\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432 worktree ${worktree} \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C:`, e);
|
|
3149
|
+
}
|
|
3150
|
+
try {
|
|
3151
|
+
const prefix = composeProjectPrefix(worktree.split("/").filter(Boolean).pop() ?? "");
|
|
3152
|
+
if (!prefix) return;
|
|
3153
|
+
const names = dockerContainerNames().filter((n) => containerBelongsToPrefix(n, prefix));
|
|
3154
|
+
if (!names.length) return;
|
|
3155
|
+
const removed = removeContainers(names);
|
|
3156
|
+
if (removed.length) console.log(`[procCleanup] worktree ${worktree}: \u0441\u043D\u0435\u0441\u0435\u043D\u043E docker-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u0432 ${removed.length} (${removed.join(", ")})`);
|
|
3157
|
+
} catch (e) {
|
|
3158
|
+
console.error(`[procCleanup] \u0441\u043D\u043E\u0441 docker-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u0432 worktree ${worktree} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
async function sweepOrphans(params) {
|
|
3162
|
+
let killedPids = [];
|
|
3163
|
+
try {
|
|
3164
|
+
const found = pidsWithCwdUnder(params.worktreesDir);
|
|
3165
|
+
const orphanPids = found.filter((f) => ![...params.activeWorktreePaths].some((p) => cwdUnder(f.cwd, p))).map((f) => f.pid);
|
|
3166
|
+
if (orphanPids.length) {
|
|
3167
|
+
console.log(`[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep: ${orphanPids.length} \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u0432\u0448\u0438\u0445 \u043F\u0440\u043E\u0446\u0435\u0441\u0441(\u043E\u0432) \u043F\u043E\u0434 ${params.worktreesDir} \u2014 \u0433\u0430\u0448\u0443`, orphanPids);
|
|
3168
|
+
killedPids = await killPidsGraceful(orphanPids, params.killOpts);
|
|
3169
|
+
}
|
|
3170
|
+
} catch (e) {
|
|
3171
|
+
console.error("[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
3172
|
+
}
|
|
3173
|
+
let removedContainers = [];
|
|
3174
|
+
try {
|
|
3175
|
+
const names = dockerContainerNames();
|
|
3176
|
+
const orphanNames = findOrphanContainers(names, params.containerRunPrefixes);
|
|
3177
|
+
if (orphanNames.length) {
|
|
3178
|
+
console.log(`[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep: ${orphanNames.length} docker-\u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440(\u043E\u0432)-\u0441\u0438\u0440\u043E\u0442 \u2014 \u0433\u0430\u0448\u0443`, orphanNames);
|
|
3179
|
+
removedContainers = removeContainers(orphanNames);
|
|
3180
|
+
}
|
|
3181
|
+
} catch (e) {
|
|
3182
|
+
console.error("[procCleanup] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u0432 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
3183
|
+
}
|
|
3184
|
+
return { killedPids, removedContainers };
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
// src/cost.ts
|
|
3188
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2, renameSync as renameSync3 } from "fs";
|
|
3189
|
+
import { join as join3 } from "path";
|
|
3190
|
+
|
|
3191
|
+
// src/pricing.ts
|
|
3192
|
+
var MODEL_PRICING = {
|
|
3193
|
+
"claude-sonnet-5": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
3194
|
+
"claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
3195
|
+
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
|
|
3196
|
+
};
|
|
3197
|
+
var CACHE_READ_FACTOR = 0.1;
|
|
3198
|
+
var CACHE_WRITE_5M_FACTOR = 1.25;
|
|
3199
|
+
function costFromUsage(usage, model) {
|
|
3200
|
+
const pricing = model ? MODEL_PRICING[model] : void 0;
|
|
3201
|
+
if (!pricing) {
|
|
3202
|
+
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` };
|
|
3203
|
+
}
|
|
3204
|
+
const input = usage.input_tokens ?? 0;
|
|
3205
|
+
const output = usage.output_tokens ?? 0;
|
|
3206
|
+
const cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
3207
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
3208
|
+
const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
|
|
3209
|
+
return { costUsd };
|
|
1905
3210
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
3211
|
+
|
|
3212
|
+
// src/claudeResult.ts
|
|
3213
|
+
function parseClaudeResultJson(raw) {
|
|
3214
|
+
const trimmed = raw.trim();
|
|
3215
|
+
if (!trimmed) return null;
|
|
1908
3216
|
try {
|
|
1909
|
-
|
|
3217
|
+
const parsed = JSON.parse(trimmed);
|
|
3218
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
1910
3219
|
} catch {
|
|
1911
|
-
return
|
|
3220
|
+
return null;
|
|
1912
3221
|
}
|
|
1913
3222
|
}
|
|
1914
|
-
function
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
args: ["run", cfg.overmindEntry, "--drone", "--run", String(run.id)],
|
|
1921
|
-
env: { SWARM_DB: cfg.dbPath }
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
};
|
|
1925
|
-
const cfgPath = join2(worktree, ".swarm-mcp.json");
|
|
1926
|
-
writeFileSync3(cfgPath, JSON.stringify(mcpConfig, null, 2));
|
|
1927
|
-
const child = spawn(cfg.claudeBin ?? "claude", claudeArgs(cfgPath, prompt, continueSession), {
|
|
1928
|
-
cwd: worktree,
|
|
1929
|
-
detached: true,
|
|
1930
|
-
stdio: "ignore"
|
|
1931
|
-
});
|
|
1932
|
-
child.unref();
|
|
1933
|
-
if (child.pid == null) throw new Error("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043D\u0435\u0442 pid)");
|
|
1934
|
-
return child.pid;
|
|
1935
|
-
};
|
|
3223
|
+
function launchCostUsd(result, model) {
|
|
3224
|
+
if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
|
|
3225
|
+
return { costUsd: result.total_cost_usd };
|
|
3226
|
+
}
|
|
3227
|
+
if (!result.usage) return { costUsd: 0 };
|
|
3228
|
+
return costFromUsage(result.usage, model);
|
|
1936
3229
|
}
|
|
1937
3230
|
|
|
1938
|
-
// src/
|
|
1939
|
-
|
|
3231
|
+
// src/cost.ts
|
|
3232
|
+
var UNPROCESSED_LOG_RE = /^\d+\.json$/;
|
|
3233
|
+
function unprocessedLogFiles(dir) {
|
|
3234
|
+
try {
|
|
3235
|
+
return readdirSync2(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
|
|
3236
|
+
} catch {
|
|
3237
|
+
return [];
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
function accountRunCost(deps, run) {
|
|
3241
|
+
const dir = join3(deps.runLogsDir, String(run.id));
|
|
3242
|
+
const files = unprocessedLogFiles(dir);
|
|
3243
|
+
if (files.length === 0) return;
|
|
3244
|
+
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
|
|
3245
|
+
let added = 0;
|
|
3246
|
+
const processed = [];
|
|
3247
|
+
for (const f of files) {
|
|
3248
|
+
const full = join3(dir, f);
|
|
3249
|
+
let raw = null;
|
|
3250
|
+
try {
|
|
3251
|
+
raw = readFileSync5(full, "utf8");
|
|
3252
|
+
} catch {
|
|
3253
|
+
continue;
|
|
3254
|
+
}
|
|
3255
|
+
const result = parseClaudeResultJson(raw);
|
|
3256
|
+
if (!result) {
|
|
3257
|
+
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?)`);
|
|
3258
|
+
} else {
|
|
3259
|
+
const est = launchCostUsd(result, model);
|
|
3260
|
+
if (est.warning) console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${est.warning}`);
|
|
3261
|
+
added += est.costUsd;
|
|
3262
|
+
}
|
|
3263
|
+
processed.push(full);
|
|
3264
|
+
}
|
|
3265
|
+
if (added > 0) deps.db.updateRun(run.id, { cost_usd: run.cost_usd + added });
|
|
3266
|
+
for (const full of processed) {
|
|
3267
|
+
try {
|
|
3268
|
+
renameSync3(full, `${full}.done`);
|
|
3269
|
+
} catch {
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
function makeCostAccountant(deps) {
|
|
3274
|
+
return (run) => accountRunCost(deps, run);
|
|
3275
|
+
}
|
|
1940
3276
|
|
|
1941
3277
|
// src/git.ts
|
|
1942
|
-
import { $ } from "execa";
|
|
3278
|
+
import { $ as $2 } from "execa";
|
|
1943
3279
|
var WorktreeBusyError = class extends Error {
|
|
1944
3280
|
constructor(branch, worktreePath) {
|
|
1945
3281
|
super(`worktree \u0437\u0430\u043D\u044F\u0442: ${worktreePath} (\u0432\u0435\u0442\u043A\u0430 ${branch}) \u2014 \u043F\u0440\u0438\u0431\u0435\u0440\u0438 worktree \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u043D\u0438 \u043F\u0440\u043E\u0433\u043E\u043D (swarm cancel ${branch})`);
|
|
@@ -1951,10 +3287,10 @@ var WorktreeBusyError = class extends Error {
|
|
|
1951
3287
|
worktreePath;
|
|
1952
3288
|
};
|
|
1953
3289
|
async function revParse(repo, ref) {
|
|
1954
|
-
return (await $({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${ref}`).stdout.trim();
|
|
3290
|
+
return (await $2({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${ref}`).stdout.trim();
|
|
1955
3291
|
}
|
|
1956
3292
|
async function worktreeForBranch(repo, branch) {
|
|
1957
|
-
const out = (await
|
|
3293
|
+
const out = (await $2`git -C ${repo} worktree list --porcelain`).stdout;
|
|
1958
3294
|
const target = `refs/heads/${branch}`;
|
|
1959
3295
|
let current = null;
|
|
1960
3296
|
for (const line of out.split("\n")) {
|
|
@@ -1963,22 +3299,29 @@ async function worktreeForBranch(repo, branch) {
|
|
|
1963
3299
|
}
|
|
1964
3300
|
return null;
|
|
1965
3301
|
}
|
|
1966
|
-
async function addWorktree(repo, path, branch, base2
|
|
3302
|
+
async function addWorktree(repo, path, branch, base2) {
|
|
1967
3303
|
const localSha = await revParse(repo, `refs/heads/${branch}`);
|
|
1968
3304
|
const originSha = await revParse(repo, `refs/remotes/origin/${branch}`);
|
|
1969
3305
|
if (!localSha && !originSha) {
|
|
1970
|
-
|
|
3306
|
+
const freshBase = base2 ?? await freshWorktreeBase(repo);
|
|
3307
|
+
await $2`git -C ${repo} worktree add -b ${branch} ${path} ${freshBase}`;
|
|
1971
3308
|
return { reused: false };
|
|
1972
3309
|
}
|
|
1973
3310
|
const busyAt = await worktreeForBranch(repo, branch);
|
|
1974
3311
|
if (busyAt) throw new WorktreeBusyError(branch, busyAt);
|
|
1975
3312
|
if (originSha) {
|
|
1976
|
-
await
|
|
3313
|
+
await $2`git -C ${repo} worktree add -B ${branch} ${path} ${`origin/${branch}`}`;
|
|
1977
3314
|
return { reused: true, from: "origin", sha: originSha };
|
|
1978
3315
|
}
|
|
1979
|
-
await
|
|
3316
|
+
await $2`git -C ${repo} worktree add ${path} ${branch}`;
|
|
1980
3317
|
return { reused: true, from: "local" };
|
|
1981
3318
|
}
|
|
3319
|
+
async function freshWorktreeBase(repo) {
|
|
3320
|
+
const hasOrigin = (await $2({ reject: false })`git -C ${repo} remote get-url origin`).exitCode === 0;
|
|
3321
|
+
if (!hasOrigin) return "HEAD";
|
|
3322
|
+
await $2`git -C ${repo} fetch --quiet origin`;
|
|
3323
|
+
return remoteMainRef(repo);
|
|
3324
|
+
}
|
|
1982
3325
|
function worktreeSourceMessage(branch, res) {
|
|
1983
3326
|
if (!res.reused) return null;
|
|
1984
3327
|
if (res.from === "origin") return `\u0432\u0435\u0442\u043A\u0430 ${branch} \u043F\u0435\u0440\u0435\u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0430 \u0441 origin/${branch} @ ${res.sha.slice(0, 7)}`;
|
|
@@ -1986,49 +3329,56 @@ function worktreeSourceMessage(branch, res) {
|
|
|
1986
3329
|
}
|
|
1987
3330
|
async function setWorktreeIdentity(worktree, name) {
|
|
1988
3331
|
const local = name.toLowerCase().replace(/\s+/g, "-");
|
|
1989
|
-
await
|
|
1990
|
-
await
|
|
1991
|
-
await
|
|
3332
|
+
await $2`git -C ${worktree} config extensions.worktreeConfig true`;
|
|
3333
|
+
await $2`git -C ${worktree} config --worktree user.name ${`${name} (swarm drone)`}`;
|
|
3334
|
+
await $2`git -C ${worktree} config --worktree user.email ${`${local}@drones.swarm.local`}`;
|
|
1992
3335
|
}
|
|
1993
3336
|
async function removeWorktree(repo, path) {
|
|
1994
|
-
const branch = (await $({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
|
|
1995
|
-
await
|
|
1996
|
-
|
|
3337
|
+
const branch = (await $2({ reject: false })`git -C ${path} symbolic-ref --quiet --short HEAD`).stdout.trim();
|
|
3338
|
+
await cleanupWorktreeProcesses(path);
|
|
3339
|
+
await $2({ reject: false })`git -C ${repo} worktree remove --force ${path}`;
|
|
3340
|
+
if (branch) await $2({ reject: false })`git -C ${repo} branch -D ${branch}`;
|
|
1997
3341
|
}
|
|
1998
3342
|
async function worktreeDirty(path) {
|
|
1999
|
-
const status = (await $({ reject: false })`git -C ${path} status --porcelain`).stdout.trim();
|
|
3343
|
+
const status = (await $2({ reject: false })`git -C ${path} status --porcelain`).stdout.trim();
|
|
2000
3344
|
if (status) return true;
|
|
2001
|
-
const unpushed = (await $({ reject: false })`git -C ${path} rev-list --count HEAD --not --remotes`).stdout.trim();
|
|
3345
|
+
const unpushed = (await $2({ reject: false })`git -C ${path} rev-list --count HEAD --not --remotes`).stdout.trim();
|
|
2002
3346
|
return unpushed !== "" && unpushed !== "0";
|
|
2003
3347
|
}
|
|
2004
3348
|
async function remoteMainRef(repo) {
|
|
2005
|
-
const head = (await $({ reject: false })`git -C ${repo} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
|
|
3349
|
+
const head = (await $2({ reject: false })`git -C ${repo} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
|
|
2006
3350
|
if (head) return head;
|
|
2007
3351
|
for (const cand of ["origin/main", "origin/master"]) {
|
|
2008
|
-
const ok = await $({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${cand}`;
|
|
3352
|
+
const ok = await $2({ reject: false })`git -C ${repo} rev-parse --verify --quiet ${cand}`;
|
|
2009
3353
|
if (ok.exitCode === 0) return cand;
|
|
2010
3354
|
}
|
|
2011
3355
|
throw new Error(`\u043D\u0435 \u043D\u0430\u0448\u0451\u043B \u0434\u0435\u0444\u043E\u043B\u0442-\u0432\u0435\u0442\u043A\u0443 origin \u0432 ${repo} (\u043D\u0435\u0442 origin/HEAD, origin/main, origin/master)`);
|
|
2012
3356
|
}
|
|
3357
|
+
var GITHUB_REMOTE_RE = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/i;
|
|
3358
|
+
async function remoteRepoFullName(repo) {
|
|
3359
|
+
const url = (await $2({ reject: false })`git -C ${repo} remote get-url origin`).stdout.trim();
|
|
3360
|
+
const m = url.match(GITHUB_REMOTE_RE);
|
|
3361
|
+
return m ? m[1] : null;
|
|
3362
|
+
}
|
|
2013
3363
|
async function prepareDetachedWorktree(repo, branch, worktree) {
|
|
2014
|
-
await
|
|
2015
|
-
await
|
|
3364
|
+
await $2`git -C ${repo} fetch --quiet origin`;
|
|
3365
|
+
await $2`git -C ${repo} worktree add --detach --force ${worktree} ${`origin/${branch}`}`;
|
|
2016
3366
|
}
|
|
2017
3367
|
async function mergeRef(worktree, ref) {
|
|
2018
|
-
const before = (await
|
|
2019
|
-
const merge = await $({ reject: false })`git -C ${worktree} merge --no-edit ${ref}`;
|
|
3368
|
+
const before = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
|
|
3369
|
+
const merge = await $2({ reject: false })`git -C ${worktree} merge --no-edit ${ref}`;
|
|
2020
3370
|
if (merge.exitCode === 0) {
|
|
2021
|
-
const after = (await
|
|
3371
|
+
const after = (await $2`git -C ${worktree} rev-parse HEAD`).stdout.trim();
|
|
2022
3372
|
return { clean: true, conflicts: [], changed: after !== before };
|
|
2023
3373
|
}
|
|
2024
|
-
const u = (await $({ reject: false })`git -C ${worktree} diff --name-only --diff-filter=U`).stdout.trim();
|
|
3374
|
+
const u = (await $2({ reject: false })`git -C ${worktree} diff --name-only --diff-filter=U`).stdout.trim();
|
|
2025
3375
|
return { clean: false, conflicts: u ? u.split("\n") : [], changed: false };
|
|
2026
3376
|
}
|
|
2027
3377
|
async function pushHeadTo(worktree, branch) {
|
|
2028
|
-
await
|
|
3378
|
+
await $2`git -C ${worktree} push origin ${`HEAD:${branch}`}`;
|
|
2029
3379
|
}
|
|
2030
3380
|
async function worktreeLastCommitMs(worktree) {
|
|
2031
|
-
const out = (await $({ reject: false })`git -C ${worktree} log -1 --format=%ct HEAD`).stdout.trim();
|
|
3381
|
+
const out = (await $2({ reject: false })`git -C ${worktree} log -1 --format=%ct HEAD`).stdout.trim();
|
|
2032
3382
|
const sec = Number(out);
|
|
2033
3383
|
return Number.isInteger(sec) && sec > 0 ? sec * 1e3 : null;
|
|
2034
3384
|
}
|
|
@@ -2050,7 +3400,7 @@ async function spawnDrone(deps, run) {
|
|
|
2050
3400
|
throw new AreaBusyError(run.area);
|
|
2051
3401
|
}
|
|
2052
3402
|
const branch = run.task_slug;
|
|
2053
|
-
const worktree =
|
|
3403
|
+
const worktree = droneWorktreePath(deps.worktreesDir, repo, run.task_slug);
|
|
2054
3404
|
const source = await addWorktree(repo, worktree, branch);
|
|
2055
3405
|
await setWorktreeIdentity(worktree, run.name);
|
|
2056
3406
|
const sourceMsg = worktreeSourceMessage(branch, source);
|
|
@@ -2062,21 +3412,22 @@ async function spawnDrone(deps, run) {
|
|
|
2062
3412
|
}
|
|
2063
3413
|
|
|
2064
3414
|
// src/gh.ts
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
var
|
|
3415
|
+
init_src();
|
|
3416
|
+
import { $ as $3 } from "execa";
|
|
3417
|
+
var realRunner = async (args) => (await $3`gh ${args}`).stdout;
|
|
3418
|
+
var realRepoRunner = async (args, cwd) => (await $3({ cwd })`gh ${args}`).stdout;
|
|
2068
3419
|
var GH_PR_RE = /^(?:https?:\/\/)?(?:www\.)?github\.com\/[^/\s]+\/[^/\s]+\/pull\/\d+(?:[/?#].*)?$/i;
|
|
2069
3420
|
function isGithubPrUrl(url) {
|
|
2070
3421
|
return GH_PR_RE.test(url.trim());
|
|
2071
3422
|
}
|
|
2072
3423
|
function softStatus() {
|
|
2073
|
-
return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false };
|
|
3424
|
+
return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [] };
|
|
2074
3425
|
}
|
|
2075
3426
|
async function prStatus(prUrl, run = realRunner) {
|
|
2076
3427
|
if (!isGithubPrUrl(prUrl)) return softStatus();
|
|
2077
3428
|
let out;
|
|
2078
3429
|
try {
|
|
2079
|
-
out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus"]);
|
|
3430
|
+
out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files"]);
|
|
2080
3431
|
} catch {
|
|
2081
3432
|
return softStatus();
|
|
2082
3433
|
}
|
|
@@ -2095,7 +3446,9 @@ async function prStatus(prUrl, run = realRunner) {
|
|
|
2095
3446
|
mergeable,
|
|
2096
3447
|
mergeStateStatus: j.mergeStateStatus ?? "UNKNOWN",
|
|
2097
3448
|
// не смержен, открыт и GitHub уверенно говорит CONFLICTING
|
|
2098
|
-
conflicting: !merged && state === "OPEN" && mergeable === "CONFLICTING"
|
|
3449
|
+
conflicting: !merged && state === "OPEN" && mergeable === "CONFLICTING",
|
|
3450
|
+
isDraft: j.isDraft === true,
|
|
3451
|
+
files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : []
|
|
2099
3452
|
};
|
|
2100
3453
|
}
|
|
2101
3454
|
async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
|
|
@@ -2108,14 +3461,131 @@ async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
|
|
|
2108
3461
|
return [];
|
|
2109
3462
|
}
|
|
2110
3463
|
}
|
|
3464
|
+
async function mergedPrForBranch(branch, repoDir, run = realRepoRunner) {
|
|
3465
|
+
if (!branch || !repoDir) return null;
|
|
3466
|
+
try {
|
|
3467
|
+
const out = await run(["pr", "list", "--head", branch, "--state", "merged", "--json", "url", "--limit", "1"], repoDir);
|
|
3468
|
+
const arr = JSON.parse(out);
|
|
3469
|
+
const url = Array.isArray(arr) ? arr[0]?.url : void 0;
|
|
3470
|
+
return typeof url === "string" ? url : null;
|
|
3471
|
+
} catch {
|
|
3472
|
+
return null;
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
2111
3475
|
async function closePR(prUrl, run = realRunner) {
|
|
2112
3476
|
await run(["pr", "close", prUrl, "--comment", "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D \u043D\u0430 \u0434\u0440\u0443\u0433\u043E\u0439 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 (swarm)"]);
|
|
2113
3477
|
}
|
|
3478
|
+
var realCommentsRunner = async (prUrl) => (await $3`gh pr view ${prUrl} --json comments`).stdout;
|
|
3479
|
+
var VERDICT_RE = /^\s*\**\s*(APPROVE|REQUEST\s+CHANGES)\b/i;
|
|
3480
|
+
function verdictLabel(verdict) {
|
|
3481
|
+
return verdict === "approve" ? "\u2705 APPROVE" : "\u274C REQUEST CHANGES";
|
|
3482
|
+
}
|
|
3483
|
+
async function latestVerdictComment(prUrl, sinceIso, run = realCommentsRunner) {
|
|
3484
|
+
if (!isGithubPrUrl(prUrl)) return null;
|
|
3485
|
+
let out;
|
|
3486
|
+
try {
|
|
3487
|
+
out = await run(prUrl);
|
|
3488
|
+
} catch {
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3491
|
+
let j;
|
|
3492
|
+
try {
|
|
3493
|
+
j = JSON.parse(out);
|
|
3494
|
+
} catch {
|
|
3495
|
+
return null;
|
|
3496
|
+
}
|
|
3497
|
+
const comments = Array.isArray(j.comments) ? j.comments : [];
|
|
3498
|
+
const sinceMs = sqliteMs(sinceIso);
|
|
3499
|
+
for (let i = comments.length - 1; i >= 0; i--) {
|
|
3500
|
+
const c = comments[i];
|
|
3501
|
+
if (!c?.body || !c.createdAt) continue;
|
|
3502
|
+
if (Number.isFinite(sinceMs) && Date.parse(c.createdAt) < sinceMs) continue;
|
|
3503
|
+
const m = c.body.match(VERDICT_RE);
|
|
3504
|
+
if (!m) continue;
|
|
3505
|
+
const verdict = /^APPROVE/i.test(m[1]) ? "approve" : "changes";
|
|
3506
|
+
return { verdict, createdAt: c.createdAt };
|
|
3507
|
+
}
|
|
3508
|
+
return null;
|
|
3509
|
+
}
|
|
3510
|
+
var realChecksRunner = async (pr) => (await $3({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`).stdout;
|
|
3511
|
+
async function prCheckRows(pr, run) {
|
|
3512
|
+
try {
|
|
3513
|
+
const out = await run(pr);
|
|
3514
|
+
const rows = JSON.parse(out);
|
|
3515
|
+
if (!Array.isArray(rows) || rows.length === 0) return null;
|
|
3516
|
+
return rows.map((r) => ({ name: r.name ?? "?", bucket: (r.bucket ?? "").toLowerCase(), link: r.link ?? "" }));
|
|
3517
|
+
} catch {
|
|
3518
|
+
return null;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
async function ghChecks(pr, run = realChecksRunner) {
|
|
3522
|
+
const rows = await prCheckRows(pr, run);
|
|
3523
|
+
if (!rows) return null;
|
|
3524
|
+
let pass = 0, fail = 0, pending2 = 0;
|
|
3525
|
+
for (const r of rows) {
|
|
3526
|
+
if (r.bucket === "pass") pass++;
|
|
3527
|
+
else if (r.bucket === "fail" || r.bucket === "cancel") fail++;
|
|
3528
|
+
else if (r.bucket === "pending") pending2++;
|
|
3529
|
+
}
|
|
3530
|
+
const total = rows.length;
|
|
3531
|
+
let bucket, text, tone;
|
|
3532
|
+
if (fail > 0) {
|
|
3533
|
+
bucket = "fail";
|
|
3534
|
+
tone = "fail";
|
|
3535
|
+
text = `\u274C CI \u0443\u043F\u0430\u043B (${fail}/${total})`;
|
|
3536
|
+
} else if (pending2 > 0) {
|
|
3537
|
+
bucket = "pending";
|
|
3538
|
+
tone = "review";
|
|
3539
|
+
text = `\u23F3 CI \u0438\u0434\u0451\u0442 (${pass}/${total})`;
|
|
3540
|
+
} else {
|
|
3541
|
+
bucket = "pass";
|
|
3542
|
+
tone = "merge";
|
|
3543
|
+
text = `\u2705 CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u043A \u043C\u0435\u0440\u0436\u0443`;
|
|
3544
|
+
}
|
|
3545
|
+
return { bucket, text, tone, total, pass, fail, pending: pending2 };
|
|
3546
|
+
}
|
|
3547
|
+
function runIdFromLink(link) {
|
|
3548
|
+
const m = link.match(/\/actions\/runs\/(\d+)/);
|
|
3549
|
+
return m ? m[1] : null;
|
|
3550
|
+
}
|
|
3551
|
+
async function prCiStatus(pr, run = realChecksRunner) {
|
|
3552
|
+
const rows = await prCheckRows(pr, run);
|
|
3553
|
+
if (!rows) return null;
|
|
3554
|
+
const failingRows = rows.filter((r) => r.bucket === "fail" || r.bucket === "cancel");
|
|
3555
|
+
if (failingRows.length > 0) {
|
|
3556
|
+
const failing = failingRows.map((r) => r.name);
|
|
3557
|
+
const failingRunIds = [...new Set(failingRows.map((r) => runIdFromLink(r.link)).filter((id) => id != null))];
|
|
3558
|
+
return { bucket: "fail", failing, failingRunIds };
|
|
3559
|
+
}
|
|
3560
|
+
if (rows.some((r) => r.bucket === "pending")) return { bucket: "pending", failing: [], failingRunIds: [] };
|
|
3561
|
+
return { bucket: "pass", failing: [], failingRunIds: [] };
|
|
3562
|
+
}
|
|
3563
|
+
var realRunLogRunner = async (runId) => (await $3({ reject: false, timeout: 3e4, maxBuffer: 10 * 1024 * 1024 })`gh run view ${runId} --log-failed`).stdout;
|
|
3564
|
+
var CI_SUMMARY_LOG_CHARS = 4e3;
|
|
3565
|
+
var CI_SUMMARY_MAX_RUNS = 2;
|
|
3566
|
+
async function ciFailureSummary(failing, failingRunIds, logRun = realRunLogRunner) {
|
|
3567
|
+
if (!failing.length) return null;
|
|
3568
|
+
const logs = [];
|
|
3569
|
+
for (const runId of failingRunIds.slice(0, CI_SUMMARY_MAX_RUNS)) {
|
|
3570
|
+
try {
|
|
3571
|
+
const out = await logRun(runId);
|
|
3572
|
+
if (out.trim()) logs.push(out.length > CI_SUMMARY_LOG_CHARS ? `\u2026
|
|
3573
|
+
${out.slice(-CI_SUMMARY_LOG_CHARS)}` : out);
|
|
3574
|
+
} catch {
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
const header = `\u274C CI \u0443\u043F\u0430\u043B: ${failing.join(", ")}`;
|
|
3578
|
+
return logs.length ? `${header}
|
|
3579
|
+
|
|
3580
|
+
${logs.join("\n---\n")}` : header;
|
|
3581
|
+
}
|
|
2114
3582
|
|
|
2115
3583
|
// src/droneServer.ts
|
|
3584
|
+
init_taskless();
|
|
2116
3585
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2117
3586
|
import { z } from "zod";
|
|
2118
3587
|
init_config();
|
|
3588
|
+
init_src();
|
|
2119
3589
|
function freezeInstruction(slug) {
|
|
2120
3590
|
return [
|
|
2121
3591
|
"\u26D4 OVERMIND \u041D\u0415\u0414\u041E\u0421\u0422\u0423\u041F\u0415\u041D \u2014 \u0435\u0433\u043E \u043F\u0443\u043B\u044C\u0441 (heartbeat) \u0443\u0441\u0442\u0430\u0440\u0435\u043B. \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",
|
|
@@ -2156,7 +3626,7 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
2156
3626
|
`overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (\u043F\u0443\u043B\u044C\u0441 \u0443\u0441\u0442\u0430\u0440\u0435\u043B > ${Math.round(heartbeatStaleMs / 1e3)}\u0441) \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`
|
|
2157
3627
|
);
|
|
2158
3628
|
}
|
|
2159
|
-
const slug = run?.task_slug ?? "<slug>";
|
|
3629
|
+
const slug = opts.expectedTaskSlug ?? run?.task_slug ?? "<slug>";
|
|
2160
3630
|
return { content: [{ type: "text", text: freezeInstruction(slug) }] };
|
|
2161
3631
|
};
|
|
2162
3632
|
server.tool(
|
|
@@ -2169,6 +3639,47 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
2169
3639
|
return { content: [{ type: "text", text: "ok" }] };
|
|
2170
3640
|
}
|
|
2171
3641
|
);
|
|
3642
|
+
server.tool(
|
|
3643
|
+
"read_task",
|
|
3644
|
+
"\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).",
|
|
3645
|
+
{},
|
|
3646
|
+
async () => {
|
|
3647
|
+
if (!opts.taskless) {
|
|
3648
|
+
return {
|
|
3649
|
+
content: [{ type: "text", text: "Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0441\u043D\u0430\u043F\u0448\u043E\u0442\u0430 \u043D\u0435\u0442, \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u0443\u0436\u0435 \u0432 \u043F\u0440\u043E\u043C\u043F\u0442\u0435." }],
|
|
3650
|
+
isError: true
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
const run = db.getRun(runId);
|
|
3654
|
+
const slug = opts.expectedTaskSlug ?? run?.task_slug;
|
|
3655
|
+
let mismatchNote;
|
|
3656
|
+
if (opts.expectedTaskSlug && !run) {
|
|
3657
|
+
mismatchNote = `\u26A0\uFE0F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0441\u0442\u043E\u0440\u0435 \u0434\u0435\u043C\u043E\u043D\u0430 \u2014 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D slug \u0438\u0437 \u0441\u043F\u0430\u0432\u043D\u0430.`;
|
|
3658
|
+
} else if (opts.expectedTaskSlug && run && run.task_slug !== opts.expectedTaskSlug) {
|
|
3659
|
+
mismatchNote = `\u26A0\uFE0F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D #${runId} \u0441\u0435\u0439\u0447\u0430\u0441 \u0447\u0438\u0441\u043B\u0438\u0442\u0441\u044F \u043A\u0430\u043A \xAB${run.task_slug}\xBB \u2014 \u044D\u0442\u043E \u041D\u0415 \u0442\u0432\u043E\u044F \u0437\u0430\u0434\u0430\u0447\u0430 (\u0441\u0442\u043E\u0440 \u0434\u0435\u043C\u043E\u043D\u0430 \u0440\u0430\u0441\u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D, TASWM-211). \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D \u0434\u043E\u0432\u0435\u0440\u0435\u043D\u043D\u044B\u0439 slug \u0438\u0437 \u0441\u043F\u0430\u0432\u043D\u0430.`;
|
|
3660
|
+
}
|
|
3661
|
+
if (!slug) return { content: [{ type: "text", text: "\u043F\u0440\u043E\u0433\u043E\u043D \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D" }], isError: true };
|
|
3662
|
+
const task = await opts.taskless.getTask(slug);
|
|
3663
|
+
if (!task) {
|
|
3664
|
+
const notFoundText = `\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0443 ${slug} \u0438\u0437 Taskless (\u0441\u043C. \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430) \u2014 \u0440\u0430\u0431\u043E\u0442\u0430\u0439 \u043F\u043E \u0442\u043E\u043C\u0443, \u0447\u0442\u043E \u0443\u0436\u0435 \u0432 \u043F\u0440\u043E\u043C\u043F\u0442\u0435.`;
|
|
3665
|
+
return {
|
|
3666
|
+
content: [{ type: "text", text: mismatchNote ? `${mismatchNote}
|
|
3667
|
+
|
|
3668
|
+
${notFoundText}` : notFoundText }],
|
|
3669
|
+
isError: true
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3672
|
+
const context = await fetchTaskContext(opts.taskless, task);
|
|
3673
|
+
const lines = [
|
|
3674
|
+
`**${task.title}** \xB7 ${task.slug}${task.url ? ` (${task.url})` : ""}`,
|
|
3675
|
+
"",
|
|
3676
|
+
task.description || "(\u043F\u0443\u0441\u0442\u043E)"
|
|
3677
|
+
];
|
|
3678
|
+
if (context) lines.push("", context);
|
|
3679
|
+
if (mismatchNote) lines.unshift(mismatchNote, "");
|
|
3680
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
3681
|
+
}
|
|
3682
|
+
);
|
|
2172
3683
|
server.tool(
|
|
2173
3684
|
"ask",
|
|
2174
3685
|
"\u0417\u0430\u0434\u0430\u0442\u044C \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u0438 \u0434\u043E\u0436\u0434\u0430\u0442\u044C\u0441\u044F \u043E\u0442\u0432\u0435\u0442\u0430. \u0415\u0441\u043B\u0438 \u0440\u0430\u0437\u0432\u0438\u043B\u043A\u0430 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u043D\u0430\u044F \u2014 \u0434\u0430\u0439 options (2\u20134 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u0430, \u0441\u0442\u0430\u043D\u0443\u0442 \u043A\u043D\u043E\u043F\u043A\u0430\u043C\u0438); \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0432\u044B\u0431\u0435\u0440\u0435\u0442 \u0432\u0430\u0440\u0438\u0430\u043D\u0442 \u0438\u043B\u0438 \u043D\u0430\u043F\u0438\u0448\u0435\u0442 \u0441\u0432\u043E\u0451. \u041E\u043F\u0443\u0441\u0442\u0438 options \u0434\u043B\u044F \u043E\u0442\u043A\u0440\u044B\u0442\u043E\u0433\u043E \u0432\u043E\u043F\u0440\u043E\u0441\u0430. \u0412\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043E\u0442\u0432\u0435\u0442 \u0441\u0442\u0440\u043E\u043A\u043E\u0439. \u0417\u043E\u0432\u0438, \u043A\u043E\u0433\u0434\u0430 \u0443\u043F\u0451\u0440\u0441\u044F \u0432 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u2014 \u043D\u0435 \u0443\u0433\u0430\u0434\u044B\u0432\u0430\u0439.",
|
|
@@ -2208,15 +3719,46 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
2208
3719
|
);
|
|
2209
3720
|
server.tool(
|
|
2210
3721
|
"done",
|
|
2211
|
-
"\u0420\u0430\u0431\u043E\u0442\u0430 \u0441\u0434\u0430\u043D\u0430. \u0421 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C GitHub-PR (github.com/OWNER/REPO/pull/N) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0443\u0445\u043E\u0434\u0438\u0442 \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u0440\u043E\u0439 \u0434\u043E\u043C\u0435\u0440\u0436\u0438\u0442. \u0411\u0435\u0437 \u0441\u0432\u043E\u0435\u0433\u043E PR (\u0440\u0435\u0432\u044C\u044E/meta-\u0437\u0430\u0434\u0430\u0447\u0430) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u0442\u0441\u044F \u0441\u0440\u0430\u0437\u0443; \u043C\u043E\u0436\u0435\u0448\u044C \u043F\u0435\u0440\u0435\u0434\u0430\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443.",
|
|
2212
|
-
{
|
|
2213
|
-
|
|
2214
|
-
|
|
3722
|
+
"\u0420\u0430\u0431\u043E\u0442\u0430 \u0441\u0434\u0430\u043D\u0430. \u0421 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C GitHub-PR (github.com/OWNER/REPO/pull/N) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0443\u0445\u043E\u0434\u0438\u0442 \u043D\u0430 \u0440\u0435\u0432\u044C\u044E, \u0440\u043E\u0439 \u0434\u043E\u043C\u0435\u0440\u0436\u0438\u0442. \u0411\u0435\u0437 \u0441\u0432\u043E\u0435\u0433\u043E PR (\u0440\u0435\u0432\u044C\u044E/meta-\u0437\u0430\u0434\u0430\u0447\u0430) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u0432\u0435\u0440\u0448\u0430\u0435\u0442\u0441\u044F \u0441\u0440\u0430\u0437\u0443; \u043C\u043E\u0436\u0435\u0448\u044C \u043F\u0435\u0440\u0435\u0434\u0430\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443. \u0414\u0440\u043E\u043D-\u0420\u0415\u0412\u042C\u042E\u0415\u0420 (\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E, TASWM-148) \u043E\u0431\u044F\u0437\u0430\u043D \u043F\u0435\u0440\u0435\u0434\u0430\u0442\u044C verdict (\u0438 \u043B\u0443\u0447\u0448\u0435 summary) \u2014 pr_url \u0442\u043E\u0433\u0434\u0430 \u0442\u0430 \u0436\u0435 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 PR.",
|
|
3723
|
+
{
|
|
3724
|
+
pr_url: z.string().describe("\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0439 GitHub-PR; \u0434\u043B\u044F \u0437\u0430\u0434\u0430\u0447 \u0431\u0435\u0437 PR \u2014 \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443 (\u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0440\u043E\u0441\u0442\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044F)"),
|
|
3725
|
+
verdict: z.enum(["approve", "changes"]).optional().describe("\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0434\u0440\u043E\u043D\u0430-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u0438\u0442\u043E\u0433 \u043F\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u043E\u043C\u0443 PR \u2014 approve (APPROVE) \u0438\u043B\u0438 changes (REQUEST CHANGES). \u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u0435\u043D \u0434\u043B\u044F \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430."),
|
|
3726
|
+
summary: z.string().optional().describe("\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0434\u0440\u043E\u043D\u0430-\u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430: \u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B/\u043D\u0430\u0448\u0451\u043B \u2014 \u0443\u0439\u0434\u0451\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A.")
|
|
3727
|
+
},
|
|
3728
|
+
async ({ pr_url, verdict, summary }) => {
|
|
3729
|
+
const run = db.getRun(runId);
|
|
3730
|
+
if (run?.kind === "hero") {
|
|
2215
3731
|
return {
|
|
2216
3732
|
content: [{ type: "text", text: "\u0413\u0435\u0440\u043E\u0439-\u043B\u0435\u0439\u0442\u0435\u043D\u0430\u043D\u0442 \u041D\u0415 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u044D\u043F\u0438\u043A \u0441\u0430\u043C \u0447\u0435\u0440\u0435\u0437 done \u2014 \u044D\u043F\u0438\u043A \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0435\u0442\u0441\u044F, \u043A\u043E\u0433\u0434\u0430 \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u044B \u0432\u0441\u0435 \u0435\u0433\u043E \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0438. \u0421\u0434\u0435\u043B\u0430\u043B \u0440\u044B\u0432\u043E\u043A (\u0441\u043E\u0437\u0434\u0430\u043B \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0438 / \u0440\u0430\u0437\u0434\u0430\u043B \u043E\u0442\u0432\u0435\u0442\u044B) \u2014 \u0437\u043E\u0432\u0438 rest(summary) \u0438 \u0437\u0430\u043C\u0440\u0438 \u0434\u043E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u043E\u0442\u0440\u044F\u0434\u0430." }],
|
|
2217
3733
|
isError: true
|
|
2218
3734
|
};
|
|
2219
3735
|
}
|
|
3736
|
+
if (run?.reviews_run_id != null) {
|
|
3737
|
+
if (isTerminalStatus(run.status)) {
|
|
3738
|
+
return { content: [{ type: "text", text: "\u043F\u0440\u043E\u0433\u043E\u043D \u0443\u0436\u0435 \u0437\u0430\u043A\u0440\u044B\u0442 (PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430) \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D" }] };
|
|
3739
|
+
}
|
|
3740
|
+
if (verdict !== "approve" && verdict !== "changes") {
|
|
3741
|
+
return {
|
|
3742
|
+
content: [{ type: "text", text: '\u0422\u044B \u0434\u0440\u043E\u043D-\u0440\u0435\u0432\u044C\u044E\u0435\u0440 (\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E, TASWM-148) \u2014 done() \u0442\u0440\u0435\u0431\u0443\u0435\u0442 verdict: "approve" | "changes" (\u0438\u0442\u043E\u0433 \u043F\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u043E\u043C\u0443 PR).' }],
|
|
3743
|
+
isError: true
|
|
3744
|
+
};
|
|
3745
|
+
}
|
|
3746
|
+
const reviewed = db.getRun(run.reviews_run_id);
|
|
3747
|
+
const verdictText = verdictLabel(verdict);
|
|
3748
|
+
db.updateRun(run.reviews_run_id, { review_verdict: verdict });
|
|
3749
|
+
db.addEvent(run.reviews_run_id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E \u043E\u0442 ${run.name}: ${verdictText}${summary ? ` \u2014 ${summary}` : ""}`);
|
|
3750
|
+
if (opts.taskless && reviewed) {
|
|
3751
|
+
const note = `${verdictText} \u043E\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 ${run.name} (\u043F\u0440\u043E\u0433\u043E\u043D #${runId})${summary ? `
|
|
3752
|
+
|
|
3753
|
+
${summary}` : ""}`;
|
|
3754
|
+
await opts.taskless.addComment(reviewed.task_slug, note).catch(() => {
|
|
3755
|
+
});
|
|
3756
|
+
}
|
|
3757
|
+
const trustedPrUrl = reviewed?.pr_url ?? pr_url;
|
|
3758
|
+
db.updateRun(runId, { status: "review", pr_url: trustedPrUrl });
|
|
3759
|
+
db.addEvent(runId, "pr_opened", trustedPrUrl);
|
|
3760
|
+
return { content: [{ type: "text", text: "\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0437\u0430\u043F\u0438\u0441\u0430\u043D, \u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E" }] };
|
|
3761
|
+
}
|
|
2220
3762
|
if (isGithubPrUrl(pr_url)) {
|
|
2221
3763
|
db.updateRun(runId, { status: "review", pr_url });
|
|
2222
3764
|
db.addEvent(runId, "pr_opened", pr_url);
|
|
@@ -2249,6 +3791,7 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
2249
3791
|
if (!taskless) return errText("Taskless \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (slug-only \u0440\u0435\u0436\u0438\u043C) \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u0442\u044C.");
|
|
2250
3792
|
const ep = await epic();
|
|
2251
3793
|
if (!ep) return errText(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u044D\u043F\u0438\u043A ${heroRun.task_slug} \u0438\u0437 Taskless \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430.`);
|
|
3794
|
+
if (!ep.projectId) return errText(`\u044D\u043F\u0438\u043A ${heroRun.task_slug} \u0431\u0435\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 \u2014 \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F (\u043D\u0443\u0436\u0435\u043D projectId).`);
|
|
2252
3795
|
const statusId = await taskless.backlogStatusId();
|
|
2253
3796
|
if (!statusId) return errText("\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0441\u0442\u0430\u0442\u0443\u0441 \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u0438 backlog \u0432\u043E\u0440\u043A\u0441\u043F\u0435\u0439\u0441\u0430 \u2014 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u044E (\u0440\u0438\u0441\u043A \u043F\u043E\u043F\u0430\u0441\u0442\u044C \u0441\u0440\u0430\u0437\u0443 \u0432 \xAB\u0421\u0434\u0435\u043B\u0430\u0442\u044C\xBB).");
|
|
2254
3797
|
const footer = `
|
|
@@ -2260,7 +3803,9 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
2260
3803
|
title,
|
|
2261
3804
|
description: description_md + footer,
|
|
2262
3805
|
estimateHours: estimate_hours,
|
|
2263
|
-
statusId
|
|
3806
|
+
statusId,
|
|
3807
|
+
projectId: ep.projectId
|
|
3808
|
+
// TASWM-138: наследуем проект эпика — иначе сирота не claimable
|
|
2264
3809
|
});
|
|
2265
3810
|
if (!created) return errText("Taskless \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043B \u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0443 (\u0441\u043C. \u043B\u043E\u0433 \u0434\u0435\u043C\u043E\u043D\u0430).");
|
|
2266
3811
|
db.addEvent(runId, "step", `\u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0430: ${created.slug}`);
|
|
@@ -2349,147 +3894,62 @@ async function runDroneServer(runId) {
|
|
|
2349
3894
|
const { DB_PATH: DB_PATH2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2350
3895
|
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
2351
3896
|
const db = new SwarmDb2(DB_PATH2);
|
|
2352
|
-
const taskless =
|
|
2353
|
-
const
|
|
3897
|
+
const taskless = await (await Promise.resolve().then(() => (init_taskless(), taskless_exports))).connectTaskless();
|
|
3898
|
+
const expectedTaskSlug = process.env.SWARM_TASK_SLUG || void 0;
|
|
3899
|
+
const server = buildDroneServer(db, runId, { taskless, ...expectedTaskSlug ? { expectedTaskSlug } : {} });
|
|
2354
3900
|
await server.connect(new StdioServerTransport());
|
|
2355
3901
|
}
|
|
2356
3902
|
|
|
2357
3903
|
// src/overmind.ts
|
|
2358
3904
|
init_taskless();
|
|
2359
3905
|
|
|
2360
|
-
// src/
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
};
|
|
2375
|
-
for (const e of events) {
|
|
2376
|
-
switch (e.kind) {
|
|
2377
|
-
case "step":
|
|
2378
|
-
if (phase === "queued") {
|
|
2379
|
-
push(e.at);
|
|
2380
|
-
phase = "running";
|
|
2381
|
-
}
|
|
2382
|
-
break;
|
|
2383
|
-
case "blocked":
|
|
2384
|
-
push(e.at);
|
|
2385
|
-
phase = "blocked";
|
|
2386
|
-
break;
|
|
2387
|
-
case "answered":
|
|
2388
|
-
push(e.at);
|
|
2389
|
-
phase = "running";
|
|
2390
|
-
break;
|
|
2391
|
-
case "pr_opened":
|
|
2392
|
-
push(e.at);
|
|
2393
|
-
phase = "review";
|
|
2394
|
-
break;
|
|
2395
|
-
case "merged":
|
|
2396
|
-
case "done":
|
|
2397
|
-
push(e.at);
|
|
2398
|
-
phase = "done";
|
|
2399
|
-
break;
|
|
2400
|
-
case "failed":
|
|
2401
|
-
push(e.at);
|
|
2402
|
-
phase = "failed";
|
|
2403
|
-
break;
|
|
2404
|
-
case "stalled":
|
|
2405
|
-
push(e.at);
|
|
2406
|
-
phase = "stalled";
|
|
2407
|
-
break;
|
|
3906
|
+
// src/maxDrones.ts
|
|
3907
|
+
function makeMaxDronesReader(opts) {
|
|
3908
|
+
const ttl = opts.ttlMs ?? 1e4;
|
|
3909
|
+
const now = opts.now ?? Date.now;
|
|
3910
|
+
let cached = null;
|
|
3911
|
+
let cachedAt = -Infinity;
|
|
3912
|
+
return async () => {
|
|
3913
|
+
const t = now();
|
|
3914
|
+
if (cached != null && t - cachedAt < ttl) return cached;
|
|
3915
|
+
const live = await opts.fetch().catch(() => null);
|
|
3916
|
+
if (live != null && Number.isFinite(live) && live > 0) {
|
|
3917
|
+
cached = Math.trunc(live);
|
|
3918
|
+
cachedAt = t;
|
|
3919
|
+
return cached;
|
|
2408
3920
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
return segs;
|
|
3921
|
+
return opts.fallback;
|
|
3922
|
+
};
|
|
2412
3923
|
}
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
return
|
|
2427
|
-
case "paused":
|
|
2428
|
-
return { icon: "\u{1F4A4}", text: "\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430", tone: "blocked" };
|
|
2429
|
-
case "review": {
|
|
2430
|
-
if (!pr) return { icon: "\u{1F440}", text: "review \u0431\u0435\u0437 PR", tone: "review" };
|
|
2431
|
-
if (!ci) return { icon: "\u23F3", text: "CI: \u0436\u0434\u0443 \u0441\u0442\u0430\u0442\u0443\u0441\u2026", tone: "review" };
|
|
2432
|
-
if (ci.tone === "merge") return { icon: "\u{1F535}", text: "CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u043C\u0435\u0440\u0436\u0430", tone: "merge" };
|
|
2433
|
-
return { icon: ci.tone === "fail" ? "\u274C" : "\u23F3", text: ci.text.replace(/^[^ ]+ /, ""), tone: ci.tone };
|
|
3924
|
+
|
|
3925
|
+
// src/verifyMode.ts
|
|
3926
|
+
function makeVerifyModeReader(opts) {
|
|
3927
|
+
const ttl = opts.ttlMs ?? 6e4;
|
|
3928
|
+
const now = opts.now ?? Date.now;
|
|
3929
|
+
const cache = /* @__PURE__ */ new Map();
|
|
3930
|
+
return async (repo) => {
|
|
3931
|
+
const t = now();
|
|
3932
|
+
const hit = cache.get(repo);
|
|
3933
|
+
if (hit && t - hit.at < ttl) return hit.mode;
|
|
3934
|
+
const live = await opts.fetch(repo).catch(() => null);
|
|
3935
|
+
if (live === "local" || live === "ci") {
|
|
3936
|
+
cache.set(repo, { mode: live, at: t });
|
|
3937
|
+
return live;
|
|
2434
3938
|
}
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
case "failed":
|
|
2438
|
-
return { icon: "\u274C", text: stopReason || "\u0443\u043F\u0430\u043B", tone: "fail" };
|
|
2439
|
-
case "stopped":
|
|
2440
|
-
return { icon: "\u23F9", text: stopReason || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", tone: "queued" };
|
|
2441
|
-
case "done":
|
|
2442
|
-
return { icon: "\u2705", text: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", tone: "done" };
|
|
2443
|
-
default:
|
|
2444
|
-
return { icon: "\u2022", text: status, tone: "queued" };
|
|
2445
|
-
}
|
|
2446
|
-
}
|
|
2447
|
-
function pickHeadline(lanes) {
|
|
2448
|
-
const live = lanes.filter((l2) => l2.live && !l2.obsolete);
|
|
2449
|
-
const by = (pred) => live.find(pred);
|
|
2450
|
-
const fmt = (l2, extra) => `${l2.name} \xB7 ${l2.slug} \u2014 ${extra}`;
|
|
2451
|
-
let l = by((x) => x.status === "review" && x.ci?.tone === "fail");
|
|
2452
|
-
if (l) return { icon: "\u274C", text: fmt(l, "CI \u0443\u043F\u0430\u043B \u2014 \u0447\u0438\u043D\u0438\u0442\u044C"), tone: "fail" };
|
|
2453
|
-
l = by((x) => x.status === "blocked");
|
|
2454
|
-
if (l) return { icon: "\u2753", text: fmt(l, "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430"), tone: "blocked" };
|
|
2455
|
-
l = by((x) => x.status === "stalled");
|
|
2456
|
-
if (l) return { icon: "\u26A0\uFE0F", text: fmt(l, "stalled \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0434\u0440\u043E\u043D\u0430"), tone: "stalled" };
|
|
2457
|
-
l = by((x) => x.status === "review" && x.blocker.tone === "merge");
|
|
2458
|
-
if (l) return { icon: "\u{1F535}", text: fmt(l, "CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u0436\u0434\u0451\u0442 \u043C\u0435\u0440\u0436\u0430"), tone: "merge" };
|
|
2459
|
-
l = by((x) => x.status === "queued" && x.dependsOn.some((d) => !d.met));
|
|
2460
|
-
if (l) {
|
|
2461
|
-
const dep = l.dependsOn.find((d) => !d.met);
|
|
2462
|
-
return { icon: "\u26D4", text: fmt(l, `\u0436\u0434\u0451\u0442 ${dep.slug}${dep.name ? " \xB7 " + dep.name : ""}`), tone: "dep" };
|
|
2463
|
-
}
|
|
2464
|
-
l = by((x) => x.status === "review");
|
|
2465
|
-
if (l) return { icon: "\u23F3", text: fmt(l, "review \u2014 CI \u0438\u0434\u0451\u0442"), tone: "review" };
|
|
2466
|
-
l = by((x) => x.status === "running");
|
|
2467
|
-
if (l) return { icon: "\u25B6", text: `${live.filter((x) => x.status === "running").length} \u0434\u0440\u043E\u043D\u0430 \u0432 \u0440\u0430\u0431\u043E\u0442\u0435 \u2014 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043E\u043A \u043D\u0435\u0442`, tone: "run" };
|
|
2468
|
-
return null;
|
|
3939
|
+
return opts.fallback;
|
|
3940
|
+
};
|
|
2469
3941
|
}
|
|
2470
3942
|
|
|
2471
|
-
//
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
paused: { pill: "paused", label: "\u0437\u0430\u043C\u0435\u0440", color: "--warning" },
|
|
2477
|
-
// overmind был недоступен — ждёт пробуждения (TASWM-41)
|
|
2478
|
-
review: { pill: "review", label: "\u043D\u0430 \u0440\u0435\u0432\u044C\u044E", color: "--info" },
|
|
2479
|
-
done: { pill: "done", label: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", color: "--success" },
|
|
2480
|
-
failed: { pill: "failed", label: "\u0443\u043F\u0430\u043B", color: "--destructive" },
|
|
2481
|
-
stalled: { pill: "stalled", label: "\u043F\u043E\u0433\u0438\u0431/\u0437\u0430\u0432\u0438\u0441", color: "--destructive" },
|
|
2482
|
-
storm_dead: { pill: "storm", label: "\u0448\u0442\u043E\u0440\u043C \u2014 \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D", color: "--warning" },
|
|
2483
|
-
// умер в шторм Anthropic, поднимется после отбоя (TASWM-23)
|
|
2484
|
-
stopped: { pill: "stopped", label: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", color: "--muted-foreground" },
|
|
2485
|
-
superseded: { pill: "superseded", label: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D", color: "--muted-foreground" }
|
|
2486
|
-
};
|
|
2487
|
-
var FALLBACK = STATUS_META.queued;
|
|
3943
|
+
// src/web.ts
|
|
3944
|
+
init_src();
|
|
3945
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
3946
|
+
import { basename as basename2 } from "path";
|
|
3947
|
+
import { serve } from "@hono/node-server";
|
|
2488
3948
|
|
|
2489
3949
|
// src/diff.ts
|
|
2490
|
-
import { statSync } from "fs";
|
|
3950
|
+
import { statSync as statSync2 } from "fs";
|
|
2491
3951
|
import { join as join6 } from "path";
|
|
2492
|
-
import { $ as $
|
|
3952
|
+
import { $ as $4 } from "execa";
|
|
2493
3953
|
var MAX_FILES = 200;
|
|
2494
3954
|
var MAX_LINES_PER_FILE = 1500;
|
|
2495
3955
|
var MAX_UNTRACKED = 60;
|
|
@@ -2609,12 +4069,12 @@ function truncateFile(f) {
|
|
|
2609
4069
|
}
|
|
2610
4070
|
async function resolveBase(wt) {
|
|
2611
4071
|
const tryRef = async (ref) => {
|
|
2612
|
-
const sha = (await $
|
|
4072
|
+
const sha = (await $4({ reject: false })`git -C ${wt} rev-parse --verify --quiet ${ref + "^{commit}"}`).stdout.trim();
|
|
2613
4073
|
if (!sha) return null;
|
|
2614
|
-
const mb = (await $
|
|
4074
|
+
const mb = (await $4({ reject: false })`git -C ${wt} merge-base HEAD ${ref}`).stdout.trim();
|
|
2615
4075
|
return mb || null;
|
|
2616
4076
|
};
|
|
2617
|
-
const originHead = (await $
|
|
4077
|
+
const originHead = (await $4({ reject: false })`git -C ${wt} symbolic-ref --quiet --short refs/remotes/origin/HEAD`).stdout.trim();
|
|
2618
4078
|
const candidates = [originHead, "origin/main", "origin/master", "main", "master"].filter(Boolean);
|
|
2619
4079
|
for (const c of candidates) {
|
|
2620
4080
|
const mb = await tryRef(c);
|
|
@@ -2624,7 +4084,7 @@ async function resolveBase(wt) {
|
|
|
2624
4084
|
}
|
|
2625
4085
|
function mtime(path) {
|
|
2626
4086
|
try {
|
|
2627
|
-
return
|
|
4087
|
+
return statSync2(path).mtimeMs;
|
|
2628
4088
|
} catch {
|
|
2629
4089
|
return 0;
|
|
2630
4090
|
}
|
|
@@ -2633,16 +4093,16 @@ async function collectRunDiff(worktree, opts = {}) {
|
|
|
2633
4093
|
const stub = (reason) => ({ ok: false, reason, pr: opts.pr ?? null, files: [], added: 0, removed: 0 });
|
|
2634
4094
|
const gone = opts.status === "queued" ? "\u0434\u0440\u043E\u043D \u0435\u0449\u0451 \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u043A\u043E\u0434\u0430 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442" : "worktree \u0443\u0431\u0440\u0430\u043D \u2014 \u043A\u043E\u0434 \u0441\u043C\u043E\u0442\u0440\u0438 \u0432 PR";
|
|
2635
4095
|
if (!worktree) return stub(gone);
|
|
2636
|
-
const isWt = (await $
|
|
4096
|
+
const isWt = (await $4({ reject: false })`git -C ${worktree} rev-parse --is-inside-work-tree`).stdout.trim();
|
|
2637
4097
|
if (isWt !== "true") return stub(gone);
|
|
2638
4098
|
const base2 = await resolveBase(worktree);
|
|
2639
4099
|
const from = base2 ?? "HEAD";
|
|
2640
|
-
const trackedTxt = (await $
|
|
4100
|
+
const trackedTxt = (await $4({ reject: false })`git -C ${worktree} diff --no-color --find-renames ${from} --`).stdout;
|
|
2641
4101
|
let files = parseUnifiedDiff(trackedTxt);
|
|
2642
|
-
const untrackedRaw = (await $
|
|
4102
|
+
const untrackedRaw = (await $4({ reject: false })`git -C ${worktree} ls-files --others --exclude-standard -z`).stdout;
|
|
2643
4103
|
const untracked = untrackedRaw.split("\0").filter(Boolean).slice(0, MAX_UNTRACKED);
|
|
2644
4104
|
for (const rel of untracked) {
|
|
2645
|
-
const t = (await $
|
|
4105
|
+
const t = (await $4({ reject: false })`git -C ${worktree} diff --no-color --no-index -- /dev/null ${rel}`).stdout;
|
|
2646
4106
|
for (const pf of parseUnifiedDiff(t)) {
|
|
2647
4107
|
pf.status = "added";
|
|
2648
4108
|
if (!pf.path || pf.path === "/dev/null") pf.path = rel;
|
|
@@ -2671,19 +4131,17 @@ init_oauth();
|
|
|
2671
4131
|
var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
2672
4132
|
var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
|
|
2673
4133
|
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>`;
|
|
2674
|
-
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2675
|
-
var sqliteMs2 = (ts) => Date.parse(ts.replace(" ", "T") + "Z");
|
|
2676
4134
|
var sqliteIso = (ts) => {
|
|
2677
|
-
const ms =
|
|
4135
|
+
const ms = sqliteMs(ts);
|
|
2678
4136
|
return Number.isNaN(ms) ? ts : new Date(ms).toISOString();
|
|
2679
4137
|
};
|
|
2680
|
-
function roySnapshot(db, now = Date.now()) {
|
|
4138
|
+
function roySnapshot(db, now = Date.now(), overmindLog = []) {
|
|
2681
4139
|
const all = db.listRuns();
|
|
2682
4140
|
const c = (s) => all.filter((r) => r.status === s).length;
|
|
2683
4141
|
const withQ = new Set(db.runIdsWithQuestions());
|
|
2684
4142
|
const shown = all.filter((r) => {
|
|
2685
4143
|
if (r.status === "superseded") return false;
|
|
2686
|
-
if (r.status === "done" || r.status === "stopped" || r.status === "failed") return
|
|
4144
|
+
if (r.status === "done" || r.status === "stopped" || r.status === "failed") return sqliteMs(r.updated_at) >= now - DAY_MS;
|
|
2687
4145
|
return true;
|
|
2688
4146
|
});
|
|
2689
4147
|
return {
|
|
@@ -2747,13 +4205,15 @@ function roySnapshot(db, now = Date.now()) {
|
|
|
2747
4205
|
active: db.stormActive(),
|
|
2748
4206
|
since: db.currentStormStartedAt(),
|
|
2749
4207
|
dead: all.filter((r) => r.status === "storm_dead").length
|
|
2750
|
-
}
|
|
4208
|
+
},
|
|
4209
|
+
overmindLog
|
|
2751
4210
|
};
|
|
2752
4211
|
}
|
|
2753
|
-
var
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
4212
|
+
var {
|
|
4213
|
+
windowMs: TL_RECENT_TERMINAL_MS,
|
|
4214
|
+
maxTerminalLanes: TL_MAX_TERMINAL_LANES,
|
|
4215
|
+
maxAxisMs: TL_MAX_AXIS_MS
|
|
4216
|
+
} = resolveRoyWindows(process.env);
|
|
2757
4217
|
var ciCache = /* @__PURE__ */ new Map();
|
|
2758
4218
|
var CI_TTL = 2e4;
|
|
2759
4219
|
var CI_MOCK = (() => {
|
|
@@ -2769,50 +4229,18 @@ function mockCi(bucket) {
|
|
|
2769
4229
|
if (b === "pending") return { bucket: "pending", tone: "review", text: "\u23F3 CI \u0438\u0434\u0451\u0442 (1/2)", total: 2, pass: 1, fail: 0, pending: 1 };
|
|
2770
4230
|
return { bucket: "pass", tone: "merge", text: "\u2705 CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u043A \u043C\u0435\u0440\u0436\u0443", total: 2, pass: 2, fail: 0, pending: 0 };
|
|
2771
4231
|
}
|
|
2772
|
-
async function ghChecks(pr) {
|
|
2773
|
-
try {
|
|
2774
|
-
const { stdout: out } = await $4({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`;
|
|
2775
|
-
const rows = JSON.parse(out);
|
|
2776
|
-
if (!Array.isArray(rows) || rows.length === 0) return null;
|
|
2777
|
-
let pass = 0, fail = 0, pending2 = 0;
|
|
2778
|
-
for (const r of rows) {
|
|
2779
|
-
const b = (r.bucket || "").toLowerCase();
|
|
2780
|
-
if (b === "pass") pass++;
|
|
2781
|
-
else if (b === "fail" || b === "cancel") fail++;
|
|
2782
|
-
else if (b === "pending") pending2++;
|
|
2783
|
-
}
|
|
2784
|
-
const total = rows.length;
|
|
2785
|
-
let bucket, text, tone;
|
|
2786
|
-
if (fail > 0) {
|
|
2787
|
-
bucket = "fail";
|
|
2788
|
-
tone = "fail";
|
|
2789
|
-
text = `\u274C CI \u0443\u043F\u0430\u043B (${fail}/${total})`;
|
|
2790
|
-
} else if (pending2 > 0) {
|
|
2791
|
-
bucket = "pending";
|
|
2792
|
-
tone = "review";
|
|
2793
|
-
text = `\u23F3 CI \u0438\u0434\u0451\u0442 (${pass}/${total})`;
|
|
2794
|
-
} else {
|
|
2795
|
-
bucket = "pass";
|
|
2796
|
-
tone = "merge";
|
|
2797
|
-
text = `\u2705 CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u043A \u043C\u0435\u0440\u0436\u0443`;
|
|
2798
|
-
}
|
|
2799
|
-
return { bucket, text, tone, total, pass, fail, pending: pending2 };
|
|
2800
|
-
} catch {
|
|
2801
|
-
return null;
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
4232
|
async function refreshCI(prs) {
|
|
2805
4233
|
await Promise.all(prs.map(async (pr) => {
|
|
2806
4234
|
const ci = await ghChecks(pr);
|
|
2807
4235
|
ciCache.set(pr, { ci, at: Date.now() });
|
|
2808
4236
|
}));
|
|
2809
4237
|
}
|
|
2810
|
-
function timelineState(db, now = Date.now()) {
|
|
4238
|
+
function timelineState(db, now = Date.now(), overmindLog = []) {
|
|
2811
4239
|
const runs = db.listRuns().slice().sort((a, b) => a.id - b.id);
|
|
2812
4240
|
const doneNameBySlug = /* @__PURE__ */ new Map();
|
|
2813
4241
|
const activeBySlug = /* @__PURE__ */ new Map();
|
|
2814
4242
|
for (const r of runs) {
|
|
2815
|
-
if (!
|
|
4243
|
+
if (!isTerminalStatus(r.status)) activeBySlug.set(r.task_slug, { id: r.id, name: r.name });
|
|
2816
4244
|
if (r.status === "done") doneNameBySlug.set(r.task_slug, r.name);
|
|
2817
4245
|
}
|
|
2818
4246
|
const openQ = /* @__PURE__ */ new Map();
|
|
@@ -2822,9 +4250,9 @@ function timelineState(db, now = Date.now()) {
|
|
|
2822
4250
|
const lanes = [];
|
|
2823
4251
|
const reviewPRs = [];
|
|
2824
4252
|
for (const r of runs) {
|
|
2825
|
-
const live = !
|
|
4253
|
+
const live = !isTerminalStatus(r.status);
|
|
2826
4254
|
const recencyGated = !live || r.status === "stalled";
|
|
2827
|
-
if (recencyGated &&
|
|
4255
|
+
if (recencyGated && sqliteMs(r.updated_at) < cutoff) continue;
|
|
2828
4256
|
const events = db.events(r.id).map((e) => ({ kind: e.kind, text: e.text ?? null, at: sqliteIso(e.at) }));
|
|
2829
4257
|
const steps = events.filter((e) => e.kind === "step" && e.text).map((e) => ({ at: e.at, text: e.text }));
|
|
2830
4258
|
const lastStep = steps.length ? steps[steps.length - 1].text : null;
|
|
@@ -2864,7 +4292,11 @@ function timelineState(db, now = Date.now()) {
|
|
|
2864
4292
|
ci,
|
|
2865
4293
|
pr: r.pr_url,
|
|
2866
4294
|
dependsOn: deps,
|
|
2867
|
-
cost: r.cost_usd || 0
|
|
4295
|
+
cost: r.cost_usd || 0,
|
|
4296
|
+
role: r.kind,
|
|
4297
|
+
heroRunId: r.hero_run_id,
|
|
4298
|
+
reviewVerdict: r.review_verdict,
|
|
4299
|
+
model: r.model
|
|
2868
4300
|
});
|
|
2869
4301
|
}
|
|
2870
4302
|
const liveLanes = lanes.filter((l) => l.live);
|
|
@@ -2894,7 +4326,7 @@ function timelineState(db, now = Date.now()) {
|
|
|
2894
4326
|
const headline = pickHeadline(shown);
|
|
2895
4327
|
const stale = [...new Set(reviewPRs)].filter((pr) => now - (ciCache.get(pr)?.at ?? 0) > CI_TTL);
|
|
2896
4328
|
if (stale.length) void refreshCI(stale);
|
|
2897
|
-
return { now: nowIso, axisStart, axisEnd: nowIso, counters: c, headline, lanes: shown };
|
|
4329
|
+
return { now: nowIso, axisStart, axisEnd: nowIso, counters: c, headline, lanes: shown, overmindLog };
|
|
2898
4330
|
}
|
|
2899
4331
|
var CSS = `
|
|
2900
4332
|
:root {
|
|
@@ -3032,7 +4464,7 @@ main { max-width: 1080px; margin: 0 auto; width: 100%; }
|
|
|
3032
4464
|
.pill .sd { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3033
4465
|
.pill.run { color: var(--primary); background: var(--primary-soft); } .pill.run .sd { animation: pulse 1.8s infinite; }
|
|
3034
4466
|
.pill.review { color: var(--info); background: color-mix(in oklch, var(--info) 14%, transparent); }
|
|
3035
|
-
.pill.blocked { color: var(--warning); background: color-mix(in oklch, var(--warning) 15%, transparent); }
|
|
4467
|
+
.pill.blocked, .pill.paused, .pill.storm { color: var(--warning); background: color-mix(in oklch, var(--warning) 15%, transparent); }
|
|
3036
4468
|
.pill.done { color: var(--success); background: color-mix(in oklch, var(--success) 13%, transparent); }
|
|
3037
4469
|
.pill.stalled, .pill.failed { color: var(--destructive); background: color-mix(in oklch, var(--destructive) 14%, transparent); }
|
|
3038
4470
|
.pill.stopped, .pill.queued, .pill.superseded { color: var(--muted-foreground); background: var(--secondary); }
|
|
@@ -3325,9 +4757,11 @@ var GLYPH = {
|
|
|
3325
4757
|
hex:'<path d="M12 2l8 4.6v9.2L12 21l-8-5.2V6.6L12 2z" stroke="currentColor" stroke-width="1.6" fill="none" stroke-linejoin="round"/><circle cx="12" cy="12" r="2" fill="currentColor"/>'
|
|
3326
4758
|
};
|
|
3327
4759
|
var GK = ['claw','spore','eye','hex'];
|
|
3328
|
-
|
|
4760
|
+
/* \u041A\u0430\u0440\u0442\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u2192[pill,label,color] \u0438 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u044C\u043D\u043E\u0441\u0442\u044C \u2014 \u0438\u0437 swarm-core (statusTuples/
|
|
4761
|
+
TERMINAL_STATUSES), \u0430 \u043D\u0435 \u0445\u0430\u0440\u0434\u043A\u043E\u0434-\u043A\u043E\u043F\u0438\u044F (\u043E\u043D\u0430 \u0443\u0436\u0435 \u0440\u0430\u0441\u0445\u043E\u0434\u0438\u043B\u0430\u0441\u044C: paused/storm_dead). */
|
|
4762
|
+
var ST = ${jsonForScript(statusTuples())};
|
|
3329
4763
|
var ORDER = { running:0, blocked:1, paused:2, storm_dead:3, review:4, stalled:5, queued:6, stopped:7, done:8, failed:9, superseded:10 };
|
|
3330
|
-
var TERM = {
|
|
4764
|
+
var TERM = ${jsonForScript(Object.fromEntries(TERMINAL_STATUSES.map((s) => [s, 1])))};
|
|
3331
4765
|
var EVL = { spawned:'\u0434\u0440\u043E\u043D \u0437\u0430\u043F\u0443\u0449\u0435\u043D', step:'', blocked:'\u0432\u043E\u043F\u0440\u043E\u0441 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443', answered:'\u043F\u043E\u043B\u0443\u0447\u0435\u043D \u043E\u0442\u0432\u0435\u0442', pr_opened:'PR \u043E\u0442\u043A\u0440\u044B\u0442', merged:'PR \u0441\u043C\u0435\u0440\u0436\u0451\u043D, \u0437\u0430\u0434\u0430\u0447\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u0430', done:'\u0433\u043E\u0442\u043E\u0432\u043E', failed:'\u0434\u0440\u043E\u043D \u0443\u043F\u0430\u043B', stalled:'\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441/\u043F\u043E\u0433\u0438\u0431', respawned:'\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435', paused:'\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D', resumed:'overmind \u0432\u0435\u0440\u043D\u0443\u043B\u0441\u044F \u2014 \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B', rescued:'worktree \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u2014 \u0437\u0430\u0431\u0435\u0440\u0438 \u0440\u0443\u043A\u0430\u043C\u0438', conflict:'\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 main (merge-main-in)', died:'\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440', storm_start:'\u26A1 \u043F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 \u2014 Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442', storm_end:'\u0441\u0432\u044F\u0437\u044C \u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u2014 \u0440\u043E\u0439 \u043F\u0440\u043E\u0431\u0443\u0436\u0434\u0430\u0435\u0442\u0441\u044F', storm_dead:'\u0434\u0440\u043E\u043D \u0432 \u043A\u043E\u043A\u043E\u043D\u0435 (\u0448\u0442\u043E\u0440\u043C)', stopped:'\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D', superseded:'\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D' };
|
|
3332
4766
|
var MDOT = { merged:'--success', done:'--success', pr_opened:'--info', spawned:'--muted-foreground', stalled:'--destructive', failed:'--destructive', respawned:'--warning', paused:'--warning', resumed:'--success', rescued:'--destructive', conflict:'--warning', died:'--destructive', storm_start:'--destructive', storm_end:'--success', storm_dead:'--warning', stopped:'--muted-foreground', superseded:'--muted-foreground' };
|
|
3333
4767
|
|
|
@@ -3360,7 +4794,7 @@ function cardStep(d){
|
|
|
3360
4794
|
if(d.status==='stalled') return d.reason || '\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441/\u043F\u043E\u0433\u0438\u0431';
|
|
3361
4795
|
if(d.status==='stopped') return d.stopReason || d.reason || '\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D';
|
|
3362
4796
|
if(d.status==='storm_dead') return '\u0434\u0440\u043E\u043D \u0432 \u043A\u043E\u043A\u043E\u043D\u0435 \u2014 \u043F\u0441\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0432\u044F\u0437\u044C \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430 (Anthropic \u0448\u0442\u043E\u0440\u043C\u0438\u0442), worktree \u0446\u0435\u043B, \u043F\u043E\u0434\u043D\u0438\u043C\u0435\u0442\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043E\u0442\u0431\u043E\u044F';
|
|
3363
|
-
if(d.status==='paused') return '\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430';
|
|
4797
|
+
if(d.status==='paused') return d.stopReason || '\u0437\u0430\u043C\u0435\u0440 \u2014 overmind \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D, \u0436\u0434\u0451\u0442 \u043F\u0435\u0440\u0435\u043F\u043E\u0434\u044A\u0451\u043C\u0430'; // TASWM-151: \u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u0437\u0430\u0434\u0430\u0451\u0442 \u0441\u0432\u043E\u0439 stopReason
|
|
3364
4798
|
if(d.status==='review') { var n=prNum(d.pr); return 'PR'+(n?(' #'+n):'')+' \u043D\u0430 \u0440\u0435\u0432\u044C\u044E'; }
|
|
3365
4799
|
if(d.status==='done') return '\u0441\u043C\u0435\u0440\u0436\u0435\u043D';
|
|
3366
4800
|
if(d.status==='queued') return (d.waiting&&d.waiting.length) ? '\u0436\u0434\u0451\u0442: '+d.waiting.join(', ') : '\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438 \u2014 \u0436\u0434\u0451\u0442 \u0441\u043B\u043E\u0442\u0430';
|
|
@@ -3829,14 +5263,15 @@ async function runDiff(db, id, now = Date.now()) {
|
|
|
3829
5263
|
diffCache.set(id, { at: now, payload });
|
|
3830
5264
|
return payload;
|
|
3831
5265
|
}
|
|
3832
|
-
|
|
5266
|
+
var readNarratorLog = (log) => log?.() ?? [];
|
|
5267
|
+
function eventsStream(db, narratorLog) {
|
|
3833
5268
|
let iv;
|
|
3834
5269
|
let last = "";
|
|
3835
5270
|
const stream = new ReadableStream({
|
|
3836
5271
|
start(controller) {
|
|
3837
5272
|
const enc = new TextEncoder();
|
|
3838
5273
|
const send = () => {
|
|
3839
|
-
const snap = JSON.stringify(roySnapshot(db));
|
|
5274
|
+
const snap = JSON.stringify(roySnapshot(db, Date.now(), readNarratorLog(narratorLog)));
|
|
3840
5275
|
if (snap === last) return;
|
|
3841
5276
|
last = snap;
|
|
3842
5277
|
try {
|
|
@@ -3870,11 +5305,11 @@ function makeHandler(deps) {
|
|
|
3870
5305
|
const p = url.pathname;
|
|
3871
5306
|
if (p === "/") {
|
|
3872
5307
|
const store = auth();
|
|
3873
|
-
return html(store ? renderDashboard(store.email, roySnapshot(deps.db), taskUrlBase) : renderLogin());
|
|
5308
|
+
return html(store ? renderDashboard(store.email, roySnapshot(deps.db, Date.now(), readNarratorLog(deps.narratorLog)), taskUrlBase) : renderLogin());
|
|
3874
5309
|
}
|
|
3875
|
-
if (p === "/events") return eventsStream(deps.db);
|
|
3876
|
-
if (p === "/state") return Response.json(roySnapshot(deps.db));
|
|
3877
|
-
if (p === "/timeline") return Response.json(timelineState(deps.db));
|
|
5310
|
+
if (p === "/events") return eventsStream(deps.db, deps.narratorLog);
|
|
5311
|
+
if (p === "/state") return Response.json(roySnapshot(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
|
|
5312
|
+
if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
|
|
3878
5313
|
const diffMatch = p.match(/^\/run\/(\d+)\/diff$/);
|
|
3879
5314
|
if (diffMatch) {
|
|
3880
5315
|
if (!auth()) return unauthorized();
|
|
@@ -3952,9 +5387,31 @@ function makeHandler(deps) {
|
|
|
3952
5387
|
function startWeb(deps, port = WEB_PORT) {
|
|
3953
5388
|
const handle = makeHandler(deps);
|
|
3954
5389
|
const server = serve({ fetch: handle, port, hostname: "127.0.0.1" });
|
|
5390
|
+
server.on("error", (err) => {
|
|
5391
|
+
const e = err;
|
|
5392
|
+
if (e.code === "EADDRINUSE") {
|
|
5393
|
+
console.error(`\u043F\u043E\u0440\u0442 ${port} \u0437\u0430\u043D\u044F\u0442 \u2014 \u043F\u043E\u0445\u043E\u0436\u0435, overmind \u0443\u0436\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D (\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430: http://127.0.0.1:${port}).`);
|
|
5394
|
+
} else {
|
|
5395
|
+
console.error(`\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0430\u0441\u044C: ${e.message}`);
|
|
5396
|
+
}
|
|
5397
|
+
process.exit(1);
|
|
5398
|
+
});
|
|
3955
5399
|
return { port, server };
|
|
3956
5400
|
}
|
|
3957
5401
|
|
|
5402
|
+
// src/narration.ts
|
|
5403
|
+
var RING_LIMIT = 20;
|
|
5404
|
+
function makeOvermindNarrator() {
|
|
5405
|
+
let ring = [];
|
|
5406
|
+
return {
|
|
5407
|
+
narrate(text) {
|
|
5408
|
+
ring.push({ at: (/* @__PURE__ */ new Date()).toISOString(), text });
|
|
5409
|
+
if (ring.length > RING_LIMIT) ring = ring.slice(-RING_LIMIT);
|
|
5410
|
+
},
|
|
5411
|
+
log: () => ring.slice()
|
|
5412
|
+
};
|
|
5413
|
+
}
|
|
5414
|
+
|
|
3958
5415
|
// src/probe.ts
|
|
3959
5416
|
import { spawn as spawn2 } from "child_process";
|
|
3960
5417
|
function anthropicHealthy(opts = {}) {
|
|
@@ -3972,7 +5429,7 @@ function anthropicHealthy(opts = {}) {
|
|
|
3972
5429
|
}
|
|
3973
5430
|
resolve3(ok);
|
|
3974
5431
|
};
|
|
3975
|
-
const child = spawn2(bin, ["-p", "ok", "--max-turns", "1"], { stdio: "ignore" });
|
|
5432
|
+
const child = spawn2(bin, [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"], { stdio: "ignore" });
|
|
3976
5433
|
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
3977
5434
|
child.on("error", () => finish(false));
|
|
3978
5435
|
child.on("exit", (code) => finish(code === 0));
|
|
@@ -3998,8 +5455,161 @@ async function fetchExternalStorm(url, opts = {}) {
|
|
|
3998
5455
|
}
|
|
3999
5456
|
}
|
|
4000
5457
|
|
|
5458
|
+
// src/anomalies.ts
|
|
5459
|
+
init_src();
|
|
5460
|
+
var AnomalyTracker = class {
|
|
5461
|
+
constructor(renotifyCooldownMs) {
|
|
5462
|
+
this.renotifyCooldownMs = renotifyCooldownMs;
|
|
5463
|
+
}
|
|
5464
|
+
renotifyCooldownMs;
|
|
5465
|
+
since = /* @__PURE__ */ new Map();
|
|
5466
|
+
lastFiredAt = /* @__PURE__ */ new Map();
|
|
5467
|
+
/** Кандидаты, готовые к сигналу (отлежали minDurationMs, не в кулдауне) — НИЧЕГО не коммитит. */
|
|
5468
|
+
eligible(now, candidates) {
|
|
5469
|
+
const result = [];
|
|
5470
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5471
|
+
for (const c of candidates) {
|
|
5472
|
+
seen.add(c.key);
|
|
5473
|
+
if (!this.since.has(c.key)) this.since.set(c.key, now);
|
|
5474
|
+
const startedAt = this.since.get(c.key);
|
|
5475
|
+
if (now - startedAt < c.minDurationMs) continue;
|
|
5476
|
+
const lastFired = this.lastFiredAt.get(c.key);
|
|
5477
|
+
if (lastFired != null && now - lastFired < this.renotifyCooldownMs) continue;
|
|
5478
|
+
result.push(c);
|
|
5479
|
+
}
|
|
5480
|
+
for (const k of [...this.since.keys()]) if (!seen.has(k)) this.since.delete(k);
|
|
5481
|
+
for (const k of [...this.lastFiredAt.keys()]) if (!seen.has(k)) this.lastFiredAt.delete(k);
|
|
5482
|
+
return result;
|
|
5483
|
+
}
|
|
5484
|
+
/** Доставка signal с этим key успешно ушла в этот момент — начинает кулдаун повтора. */
|
|
5485
|
+
commit(key, now) {
|
|
5486
|
+
this.lastFiredAt.set(key, now);
|
|
5487
|
+
}
|
|
5488
|
+
};
|
|
5489
|
+
var REVIEW_LIKE_STATUS = "review";
|
|
5490
|
+
var MAX_BODY_CHARS = 1800;
|
|
5491
|
+
function ageMs(now, iso) {
|
|
5492
|
+
return now - sqliteMs(iso);
|
|
5493
|
+
}
|
|
5494
|
+
function runLabel(run) {
|
|
5495
|
+
return run.name || `#${run.id}`;
|
|
5496
|
+
}
|
|
5497
|
+
function hrefOf(url) {
|
|
5498
|
+
return url ? { href: url } : {};
|
|
5499
|
+
}
|
|
5500
|
+
function joinCapped(labels) {
|
|
5501
|
+
let joined = labels.join(", ");
|
|
5502
|
+
if (joined.length <= MAX_BODY_CHARS) return joined;
|
|
5503
|
+
let cut = 0;
|
|
5504
|
+
let len = 0;
|
|
5505
|
+
for (; cut < labels.length; cut++) {
|
|
5506
|
+
len += labels[cut].length + 2;
|
|
5507
|
+
if (len > MAX_BODY_CHARS) break;
|
|
5508
|
+
}
|
|
5509
|
+
joined = labels.slice(0, Math.max(cut, 1)).join(", ");
|
|
5510
|
+
return `${joined} \u0438 \u0435\u0449\u0451 ${labels.length - Math.max(cut, 1)}`;
|
|
5511
|
+
}
|
|
5512
|
+
function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
5513
|
+
const candidates = [];
|
|
5514
|
+
const allRuns = db.listRuns();
|
|
5515
|
+
const recentFailed = allRuns.filter(
|
|
5516
|
+
(r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs
|
|
5517
|
+
);
|
|
5518
|
+
if (recentFailed.length >= thresholds.failedSpikeCount) {
|
|
5519
|
+
candidates.push({
|
|
5520
|
+
key: "failed_spike",
|
|
5521
|
+
minDurationMs: 0,
|
|
5522
|
+
build: () => ({
|
|
5523
|
+
title: `\u0420\u043E\u0439: ${recentFailed.length} \u0443\u043F\u0430\u0432\u0448\u0438\u0445 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 \u0437\u0430 ${Math.round(thresholds.failedSpikeWindowMs / 6e4)} \u043C\u0438\u043D`,
|
|
5524
|
+
body: `${joinCapped(recentFailed.map(runLabel))} \u2014 \u0440\u043E\u0441\u0442 failed \u0437\u0430 \u043E\u043A\u043D\u043E, \u043F\u043E\u0445\u043E\u0436\u0435 \u043D\u0430 \u0441\u0438\u0441\u0442\u0435\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0443\u044E \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0443, \u0430 \u043D\u0435 \u0435\u0434\u0438\u043D\u0438\u0447\u043D\u044B\u0439 \u0441\u0431\u043E\u0439.`
|
|
5525
|
+
})
|
|
5526
|
+
});
|
|
5527
|
+
}
|
|
5528
|
+
for (const run of allRuns) {
|
|
5529
|
+
if (run.status !== "stalled") continue;
|
|
5530
|
+
candidates.push({
|
|
5531
|
+
key: `run_stalled:${run.id}`,
|
|
5532
|
+
minDurationMs: 0,
|
|
5533
|
+
// само по себе попадание в stalled УЖЕ прошло свой STALL_MS/RESPAWN_LIMIT
|
|
5534
|
+
build: () => ({
|
|
5535
|
+
runId: run.id,
|
|
5536
|
+
title: `\u0414\u0440\u043E\u043D ${runLabel(run)} \u0437\u0430\u0432\u0438\u0441`,
|
|
5537
|
+
body: `\u041F\u0440\u043E\u0433\u043E\u043D ${run.task_slug} \u043F\u043E\u043C\u0435\u0447\u0435\u043D stalled \u2014 \u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u043E, \u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440.`,
|
|
5538
|
+
...hrefOf(run.task_url)
|
|
5539
|
+
})
|
|
5540
|
+
});
|
|
5541
|
+
}
|
|
5542
|
+
const queuedCount = allRuns.filter((r) => r.status === "queued").length;
|
|
5543
|
+
const activeDroneCount = allRuns.filter((r) => r.kind !== "hero" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
|
|
5544
|
+
if (queuedCount > 0 && activeDroneCount === 0) {
|
|
5545
|
+
candidates.push({
|
|
5546
|
+
key: "queue_stalled",
|
|
5547
|
+
minDurationMs: thresholds.queueStalledMs,
|
|
5548
|
+
build: () => ({
|
|
5549
|
+
title: `\u0420\u043E\u0439 \u0441\u0442\u043E\u0438\u0442: \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u043D\u0435 \u043F\u0443\u0441\u0442\u0435\u0435\u0442`,
|
|
5550
|
+
body: `${queuedCount} \u043F\u0440\u043E\u0433\u043E\u043D(\u043E\u0432) \u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438, \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0436\u0438\u0432\u043E\u0433\u043E \u0434\u0440\u043E\u043D\u0430 \u2265${Math.round(thresholds.queueStalledMs / 6e4)} \u043C\u0438\u043D \u2014 \u043F\u043E\u0445\u043E\u0436\u0435, \u0433\u0435\u0439\u0442 (\u0440\u0435\u0441\u0443\u0440\u0441\u044B/maxDrones/\u0448\u0442\u043E\u0440\u043C) \u0434\u0435\u0440\u0436\u0438\u0442 \u0438\u043D\u0442\u0435\u0439\u043A \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0434\u043E\u043B\u0433\u043E.`
|
|
5551
|
+
})
|
|
5552
|
+
});
|
|
5553
|
+
}
|
|
5554
|
+
for (const run of allRuns) {
|
|
5555
|
+
if (run.status !== REVIEW_LIKE_STATUS) continue;
|
|
5556
|
+
if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
|
|
5557
|
+
if (!run.pr_url) continue;
|
|
5558
|
+
if (run.review_verdict === "approve") {
|
|
5559
|
+
candidates.push({
|
|
5560
|
+
key: `pr_stuck_merge:${run.id}`,
|
|
5561
|
+
minDurationMs: thresholds.prStuckMergeMs,
|
|
5562
|
+
build: () => ({
|
|
5563
|
+
runId: run.id,
|
|
5564
|
+
title: `PR ${runLabel(run)} \u043E\u0434\u043E\u0431\u0440\u0435\u043D, \u043D\u043E \u043D\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D`,
|
|
5565
|
+
body: `${run.task_slug}: \u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0434\u0430\u043B APPROVE \u2265${Math.round(thresholds.prStuckMergeMs / 6e4)} \u043C\u0438\u043D \u043D\u0430\u0437\u0430\u0434, PR \u0432\u0441\u0451 \u0435\u0449\u0451 \u043E\u0442\u043A\u0440\u044B\u0442 \u2014 ${run.pr_url}.`,
|
|
5566
|
+
...hrefOf(run.task_url)
|
|
5567
|
+
})
|
|
5568
|
+
});
|
|
5569
|
+
} else if (opts.autoReviewEnabled && run.review_verdict == null && db.reviewerCountFor(run.id) === 0) {
|
|
5570
|
+
candidates.push({
|
|
5571
|
+
key: `pr_no_reviewer:${run.id}`,
|
|
5572
|
+
minDurationMs: thresholds.prNoReviewerMs,
|
|
5573
|
+
build: () => ({
|
|
5574
|
+
runId: run.id,
|
|
5575
|
+
title: `PR ${runLabel(run)} \u0431\u0435\u0437 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430`,
|
|
5576
|
+
body: `${run.task_slug}: PR \u043E\u0442\u043A\u0440\u044B\u0442 \u2265${Math.round(thresholds.prNoReviewerMs / 6e4)} \u043C\u0438\u043D, \u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E\u0435\u0440 \u0442\u0430\u043A \u0438 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0441\u044F (SWARM_REVIEW_CAP/maxDrones/cutoff?) \u2014 ${run.pr_url}.`,
|
|
5577
|
+
...hrefOf(run.task_url)
|
|
5578
|
+
})
|
|
5579
|
+
});
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
const runsById = new Map(allRuns.map((r) => [r.id, r]));
|
|
5583
|
+
for (const q of db.openQuestions()) {
|
|
5584
|
+
const run = runsById.get(q.run_id);
|
|
5585
|
+
candidates.push({
|
|
5586
|
+
key: `question_unanswered:${q.id}`,
|
|
5587
|
+
minDurationMs: thresholds.questionUnansweredMs,
|
|
5588
|
+
build: () => ({
|
|
5589
|
+
runId: q.run_id,
|
|
5590
|
+
title: `${run ? runLabel(run) : `\u043F\u0440\u043E\u0433\u043E\u043D #${q.run_id}`} \u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430`,
|
|
5591
|
+
body: `\u0412\u043E\u043F\u0440\u043E\u0441 \u0431\u0435\u0437 \u043E\u0442\u0432\u0435\u0442\u0430 \u2265${Math.round(thresholds.questionUnansweredMs / 6e4)} \u043C\u0438\u043D: \xAB${q.text}\xBB.`,
|
|
5592
|
+
...hrefOf(run?.task_url)
|
|
5593
|
+
})
|
|
5594
|
+
});
|
|
5595
|
+
}
|
|
5596
|
+
if (opts.outboxPendingCount != null && opts.outboxPendingCount > 0) {
|
|
5597
|
+
const pending2 = opts.outboxPendingCount;
|
|
5598
|
+
candidates.push({
|
|
5599
|
+
key: "outbox_stuck",
|
|
5600
|
+
minDurationMs: thresholds.outboxStuckMs,
|
|
5601
|
+
build: () => ({
|
|
5602
|
+
title: `Outbox \u0440\u043E\u044F \u043D\u0435 \u043F\u0443\u0441\u0442\u0435\u0435\u0442`,
|
|
5603
|
+
body: `${pending2} \u043E\u043F\u0435\u0440\u0430\u0446\u0438(\u0439) \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u043E \u0434\u043E \u043F\u0440\u043E\u0434\u0430 \u2265${Math.round(thresholds.outboxStuckMs / 6e4)} \u043C\u0438\u043D \u2014 \u043F\u0440\u043E\u0434 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0434\u043E\u043B\u044C\u0448\u0435 \u043E\u0431\u044B\u0447\u043D\u043E\u0433\u043E \u0441\u0431\u043E\u044F.`
|
|
5604
|
+
})
|
|
5605
|
+
});
|
|
5606
|
+
}
|
|
5607
|
+
return candidates;
|
|
5608
|
+
}
|
|
5609
|
+
|
|
4001
5610
|
// src/overmind.ts
|
|
4002
5611
|
init_storm();
|
|
5612
|
+
init_src();
|
|
4003
5613
|
var THIS_FILE = fileURLToPath(import.meta.url);
|
|
4004
5614
|
var OVERMIND_ENTRY = THIS_FILE;
|
|
4005
5615
|
var STALL_MS = Number(process.env.SWARM_STALL_MS) || 10 * 60 * 1e3;
|
|
@@ -4007,38 +5617,199 @@ var RESPAWN_LIMIT = Number(process.env.SWARM_RESPAWN_LIMIT) || 2;
|
|
|
4007
5617
|
var RESPAWN_YOUNG_MS = Number(process.env.SWARM_RESPAWN_YOUNG_MS) || 60 * 1e3;
|
|
4008
5618
|
var CONFLICT_RESOLVE_COOLDOWN_MS = Number(process.env.SWARM_CONFLICT_COOLDOWN_MS) || 3 * 60 * 1e3;
|
|
4009
5619
|
var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
|
|
4010
|
-
|
|
5620
|
+
var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
|
|
5621
|
+
var REVIEW_SPAWN_LIMIT = Number(process.env.SWARM_REVIEW_SPAWN_LIMIT) || 3;
|
|
5622
|
+
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";
|
|
5623
|
+
var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
|
|
5624
|
+
var CI_WAKE_COOLDOWN_MS = Number(process.env.SWARM_CI_WAKE_COOLDOWN_MS) || 3 * 60 * 1e3;
|
|
5625
|
+
var CI_CHECK_COOLDOWN_MS = Number(process.env.SWARM_CI_CHECK_COOLDOWN_MS) || 60 * 1e3;
|
|
5626
|
+
var CI_WAKE_ESCALATION_MARK = "CI-\u0430\u0432\u0442\u043E\u043F\u043E\u0447\u0438\u043D\u043A\u0430 \u043D\u0435 \u0441\u043F\u0440\u0430\u0432\u0438\u043B\u0430\u0441\u044C";
|
|
5627
|
+
function psLines(extraArgs) {
|
|
4011
5628
|
try {
|
|
4012
|
-
|
|
4013
|
-
|
|
5629
|
+
return execFileSync3("ps", ["-A", "-ww", ...extraArgs], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }).split(
|
|
5630
|
+
"\n"
|
|
5631
|
+
);
|
|
4014
5632
|
} catch (e) {
|
|
4015
|
-
|
|
5633
|
+
logTransientSpawnError("ps", e);
|
|
5634
|
+
return [];
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
function parseLivePpids(lines) {
|
|
5638
|
+
const set = /* @__PURE__ */ new Set();
|
|
5639
|
+
for (const line of lines) {
|
|
5640
|
+
const m = line.trim().match(/^(\d+)\s+(\S+)$/);
|
|
5641
|
+
if (!m || m[2].includes("Z")) continue;
|
|
5642
|
+
const ppid = Number(m[1]);
|
|
5643
|
+
if (ppid > 0) set.add(ppid);
|
|
5644
|
+
}
|
|
5645
|
+
return set;
|
|
5646
|
+
}
|
|
5647
|
+
function livePpids() {
|
|
5648
|
+
return parseLivePpids(psLines(["-o", "ppid=,stat="]));
|
|
5649
|
+
}
|
|
5650
|
+
function hasLiveDescendant(pid) {
|
|
5651
|
+
return livePpids().has(pid);
|
|
5652
|
+
}
|
|
5653
|
+
function mcpConfigPath(args) {
|
|
5654
|
+
const m = args.match(/--mcp-config\s+(\S+\/\.swarm-mcp\.json)/);
|
|
5655
|
+
return m ? m[1] : null;
|
|
5656
|
+
}
|
|
5657
|
+
function listDroneProcesses() {
|
|
5658
|
+
const rows = [];
|
|
5659
|
+
for (const line of psLines(["-o", "pid=,args="])) {
|
|
5660
|
+
const m = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
5661
|
+
if (!m) continue;
|
|
5662
|
+
const cfg = mcpConfigPath(m[2]);
|
|
5663
|
+
if (!cfg) continue;
|
|
5664
|
+
rows.push({ pid: Number(m[1]), worktree: dirname2(cfg) });
|
|
4016
5665
|
}
|
|
5666
|
+
return rows;
|
|
4017
5667
|
}
|
|
4018
5668
|
function lastActivityMs(db, run) {
|
|
4019
5669
|
const evs = db.events(run.id);
|
|
4020
|
-
const
|
|
4021
|
-
return
|
|
5670
|
+
const last = (evs.length ? evs[evs.length - 1].at : null) ?? run.updated_at;
|
|
5671
|
+
return sqliteMs(last);
|
|
4022
5672
|
}
|
|
4023
|
-
async function livenessMs(deps, run) {
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
5673
|
+
async function livenessMs(deps, run, now, liveChildPpids) {
|
|
5674
|
+
if (run.pid != null && liveChildPpids?.has(run.pid)) return now;
|
|
5675
|
+
let ms = lastActivityMs(deps.db, run);
|
|
5676
|
+
if (!run.worktree_path) return ms;
|
|
5677
|
+
if (deps.lastCommitMs) {
|
|
5678
|
+
const commitMs = await deps.lastCommitMs(run.worktree_path).catch(() => null);
|
|
5679
|
+
if (commitMs != null) ms = Math.max(ms, commitMs);
|
|
5680
|
+
}
|
|
5681
|
+
if (deps.transcriptMtimeMs) {
|
|
5682
|
+
const transcriptMs = deps.transcriptMtimeMs(run.worktree_path);
|
|
5683
|
+
if (transcriptMs != null) ms = Math.max(ms, transcriptMs);
|
|
5684
|
+
}
|
|
5685
|
+
return ms;
|
|
4028
5686
|
}
|
|
4029
5687
|
function lastConflictAttemptMs(db, runId) {
|
|
4030
5688
|
const evs = db.events(runId).filter((e) => e.kind === "conflict");
|
|
4031
5689
|
if (!evs.length) return null;
|
|
4032
5690
|
return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
|
|
4033
5691
|
}
|
|
5692
|
+
function lastCiCheckMs(db, runId) {
|
|
5693
|
+
const evs = db.events(runId).filter((e) => e.kind === "ci_wake" || e.kind === "ci_checked");
|
|
5694
|
+
if (!evs.length) return null;
|
|
5695
|
+
return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
|
|
5696
|
+
}
|
|
5697
|
+
function ciWakeStats(db, runId) {
|
|
5698
|
+
const evs = db.events(runId).filter((e) => e.kind === "ci_wake");
|
|
5699
|
+
return {
|
|
5700
|
+
count: evs.length,
|
|
5701
|
+
lastMs: evs.length ? Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z") : null,
|
|
5702
|
+
escalated: evs.some((e) => (e.text ?? "").includes(CI_WAKE_ESCALATION_MARK))
|
|
5703
|
+
};
|
|
5704
|
+
}
|
|
4034
5705
|
var IN_FLIGHT_STATUSES = /* @__PURE__ */ new Set(["queued", "running", "blocked"]);
|
|
4035
5706
|
function countInFlight(db) {
|
|
4036
5707
|
return db.listRuns().filter((r) => r.kind !== "hero" && IN_FLIGHT_STATUSES.has(r.status)).length;
|
|
4037
5708
|
}
|
|
5709
|
+
function countActiveDrones(db) {
|
|
5710
|
+
return db.listRuns().filter((r) => r.kind !== "hero" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
|
|
5711
|
+
}
|
|
5712
|
+
function countActiveReviewers(db) {
|
|
5713
|
+
return db.listRuns().filter((r) => r.kind === "reviewer" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
|
|
5714
|
+
}
|
|
5715
|
+
async function closeReviewersOnMerge(deps, reviewRunId) {
|
|
5716
|
+
const stale = deps.db.activeReviewersFor(reviewRunId);
|
|
5717
|
+
for (const rv of stale) {
|
|
5718
|
+
if (rv.pid != null && deps.kill && deps.isAlive(rv.pid)) {
|
|
5719
|
+
try {
|
|
5720
|
+
deps.kill(rv.pid);
|
|
5721
|
+
} catch {
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
if (rv.worktree_path) await deps.removeWorktree(rv.repo, rv.worktree_path).catch(() => {
|
|
5725
|
+
});
|
|
5726
|
+
deps.db.updateRun(rv.id, { status: "done" });
|
|
5727
|
+
deps.db.addEvent(rv.id, "done", "PR \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u0440\u0430\u043D\u044C\u0448\u0435 \u0432\u0435\u0440\u0434\u0438\u043A\u0442\u0430 \u0440\u0435\u0432\u044C\u044E \u2014 \u0440\u0435\u0432\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u043D\u0443\u0436\u043D\u043E");
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
async function finalizeMergedReview(deps, run) {
|
|
5731
|
+
await deps.finishTask(run.task_slug);
|
|
5732
|
+
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
5733
|
+
});
|
|
5734
|
+
deps.db.addEvent(run.id, "merged", run.pr_url ?? "");
|
|
5735
|
+
deps.db.updateRun(run.id, { status: "done" });
|
|
5736
|
+
deps.db.addEvent(run.id, "done");
|
|
5737
|
+
await closeReviewersOnMerge(deps, run.id);
|
|
5738
|
+
}
|
|
5739
|
+
function doneMergedPrForSlug(db, slug) {
|
|
5740
|
+
const done = db.listRuns("done").find((r) => r.task_slug === slug && r.pr_url && r.kind !== "hero" && r.resolves_run_id == null && r.reviews_run_id == null);
|
|
5741
|
+
return done?.pr_url ?? null;
|
|
5742
|
+
}
|
|
5743
|
+
async function closeDuplicateTaskRun(deps, run, mergedUrl) {
|
|
5744
|
+
if (run.pid != null && deps.kill && deps.isAlive(run.pid)) {
|
|
5745
|
+
try {
|
|
5746
|
+
deps.kill(run.pid);
|
|
5747
|
+
} catch {
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
5751
|
+
});
|
|
5752
|
+
const reason = `PR \u0437\u0430\u0434\u0430\u0447\u0438 \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D (${mergedUrl}) \u2014 \u043F\u0440\u043E\u0433\u043E\u043D-\u0434\u0443\u0431\u043B\u044C \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u0441\u043F\u0430\u0432\u043D\u0430/\u0434\u043E\u0440\u0430\u0431\u043E\u0442\u043A\u0438 (TASWM-206)`;
|
|
5753
|
+
deps.db.updateRun(run.id, { status: "superseded", stop_reason: reason });
|
|
5754
|
+
deps.db.addEvent(run.id, "superseded", reason);
|
|
5755
|
+
}
|
|
5756
|
+
async function closeMergedTaskRuns(deps) {
|
|
5757
|
+
const bySlug = /* @__PURE__ */ new Map();
|
|
5758
|
+
for (const run of deps.db.listRuns()) {
|
|
5759
|
+
if (run.kind === "hero" || run.resolves_run_id != null || run.reviews_run_id != null) continue;
|
|
5760
|
+
let group = bySlug.get(run.task_slug);
|
|
5761
|
+
if (!group) bySlug.set(run.task_slug, group = []);
|
|
5762
|
+
group.push(run);
|
|
5763
|
+
}
|
|
5764
|
+
for (const [slug, group] of bySlug) {
|
|
5765
|
+
const nonTerminal = group.filter((r) => !isTerminalStatus(r.status));
|
|
5766
|
+
if (nonTerminal.length === 0) continue;
|
|
5767
|
+
if (nonTerminal.length === 1 && nonTerminal[0].status === "review") continue;
|
|
5768
|
+
try {
|
|
5769
|
+
const withPr = [...group].sort((a, b) => b.id - a.id).find((r) => r.pr_url);
|
|
5770
|
+
let mergedUrl = null;
|
|
5771
|
+
if (withPr) {
|
|
5772
|
+
if (withPr.status === "done") {
|
|
5773
|
+
mergedUrl = withPr.pr_url;
|
|
5774
|
+
} else {
|
|
5775
|
+
const st = await deps.prStatus(withPr.pr_url);
|
|
5776
|
+
if (st.merged) mergedUrl = withPr.pr_url;
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
if (!mergedUrl && deps.mergedPrForBranch) {
|
|
5780
|
+
mergedUrl = await deps.mergedPrForBranch(slug, group[0].repo || "").catch(() => null);
|
|
5781
|
+
}
|
|
5782
|
+
if (!mergedUrl) continue;
|
|
5783
|
+
const canonical = withPr && withPr.pr_url === mergedUrl && !isTerminalStatus(withPr.status) ? withPr : null;
|
|
5784
|
+
for (const run of nonTerminal) {
|
|
5785
|
+
if (canonical && run.id === canonical.id) {
|
|
5786
|
+
await finalizeMergedReview(deps, run);
|
|
5787
|
+
narrate(deps, `PR ${droneLabel(run)} \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 ${slug} \u0437\u0430\u043A\u0440\u044B\u0442\u0430 (\u0434\u0443\u0431\u043B\u044C-\u0440\u0430\u043D\u044B \u0440\u043E\u044F, TASWM-206).`);
|
|
5788
|
+
} else {
|
|
5789
|
+
await closeDuplicateTaskRun(deps, run, mergedUrl);
|
|
5790
|
+
narrate(deps, `${droneLabel(run)} \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 ${slug} \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u0430 \u0434\u0440\u0443\u0433\u0438\u043C \u0440\u0430\u043D\u043E\u043C, \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E \u0434\u0443\u0431\u043B\u044C.`);
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
if (!canonical) await deps.finishTask(slug).catch(() => {
|
|
5794
|
+
});
|
|
5795
|
+
} catch (e) {
|
|
5796
|
+
console.error(`[overmind] \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u0434\u0443\u0431\u043B\u0435\u0439-\u0440\u0430\u043D\u043E\u0432 \u0437\u0430\u0434\u0430\u0447\u0438 ${slug} (TASWM-206) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
}
|
|
4038
5800
|
var CONTINUE_NUDGE = "\u0422\u0432\u043E\u044F \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0430\u044F \u0441\u0435\u0441\u0441\u0438\u044F \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C (\u0441\u0435\u0442\u044C/\u043F\u0440\u043E\u0446\u0435\u0441\u0441) \u2014 \u0442\u044B headless-\u0434\u0440\u043E\u043D, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u0423 \u0416\u0415 \u0437\u0430\u0434\u0430\u0447\u0443. \u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 `git status`/`git log` \u0438 \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0441\u0432\u043E\u0438\u0445 \u0448\u0430\u0433\u043E\u0432: \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u0434\u0435\u043B\u0430\u043D\u043E \u2014 \u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438 \u0441 \u043C\u0435\u0441\u0442\u0430 \u043E\u0431\u0440\u044B\u0432\u0430. \u0414\u043E\u0432\u0435\u0434\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u0434\u043E \u043A\u043E\u043D\u0446\u0430, \u041A\u041E\u041C\u041C\u0418\u0422\u042C \u0418 \u041F\u0423\u0428\u042C \u043F\u043E\u0441\u043B\u0435 \u043A\u0430\u0436\u0434\u043E\u0433\u043E \u0448\u0430\u0433\u0430, \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url). \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 MCP-\u0442\u0443\u043B ask.";
|
|
5801
|
+
function buildCiWakeNudge(summary) {
|
|
5802
|
+
return [
|
|
5803
|
+
"\u{1F534} CI \u0443\u043F\u0430\u043B \u043D\u0430 \u0442\u0432\u043E\u0451\u043C PR \u2014 \u0440\u043E\u0439 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043B `gh pr checks` \u0438 \u043F\u0440\u0438\u0441\u043B\u0430\u043B \u0442\u0435\u0431\u0435 \u0441\u0436\u0430\u0442\u044B\u0439 \u0440\u0430\u0437\u0431\u043E\u0440 \u0444\u0435\u0439\u043B\u0430 (\u043D\u0435 \u0432\u0435\u0441\u044C \u043B\u043E\u0433, \u0442\u043E\u043A\u0435\u043D-\u0434\u0438\u0441\u0446\u0438\u043F\u043B\u0438\u043D\u0430):",
|
|
5804
|
+
"",
|
|
5805
|
+
summary,
|
|
5806
|
+
"",
|
|
5807
|
+
"\u041F\u043E\u0447\u0438\u043D\u0438 \u043F\u0440\u0438\u0447\u0438\u043D\u0443 \u0444\u0435\u0439\u043B\u0430 (\u041D\u0415 \u043F\u0435\u0440\u0435\u0434\u0435\u043B\u044B\u0432\u0430\u0439 \u0444\u0438\u0447\u0443 \u0441 \u043D\u0443\u043B\u044F), \u0437\u0430\u043A\u043E\u043C\u043C\u0438\u0442\u044C \u0438 \u0437\u0430\u043F\u0443\u0448\u044C \u0432 \u0422\u0423 \u0416\u0415 \u0432\u0435\u0442\u043A\u0443 \u2014 CI \u043F\u0435\u0440\u0435\u0433\u043E\u043D\u0438\u0442 \u0441\u0430\u043C, \u0441\u0430\u043C PR \u0442\u0440\u043E\u0433\u0430\u0442\u044C \u043D\u0435 \u043D\u0443\u0436\u043D\u043E. \u0418\u0441\u0442\u043E\u0440\u0438\u044F/force-push \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u044B. \u0420\u0430\u0437\u043E\u0431\u0440\u0430\u043B\u0441\u044F \u0438 \u0437\u0430\u043F\u0443\u0448\u0438\u043B \u2014 \u041E\u0411\u042F\u0417\u0410\u0422\u0415\u041B\u042C\u041D\u041E \u0441\u043D\u043E\u0432\u0430 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url) \u0441 \u0442\u043E\u0439 \u0436\u0435 \u0441\u0441\u044B\u043B\u043A\u043E\u0439 \u043D\u0430 PR: \u044D\u0442\u043E \u0432\u0435\u0440\u043D\u0451\u0442 \u043F\u0440\u043E\u0433\u043E\u043D \u0432 review \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E; \u0431\u0435\u0437 \u044D\u0442\u043E\u0433\u043E \u0442\u0432\u043E\u0439 \u0432\u044B\u0445\u043E\u0434 \u0440\u043E\u0439 \u043F\u0440\u0438\u043C\u0435\u0442 \u0437\u0430 \u0441\u043C\u0435\u0440\u0442\u044C \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430 \u0438 \u0437\u0440\u044F \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u0435\u0442 \u043F\u043E\u0434\u043D\u044F\u0442\u044C \u0442\u0435\u0431\u044F \u0441\u043D\u043E\u0432\u0430. \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 MCP-\u0442\u0443\u043B ask."
|
|
5808
|
+
].join("\n");
|
|
5809
|
+
}
|
|
4039
5810
|
var HERO_CONTINUE_NUDGE = "\u0422\u0435\u0431\u044F \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043B\u0438, \u0413\u0435\u0440\u043E\u0439: \u0432 \u043E\u0442\u0440\u044F\u0434\u0435 \u0447\u0442\u043E-\u0442\u043E \u043F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u043E (\u0434\u0440\u043E\u043D \u0437\u0430\u0434\u0430\u043B \u0432\u043E\u043F\u0440\u043E\u0441, \u0441\u0434\u0430\u043B PR \u0438\u043B\u0438 \u0443\u043C\u0435\u0440) \u2014 \u043B\u0438\u0431\u043E \u0442\u0435\u0431\u044F \u043F\u043E\u0437\u0432\u0430\u043B \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440. \u0422\u044B headless, \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0448\u044C \u0422\u041E\u0422 \u0416\u0415 \u044D\u043F\u0438\u043A: \u0443\u0441\u0442\u0430\u0432 \u0438 \u0442\u0435\u043B\u043E \u044D\u043F\u0438\u043A\u0430 \u0443\u0436\u0435 \u0432 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0435 \u043F\u0440\u043E\u0448\u043B\u043E\u0439 \u0441\u0435\u0441\u0441\u0438\u0438. \u0413\u043B\u044F\u043D\u044C \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043E\u0442\u0440\u044F\u0434\u0430 (squad_status) \u0438 \u0440\u0430\u0437\u0431\u0435\u0440\u0438\u0441\u044C, \u0447\u0442\u043E \u0438\u0437\u043C\u0435\u043D\u0438\u043B\u043E\u0441\u044C: \u0442\u0435\u0445\u043D\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u0434\u0440\u043E\u043D\u043E\u0432 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0439 answer_drone; \u043F\u0440\u043E\u0434\u0443\u043A\u0442\u043E\u0432\u043E\u0435/\u0434\u0438\u0437\u0430\u0439\u043D/scope \u2014 \u044D\u0441\u043A\u0430\u043B\u0438\u0440\u0443\u0439 \u0447\u0435\u0440\u0435\u0437 ask. \u041A\u043E\u0434 \u043D\u0435 \u043F\u0438\u0448\u0438, PR \u043D\u0435 \u043C\u0435\u0440\u0436\u0438, done \u041D\u0415 \u0437\u043E\u0432\u0438. \u0420\u0430\u0437\u043E\u0431\u0440\u0430\u043B\u0441\u044F \u0441 \u0440\u044B\u0432\u043A\u043E\u043C \u2014 \u0441\u043D\u043E\u0432\u0430 \u0437\u043E\u0432\u0438 rest(summary) \u0438 \u0437\u0430\u043C\u0440\u0438. \u0412\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0432 stdout \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0435\u0440\u0435\u0437 ask.";
|
|
4040
5811
|
var HEADLESS_BLOCK = "\u26D4 HEADLESS-\u0420\u0415\u0416\u0418\u041C \u2014 \u0416\u0401\u0421\u0422\u041A\u041E. \u0418\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u0447\u0430\u0442\u0430 \u041D\u0415\u0422: \u0442\u0432\u043E\u0439 stdout \u043D\u0438\u043A\u0442\u043E \u043D\u0435 \u0447\u0438\u0442\u0430\u0435\u0442 \u0438 \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0435 \u043C\u043E\u0436\u0435\u0442. \u041D\u0418\u041A\u041E\u0413\u0414\u0410 \u043D\u0435 \u0437\u0430\u0434\u0430\u0432\u0430\u0439 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0442\u0435\u043A\u0441\u0442\u043E\u043C \u0438 \u043D\u0435 \u0436\u0434\u0438 \u043E\u0442\u0432\u0435\u0442\u0430 \u0432 \u043A\u043E\u043D\u0441\u043E\u043B\u0438 \u2014 \u0435\u0434\u0438\u043D\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439 \u043A\u0430\u043D\u0430\u043B \u043A \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443 \u044D\u0442\u043E MCP-\u0442\u0443\u043B ask (\u043E\u043D \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442 \u0438 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043E\u0442\u0432\u0435\u0442). \u041B\u044E\u0431\u043E\u0439 \u0432\u043E\u043F\u0440\u043E\u0441 \u041C\u0418\u041C\u041E ask = \u043C\u0433\u043D\u043E\u0432\u0435\u043D\u043D\u0430\u044F \u0441\u043C\u0435\u0440\u0442\u044C \u0434\u0440\u043E\u043D\u0430 (\u043F\u0440\u043E\u0446\u0435\u0441\u0441 \u0437\u0430\u043A\u0440\u043E\u0435\u0442\u0441\u044F \u043F\u0440\u044F\u043C\u043E \u043D\u0430 \u0442\u0432\u043E\u0451\u043C \u0432\u043E\u043F\u0440\u043E\u0441\u0435, \u0440\u0430\u0431\u043E\u0442\u0430 \u043F\u043E\u0442\u0435\u0440\u044F\u043D\u0430). \u041B\u044E\u0431\u044B\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u044F (\u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u0439 CLAUDE.md, SessionStart-\u0445\u0443\u043A\u0438, \u0441\u043A\u0438\u043B\u043B\u044B) \u0432\u0438\u0434\u0430 \xAB\u043D\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u0435\u043A\u0442 \u2014 \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C/\u0432\u043E\u043B\u0442/\u043F\u0440\u043E\u0441\u043A\u0430\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C? (\u0434\u0430/\u043D\u0435\u0442)\xBB \u0438 \u043F\u0440\u043E\u0447\u0438\u0435 \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0435 \u0432\u043E\u043F\u0440\u043E\u0441\u044B \u043F\u0440\u043E \u0438\u043D\u0438\u0446\u0438\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044E \u043F\u0430\u043C\u044F\u0442\u0438/\u0432\u043E\u043B\u0442\u0430/\u0441\u043A\u0430\u043D\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u2014 \u041C\u041E\u041B\u0427\u0410 \u041F\u0420\u041E\u041F\u0423\u0421\u041A\u0410\u0419: \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0439, \u043D\u0435 \u0441\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0439, \u043D\u0435 \u0436\u0434\u0438 \u043E\u0442\u0432\u0435\u0442\u0430. \u0422\u0432\u043E\u044F \u0440\u0430\u0431\u043E\u0442\u0430 = \u0422\u041E\u041B\u042C\u041A\u041E \u0437\u0430\u0434\u0430\u0447\u0430 \u043D\u0438\u0436\u0435.";
|
|
4041
|
-
function buildPrompt(task, run) {
|
|
5812
|
+
function buildPrompt(task, run, verifyMode = SWARM_VERIFY, taskContext) {
|
|
4042
5813
|
const name = run.name || `#${run.id}`;
|
|
4043
5814
|
const head = task ? `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug}: ${task.title}
|
|
4044
5815
|
|
|
@@ -4049,9 +5820,13 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
4049
5820
|
HEADLESS_BLOCK,
|
|
4050
5821
|
"",
|
|
4051
5822
|
head,
|
|
5823
|
+
// TASWM-153: комменты (автор+время+текст) и содержимое привязанных документов —
|
|
5824
|
+
// спеки живут именно там, для дрона это ТЗ. Снимок на момент спавна; если оператор
|
|
5825
|
+
// допишет требование позже — read_task() ниже даёт канал дочитать актуальное.
|
|
5826
|
+
...taskContext ? ["", taskContext] : [],
|
|
4052
5827
|
"",
|
|
4053
5828
|
"\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 \u0443\u0436\u0435 \u0441\u043E\u0437\u0434\u0430\u043D\u0430 \u043F\u043E slug \u0437\u0430\u0434\u0430\u0447\u0438.",
|
|
4054
|
-
"\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; 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.",
|
|
5829
|
+
"\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\u043E\u043C\u043C\u0435\u043D\u0442\u044B+\u0434\u043E\u043A\u0438) \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.",
|
|
4055
5830
|
"\u041F\u0440\u0430\u0432\u0438\u043B\u0430: \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439 uikit/\u0433\u0430\u0439\u0434\u043B\u0430\u0439\u043D \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (\u043D\u0435\u0442 \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; \u043E\u0442\u043A\u0440\u043E\u0439 PR \u0447\u0435\u0440\u0435\u0437 gh \u0438 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url).",
|
|
4056
5831
|
`\u0412 \u041E\u041F\u0418\u0421\u0410\u041D\u0418\u0415 PR \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u0434\u043E\u0431\u0430\u0432\u044C \u0441\u0441\u044B\u043B\u043A\u0443 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443${task?.url ? ` \u2014 ${task.url}` : " \u0432 Taskless"} (\u043F\u0435\u0440\u0432\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439), \u0447\u0442\u043E\u0431\u044B \u043E\u0442 PR \u0431\u044B\u043B \u043E\u0434\u0438\u043D \u043A\u043B\u0438\u043A \u0434\u043E \u0437\u0430\u0434\u0430\u0447\u0438, \u0431\u0435\u0437 \u043F\u043E\u0438\u0441\u043A\u0430.`,
|
|
4057
5832
|
`\u041F\u043E\u043C\u0435\u0442\u044C \u0440\u0430\u0431\u043E\u0442\u0443 \u0441\u0432\u043E\u0438\u043C \u0438\u043C\u0435\u043D\u0435\u043C ${name}: \u0417\u0410\u0413\u041E\u041B\u041E\u0412\u041E\u041A PR \u043E\u043A\u0430\u043D\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u0445\u0432\u043E\u0441\u0442\u043E\u043C [${name}] (\u043D\u0430\u043F\u0440. \xABfeat(api): reserved slug swarm (${run.task_slug}) [${name}]\xBB); \u0432 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0438 PR \u0441\u0440\u0430\u0437\u0443 \u041F\u041E\u0421\u041B\u0415 \u0441\u0441\u044B\u043B\u043A\u0438 \u043D\u0430 \u0437\u0430\u0434\u0430\u0447\u0443 \u2014 \u0441\u0442\u0440\u043E\u043A\u0430 \xAB\u{1F41D} \u0414\u0440\u043E\u043D ${name} \xB7 \u043F\u0440\u043E\u0433\u043E\u043D #${run.id}\xBB.`,
|
|
@@ -4060,13 +5835,17 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
4060
5835
|
"\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:",
|
|
4061
5836
|
`\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.task_slug} (\u0432\u0435\u0442\u043A\u0430 = 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.`,
|
|
4062
5837
|
"\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.",
|
|
5838
|
+
'\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.',
|
|
4063
5839
|
"",
|
|
4064
5840
|
"\u041F\u0420\u041E\u0422\u041E\u041A\u041E\u041B \u0421\u0414\u0410\u0427\u0418 \u2014 \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0448\u0430\u0433\u0438 \u041F\u0415\u0420\u0415\u0414 done(pr_url), \u044D\u0442\u043E \u0448\u0430\u0433\u0438, \u0430 \u043D\u0435 \u043F\u043E\u0436\u0435\u043B\u0430\u043D\u0438\u044F:",
|
|
4065
|
-
"
|
|
5841
|
+
"0. \u041F\u0435\u0440\u0435\u0437\u0432\u043E\u043D\u0438 read_task() \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 \u0438\u043B\u0438 \u0432 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u0443\u0436\u0435 \u041F\u041E\u0421\u041B\u0415 \u0442\u0432\u043E\u0435\u0433\u043E \u0441\u0442\u0430\u0440\u0442\u0430 (\u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u043C\u043F\u0442 \u044D\u0442\u043E\u0433\u043E \u043D\u0435 \u0432\u0438\u0434\u0438\u0442, \u043E\u043D \u0441\u043D\u044F\u0442 \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u043F\u0440\u0438 \u0441\u043F\u0430\u0432\u043D\u0435). \u0421\u0432\u0435\u0440\u044C\u0441\u044F \u0441 \u0442\u0435\u043C, \u0447\u0442\u043E \u0432\u0435\u0440\u043D\u0443\u043B read_task, \u043F\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043C \u0441\u0434\u0430\u0432\u0430\u0442\u044C.",
|
|
5842
|
+
verifyMode === "ci" ? "1. CI-\u0440\u0435\u0436\u0438\u043C (SWARM_VERIFY=ci): \u0433\u043E\u043D\u044F\u0439 \u0442\u043E\u043B\u044C\u043A\u043E \u0431\u044B\u0441\u0442\u0440\u044B\u0439 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 `pnpm typecheck` \u043F\u0430\u043A\u0435\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0442\u044B \u0442\u0440\u043E\u0433\u0430\u043B (\u0434\u0451\u0448\u0435\u0432\u043E, \u043B\u043E\u0432\u0438\u0442 \u0442\u0443\u043F\u044B\u0435 \u043E\u0448\u0438\u0431\u043A\u0438 \u0442\u0438\u043F\u043E\u0432 \u0434\u043E \u0442\u0440\u0430\u0442\u044B CI-\u043F\u0440\u043E\u0433\u043E\u043D\u0430) \u2014 \u043A\u0440\u0430\u0441\u043D\u044B\u0439 typecheck \u0447\u0438\u043D\u0438. \u0422\u044F\u0436\u0451\u043B\u0443\u044E \u0432\u0435\u0440\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E (\u043F\u043E\u043B\u043D\u044B\u0439 \u0442\u0435\u0441\u0442-\u0441\u044C\u044E\u0442, e2e, docker-\u0441\u0431\u043E\u0440\u043A\u0438) \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E \u041D\u0415 \u0433\u043E\u043D\u044F\u0439, CI \u043F\u0440\u043E\u0433\u043E\u043D\u0438\u0442 \u0435\u0451 \u0441\u0430\u043C \u043F\u043E\u0441\u043B\u0435 push (\u0441\u043C. skill test-taskless \xABmirror CI locally\xBB \u2014 \u044D\u0442\u043E \u0442\u043E, \u0447\u0442\u043E \u0442\u0435\u043F\u0435\u0440\u044C \u0434\u0435\u043B\u0430\u0435\u0442 CI, \u0430 \u043D\u0435 \u0442\u044B); \u043F\u043E\u043B\u0430\u0433\u0430\u0439\u0441\u044F \u043D\u0430 \u043D\u0435\u0451, \u0430 \u043D\u0435 \u0434\u043E\u0434\u0443\u043C\u044B\u0432\u0430\u0439 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442." : "1. \u0417\u0435\u043B\u0451\u043D\u044B\u0439 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D \u043F\u0440\u043E\u0432\u0435\u0440\u043E\u043A \u043F\u0430\u043A\u0435\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0442\u044B \u0442\u0440\u043E\u0433\u0430\u043B: typecheck + \u0442\u0435\u0441\u0442\u044B (\u0440\u0435\u0446\u0435\u043F\u0442 \u0438 \u043A\u0440\u0438\u0442\u0435\u0440\u0438\u0439 \u0437\u0435\u043B\u0451\u043D\u043E\u0441\u0442\u0438 \u2014 \u0441\u043A\u0438\u043B\u043B test-taskless). \u041A\u0440\u0430\u0441\u043D\u043E\u0435 \u2014 \u041D\u0415 \u0441\u0434\u0430\u0432\u0430\u0439, \u0447\u0438\u043D\u0438.",
|
|
4066
5843
|
"2. `/code-review --fix` \u2014 \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0440\u0435\u0432\u044C\u044E \u0441\u0432\u043E\u0435\u0433\u043E \u0434\u0438\u0444\u0444\u0430; \u043F\u0440\u0438\u043C\u0435\u043D\u0451\u043D\u043D\u044B\u0435 \u043F\u0440\u0430\u0432\u043A\u0438 \u043F\u0435\u0440\u0435\u0447\u0438\u0442\u0430\u0439 \u0438 \u043F\u0435\u0440\u0435\u0443\u0431\u0435\u0434\u0438\u0441\u044C, \u0447\u0442\u043E \u043D\u0435 \u0441\u043B\u043E\u043C\u0430\u043B \u0444\u0438\u0447\u0443.",
|
|
4067
|
-
"3. `/verify` \u2014 \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044F\u043B \
|
|
5844
|
+
"3. `/verify` \u2014 \u0422\u041E\u041B\u042C\u041A\u041E \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044F\u043B \u0432\u0438\u0434\u0438\u043C\u044B\u0439 UI/\u0432\u0438\u0437\u0443\u0430\u043B: \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0444\u0438\u0447\u0443 \u043D\u0430 \u0436\u0438\u0432\u043E\u043C \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 (\u0441\u043A\u0438\u043B\u043B run-taskless) \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435, \u043D\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u043E \u043A\u043E\u043C\u043F\u0438\u043B\u0438\u0440\u0443\u0435\u0442\u0441\u044F.",
|
|
5845
|
+
`4. \u0421\u043A\u0440\u0438\u043D\u044B mobile+desktop \u2014 \u0422\u041E\u041B\u042C\u041A\u041E \u0435\u0441\u043B\u0438 \u043C\u0435\u043D\u044F\u043B \u0432\u0438\u0434\u0438\u043C\u044B\u0439 UI/\u0432\u0438\u0437\u0443\u0430\u043B, \u041E\u0414\u041D\u041E\u0419 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439: \`swarm shot <url> <out-prefix>\` (headless Chromium \u0441\u0430\u043C \u043E\u0442\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u043E\u0431\u0430 \u0432\u044C\u044E\u043F\u043E\u0440\u0442\u0430, \u0441\u043D\u0438\u043C\u0430\u0435\u0442 \u0438 \u0432\u044B\u0445\u043E\u0434\u0438\u0442 \u2014 \u043D\u0435 \u0434\u0435\u0440\u0436\u0438\u0442 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C). \u041F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C \u2014 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u043E\u0431\u0430 \u0444\u0430\u0439\u043B\u0430 \u043A \u0437\u0430\u0434\u0430\u0447\u0435/PR. \u23F1 \u0425\u0430\u0440\u0434-\u043B\u0438\u043C\u0438\u0442 \u043D\u0430 \u0432\u0435\u0441\u044C \u0448\u0430\u0433 \u0441\u043A\u0440\u0438\u043D\u043E\u0432 \u2014 5 \u043C\u0438\u043D\u0443\u0442. \u041D\u0415 \u0433\u043E\u043D\u044F\u0439 Playwright/\u0441\u0438\u0441\u0442\u0435\u043C\u043D\u044B\u0439 Chrome \u0432\u0440\u0443\u0447\u043D\u0443\u044E \u043F\u043E \u0448\u0430\u0433\u0430\u043C (\u043E\u0442\u043A\u0440\u044B\u043B\u2192\u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u043B\u2192\u043A\u043B\u0438\u043A\u043D\u0443\u043B\u2192\u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u043B\u2026) \u2014 \u0442\u0430\u043A \u0434\u0440\u043E\u043D \u0414\u0412\u0410\u0416\u0414\u042B \u0437\u0430\u043B\u0438\u043F\u0430\u043B \u043D\u0430 \u044D\u0442\u043E\u043C (TASWM-30, >30 \u043C\u0438\u043D stall \u0431\u0435\u0437 \u0435\u0434\u0438\u043D\u043E\u0433\u043E \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u0434\u0430\u043B\u044C\u0448\u0435) \u0432\u043C\u0435\u0441\u0442\u043E \u0442\u043E\u0433\u043E \u0447\u0442\u043E\u0431\u044B \u0441\u0434\u0430\u0442\u044C \u0433\u043E\u0442\u043E\u0432\u0443\u044E \u0440\u0430\u0431\u043E\u0442\u0443. \u041D\u0435 \u0443\u043B\u043E\u0436\u0438\u043B\u0441\u044F \u0432 \u043B\u0438\u043C\u0438\u0442 \u0438\u043B\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u043D\u0435 \u0438\u0434\u0451\u0442 \u2014 \u043F\u0440\u0438\u043B\u043E\u0436\u0438 \u0447\u0442\u043E \u0443\u0436\u0435 \u0441\u043D\u044F\u043B, \u0438\u043B\u0438 \u043F\u0440\u044F\u043C\u043E \u043D\u0430\u043F\u0438\u0448\u0438 \u0432 PR/\u0437\u0430\u0434\u0430\u0447\u0435 \xAB\u0441\u043A\u0440\u0438\u043D\u044B \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\xBB \u0438 \u0421\u0420\u0410\u0417\u0423 \u0432\u044B\u0437\u043E\u0432\u0438 done(pr_url) \u2014 \u043D\u0435 \u043F\u0435\u0440\u0435\u0438\u0433\u0440\u044B\u0432\u0430\u0439 \u043F\u043E\u043F\u044B\u0442\u043A\u0438 \u043F\u043E \u043A\u0440\u0443\u0433\u0443.`,
|
|
5846
|
+
"\u0427\u0438\u0441\u0442\u043E \u0431\u044D\u043A\u0435\u043D\u0434/\u043B\u043E\u0433\u0438\u043A\u0443/API/\u0434\u0435\u043C\u043E\u043D/CI/\u0442\u0438\u043F\u044B \u2014 \u0448\u0430\u0433\u0438 3\u20134 \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438 (\u041D\u0415 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u0439 run-taskless \u0438 \u043D\u0435 \u0441\u043D\u0438\u043C\u0430\u0439 \u0441\u043A\u0440\u0438\u043D\u044B): \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u0442\u0435\u0441\u0442\u044B (\u0448\u0430\u0433 1) + e2e \u0432 CI \u044D\u0442\u043E \u0443\u0436\u0435 \u043F\u043E\u043A\u0440\u044B\u0432\u0430\u044E\u0442; \u044D\u043A\u043E\u043D\u043E\u043C\u0438\u043C \u0432\u0440\u0435\u043C\u044F \u0438 \u0442\u043E\u043A\u0435\u043D\u044B.",
|
|
4068
5847
|
"\u0422\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u043B\u0435 \u044D\u0442\u043E\u0433\u043E \u0437\u043E\u0432\u0438 done(pr_url), \u0438 \u0432 \u0441\u0434\u0430\u0447\u0435 \u043F\u043E\u043A\u0430\u0436\u0438 \u0434\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u043E (\u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u0432\u044B\u0432\u043E\u0434/\u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0435\u043C\u043E\u0435 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435), \u0430 \u043D\u0435 \xAB\u0434\u043E\u043B\u0436\u043D\u043E \u0440\u0430\u0431\u043E\u0442\u0430\u0442\u044C\xBB.",
|
|
4069
|
-
"\u0421\u043A\u0438\u043B\u043B\u044B \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (.claude/skills: run-taskless, test-taskless, verify-before-done, swarm-merge-main, swarm-await-process, swarm-save-often) \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u044B\u0432\u0430\u044E\u0442\u0441\u044F \u0432 worktree \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u2014 \u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0441\u044F \u0438\u043C\u0438, \u043D\u0435 \u043F\u0435\u0440\u0435\u0438\u0437\u043E\u0431\u0440\u0435\u0442\u0430\u0439; \u0434\u043B\u044F \u043E\u0442\u043B\u0430\u0434\u043A\u0438 \u0431\u0430\u0433\u043E\u0432 \u2014
|
|
5848
|
+
"\u0421\u043A\u0438\u043B\u043B\u044B \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F (.claude/skills: run-taskless, test-taskless, verify-before-done, swarm-merge-main, swarm-await-process, swarm-save-often) \u043F\u043E\u0434\u0445\u0432\u0430\u0442\u044B\u0432\u0430\u044E\u0442\u0441\u044F \u0432 worktree \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u2014 \u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0441\u044F \u0438\u043C\u0438, \u043D\u0435 \u043F\u0435\u0440\u0435\u0438\u0437\u043E\u0431\u0440\u0435\u0442\u0430\u0439; \u0434\u043B\u044F \u043E\u0442\u043B\u0430\u0434\u043A\u0438 \u0431\u0430\u0433\u043E\u0432 \u2014 swarm-drone:systematic-debugging (\u0432\u0435\u043D\u0434\u043E\u0440\u0435\u043D\u043E \u0432 .claude/roles/drone, TASWM-90; \u0441\u043F\u0430\u0432\u043D \u0438\u0437\u043E\u043B\u0438\u0440\u043E\u0432\u0430\u043D \u043E\u0442 \u044E\u0437\u0435\u0440\u0441\u043A\u043E\u0433\u043E ~/.claude, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 superpowers-\u043F\u043B\u0430\u0433\u0438\u043D \u0434\u0440\u043E\u043D\u0443 \u043D\u0435 \u0432\u0438\u0434\u0435\u043D)."
|
|
4070
5849
|
];
|
|
4071
5850
|
if (run.decision) {
|
|
4072
5851
|
const d = run.decision;
|
|
@@ -4078,9 +5857,9 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
4078
5857
|
return parts.join("\n");
|
|
4079
5858
|
}
|
|
4080
5859
|
var HERO_CHARTER_PATH = join7(dirname2(THIS_FILE), "..", "docs", "HERO.md");
|
|
4081
|
-
function buildHeroPrompt(task, run) {
|
|
5860
|
+
function buildHeroPrompt(task, run, taskContext) {
|
|
4082
5861
|
const name = run.name || `#${run.id}`;
|
|
4083
|
-
const charter =
|
|
5862
|
+
const charter = readFileSync8(HERO_CHARTER_PATH, "utf8");
|
|
4084
5863
|
const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
|
|
4085
5864
|
|
|
4086
5865
|
${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).`;
|
|
@@ -4093,6 +5872,8 @@ ${task.description}` : `\u042D\u041F\u0418\u041A ${run.task_slug} (\u0442\u0435\
|
|
|
4093
5872
|
HEADLESS_BLOCK,
|
|
4094
5873
|
"",
|
|
4095
5874
|
head + epicUrl,
|
|
5875
|
+
// TASWM-153: комменты+доки эпика — то же обогащение, что у рабочего дрона.
|
|
5876
|
+
...taskContext ? ["", taskContext] : [],
|
|
4096
5877
|
"",
|
|
4097
5878
|
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u0422\u0412\u041E\u0419 \u0423\u0421\u0422\u0410\u0412 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
4098
5879
|
charter
|
|
@@ -4124,6 +5905,51 @@ function buildResolverPrompt(reviewRun, resolver, conflicts, branch, mainRef) {
|
|
|
4124
5905
|
`\u0412 \u043A\u0430\u0436\u0434\u044B\u0439 \u043A\u043E\u043C\u043C\u0438\u0442 \u0434\u043E\u0431\u0430\u0432\u043B\u044F\u0439 \u0442\u0440\u0435\u0439\u043B\u0435\u0440-\u0441\u0442\u0440\u043E\u043A\u0443 \xABDrone: ${name} \xB7 resolver run #${resolver.id}\xBB. \u041A\u043E\u043C\u043C\u0438\u0442\u044C \u0438 \u043F\u0443\u0448\u044C \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \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 \u0434\u0440\u043E\u043D\u043E\u043C. \u0414\u043E\u043B\u0433\u043E\u0435 (\u0442\u0435\u0441\u0442\u044B) \u0434\u0435\u0440\u0436\u0438 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u044E\u0449\u0438\u043C \u0432\u044B\u0437\u043E\u0432\u043E\u043C, \u043D\u0435 \u0432 \u0444\u043E\u043D\u0435.`
|
|
4125
5906
|
].join("\n");
|
|
4126
5907
|
}
|
|
5908
|
+
function buildReviewChecklist(reviewRun, files) {
|
|
5909
|
+
const header = files.length ? `\u0424\u0430\u0439\u043B\u044B \u0434\u0438\u0444\u0444\u0430 (${files.length}):
|
|
5910
|
+
${files.map((f) => `- ${f}`).join("\n")}` : "\u0424\u0430\u0439\u043B\u044B \u0434\u0438\u0444\u0444\u0430: gh \u043D\u0435 \u043E\u0442\u0434\u0430\u043B \u0441\u043F\u0438\u0441\u043E\u043A \u2014 \u0441\u043C. PR.";
|
|
5911
|
+
return [
|
|
5912
|
+
`\u0420\u0435\u0432\u044C\u044E PR ${reviewRun.pr_url ?? "(\u0441\u043C. \u043B\u043E\u0433 \u043F\u0440\u043E\u0433\u043E\u043D\u0430)"} \u2014 ${reviewRun.task_slug}${reviewRun.task_title ? `: ${reviewRun.task_title}` : ""}.`,
|
|
5913
|
+
"",
|
|
5914
|
+
header,
|
|
5915
|
+
"",
|
|
5916
|
+
"\u0427\u0435\u043A-\u043B\u0438\u0441\u0442 (\u043F\u0440\u043E\u0432\u0435\u0440\u044C \u0421\u0410\u041C \u0432 \u0441\u0432\u043E\u0451\u043C worktree \u2014 \u043D\u0435 \u0432\u0435\u0440\u044C \u0446\u0438\u0444\u0440\u0430\u043C \u0430\u0432\u0442\u043E\u0440\u0430 PR/CI):",
|
|
5917
|
+
"- [ ] \u041F\u043E\u043B\u043D\u044B\u0439 \u0442\u0435\u0441\u0442-\u0441\u044C\u044E\u0442 + typecheck \u0437\u0435\u043B\u0451\u043D\u044B\u0435 \u2014 \u043F\u0440\u043E\u0433\u043E\u043D\u0438 \u0441\u0430\u043C.",
|
|
5918
|
+
"- [ ] \u041C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 (\u0435\u0441\u043B\u0438 \u0435\u0441\u0442\u044C) \u2014 \u0441\u043E\u0432\u043C\u0435\u0441\u0442\u0438\u043C\u043E\u0441\u0442\u044C \u0441\u043E \u0441\u0442\u0430\u0440\u044B\u043C\u0438 \u0434\u0430\u043D\u043D\u044B\u043C\u0438, \u043E\u0442\u043A\u0430\u0442.",
|
|
5919
|
+
"- [ ] \u0413\u043E\u043D\u043A\u0438/\u043A\u043E\u043D\u043A\u0443\u0440\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C \u2014 race conditions, \u043F\u043E\u0440\u044F\u0434\u043E\u043A \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0439, \u0438\u0434\u0435\u043C\u043F\u043E\u0442\u0435\u043D\u0442\u043D\u043E\u0441\u0442\u044C.",
|
|
5920
|
+
"- [ ] UI-\u0434\u0438\u0441\u0446\u0438\u043F\u043B\u0438\u043D\u0430 (\u0434\u043B\u044F web-PR) \u2014 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435 \u0434\u0438\u0437\u0430\u0439\u043D-\u0441\u0438\u0441\u0442\u0435\u043C\u0435 (uikit), \u0441\u043A\u0440\u0438\u043D\u044B mobile+desktop \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u044B \u043A \u0437\u0430\u0434\u0430\u0447\u0435/PR.",
|
|
5921
|
+
"- [ ] \u0415\u0441\u043B\u0438 \u043F\u0440\u0430\u0432\u0438\u0442\u0441\u044F router.tsx \u2014 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0430 per-page meta \u0434\u043B\u044F \u0430\u0433\u0435\u043D\u0442\u0441\u043A\u043E\u0433\u043E \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0430 (\u0438\u043D\u0430\u0447\u0435 NL-\u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F \u043C\u043E\u043B\u0447\u0430 \u043B\u043E\u043C\u0430\u0435\u0442\u0441\u044F)."
|
|
5922
|
+
].join("\n");
|
|
5923
|
+
}
|
|
5924
|
+
function buildReviewerPrompt(reviewRun, reviewer, checklist, branch) {
|
|
5925
|
+
const name = reviewer.name || `#${reviewer.id}`;
|
|
5926
|
+
const prUrl = reviewRun.pr_url ?? "(\u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 PR \u2014 \u0441\u043C. \u043B\u043E\u0433 \u043F\u0440\u043E\u0433\u043E\u043D\u0430)";
|
|
5927
|
+
return [
|
|
5928
|
+
`\u0422\u044B \u2014 \u0434\u0440\u043E\u043D-\u0420\u0415\u0412\u042C\u042E\u0415\u0420 ${name} \u0440\u043E\u044F, headless-\u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C (\u041D\u0415 \u0438\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0439 \u0447\u0430\u0442). \u0410\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E (TASWM-148): \u0440\u043E\u0439 \u0441\u0430\u043C \u043F\u043E\u0434\u043D\u044F\u043B \u0442\u0435\u0431\u044F \u043D\u0430 PR \u0438\u0441\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044F \u2014 \u0442\u044B \u041D\u0415 \u0432\u044B\u0431\u0440\u0430\u043D \u0438\u043C \u0438 \u043E\u0442 \u043D\u0435\u0433\u043E \u043D\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u0448\u044C.`,
|
|
5929
|
+
"",
|
|
5930
|
+
HEADLESS_BLOCK,
|
|
5931
|
+
"",
|
|
5932
|
+
`\u0417\u0410\u0414\u0410\u0427\u0410: \u043F\u0440\u043E\u0440\u0435\u0432\u044C\u044E\u0438\u0440\u043E\u0432\u0430\u0442\u044C PR ${prUrl} \u043F\u0440\u043E\u0433\u043E\u043D\u0430 ${reviewRun.task_slug} (\u0434\u0440\u043E\u043D ${reviewRun.name}). \u0422\u044B \u0432 git-worktree \u0441 \u0447\u0435\u043A\u0430\u0443\u0442\u043E\u043C \u0432\u0435\u0442\u043A\u0438 \`${branch}\` (detached, \u043A\u043E\u043D\u0447\u0438\u043A origin) \u2014 \u043A\u043E\u0434 \u0443\u0436\u0435 \u043D\u0430 \u0434\u0438\u0441\u043A\u0435.`,
|
|
5933
|
+
"",
|
|
5934
|
+
"\u26A0\uFE0F \u0422\u042B \u041D\u0415 \u041F\u0420\u0410\u0412\u0418\u0428\u042C \u0424\u0410\u0419\u041B\u042B \u0418 \u041D\u0415 \u041F\u0423\u0428\u0418\u0428\u042C \u041A\u041E\u041C\u041C\u0418\u0422\u042B. \u0422\u0432\u043E\u044F \u0440\u0430\u0431\u043E\u0442\u0430 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0438 \u0432\u044B\u043D\u0435\u0441\u0442\u0438 \u0432\u0435\u0440\u0434\u0438\u043A\u0442.",
|
|
5935
|
+
"",
|
|
5936
|
+
checklist,
|
|
5937
|
+
"",
|
|
5938
|
+
"\u041F\u043E\u0440\u044F\u0434\u043E\u043A:",
|
|
5939
|
+
"1. \u041F\u0440\u043E\u0433\u043E\u043D\u0438 \u0447\u0435\u043A-\u043B\u0438\u0441\u0442 \u0432\u044B\u0448\u0435: \u0442\u0435\u0441\u0442\u044B/typecheck \u0421\u0410\u041C (\u043D\u0435 \u0432\u0435\u0440\u044C \u043E\u0442\u0447\u0451\u0442\u0443 \u0430\u0432\u0442\u043E\u0440\u0430/CI), \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438/\u0433\u043E\u043D\u043A\u0438/UI/per-page meta \u043F\u043E \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u043C\u043E\u0441\u0442\u0438.",
|
|
5940
|
+
`2. \u041E\u0441\u0442\u0430\u0432\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043D\u0430 \u0441\u0430\u043C PR: \`gh pr comment ${prUrl} --body "..."\` (\u041D\u0415 \`gh pr review\` \u2014 GitHub \u0435\u0433\u043E \u043E\u0442\u043A\u043B\u043E\u043D\u0438\u0442 \u043D\u0430 \u0441\u0432\u043E\u0451\u043C PR). \u041D\u0430\u0447\u043D\u0438 \u0441 **APPROVE** \u0438\u043B\u0438 **REQUEST CHANGES** \u0437\u0430\u0433\u043B\u0430\u0432\u043D\u044B\u043C\u0438, \u0434\u0430\u043B\u044C\u0448\u0435 \u2014 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u0438\u043A\u0430: \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B, \u0447\u0442\u043E \u043D\u0430\u0448\u0451\u043B; \u0434\u043B\u044F REQUEST CHANGES \u2014 \u0442\u043E\u0447\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B/\u0441\u0442\u0440\u043E\u043A\u0438 \u0438 \u0447\u0442\u043E \u0447\u0438\u043D\u0438\u0442\u044C.`,
|
|
5941
|
+
`3. \u0412\u044B\u0437\u043E\u0432\u0438 done(${prUrl}, verdict: "approve"|"changes", summary: "\u043A\u043E\u0440\u043E\u0442\u043A\u043E \u0447\u0442\u043E \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0438 \u0447\u0442\u043E \u0440\u0435\u0448\u0438\u043B") \u2014 \u0440\u043E\u0439 \u0441\u0430\u043C \u0440\u0430\u0437\u043D\u0435\u0441\u0451\u0442 summary \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u043E\u043C \u0432 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A, \u0430 verdict \u0437\u0430\u043F\u0438\u0448\u0435\u0442 \u043D\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u043C\u044B\u0439 \u043F\u0440\u043E\u0433\u043E\u043D (\u0431\u0435\u0439\u0434\u0436 \u0432 \u0432\u0435\u0431\u0435). \u0422\u044B \u0441\u0430\u043C \u0440\u0435\u0432\u044C\u044E \u041D\u0415 \u043F\u043E\u0434\u043B\u0435\u0436\u0438\u0448\u044C \u2014 \u0440\u0435\u043A\u0443\u0440\u0441\u0438\u0438 \u043D\u0435\u0442.`
|
|
5942
|
+
].join("\n");
|
|
5943
|
+
}
|
|
5944
|
+
function droneLabel(run) {
|
|
5945
|
+
return run.name || `#${run.id}`;
|
|
5946
|
+
}
|
|
5947
|
+
function narrate(deps, text) {
|
|
5948
|
+
try {
|
|
5949
|
+
deps.narrate?.(text);
|
|
5950
|
+
} catch {
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
4127
5953
|
async function manageStorm(deps, now, worktreeExists, hasTranscript) {
|
|
4128
5954
|
const db = deps.db;
|
|
4129
5955
|
const s = deps.storm;
|
|
@@ -4141,6 +5967,7 @@ async function manageStorm(deps, now, worktreeExists, hasTranscript) {
|
|
|
4141
5967
|
const consec = trailingProbeSuccesses(recs);
|
|
4142
5968
|
if (stormShouldEnd({ consecutiveProbeOk: consec, externalDown: lastExternalDown(recs) === true }, cfg)) {
|
|
4143
5969
|
db.endStorm(`${STORM_END_TEXT} (${consec} \u0443\u0441\u043F\u0435\u0448\u043D\u044B\u0435 health-probe \u043F\u043E\u0434\u0440\u044F\u0434)`);
|
|
5970
|
+
narrate(deps, "\u0428\u0442\u043E\u0440\u043C \u043A\u043E\u043D\u0447\u0438\u043B\u0441\u044F \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043F\u0440\u043E\u0433\u043E\u043D\u044B.");
|
|
4144
5971
|
await resurrectStormDead(deps, worktreeExists, hasTranscript);
|
|
4145
5972
|
return false;
|
|
4146
5973
|
}
|
|
@@ -4157,6 +5984,7 @@ async function manageStorm(deps, now, worktreeExists, hasTranscript) {
|
|
|
4157
5984
|
};
|
|
4158
5985
|
if (stormShouldStart(signals, cfg)) {
|
|
4159
5986
|
db.startStorm(stormStartReason(signals, cfg));
|
|
5987
|
+
narrate(deps, "Anthropic \u043B\u0435\u0436\u0438\u0442 \u2014 \u0440\u043E\u0439 \u0443\u0445\u043E\u0434\u0438\u0442 \u0432 \u0448\u0442\u043E\u0440\u043C, \u043F\u0430\u0443\u0437\u044E \u0441\u043F\u0430\u0432\u043D\u044B \u0438 \u0441\u043B\u0435\u0436\u0443 \u0437\u0430 \u043E\u0442\u0431\u043E\u0435\u043C.");
|
|
4160
5988
|
return true;
|
|
4161
5989
|
}
|
|
4162
5990
|
return false;
|
|
@@ -4172,33 +6000,199 @@ async function resurrectStormDead(deps, worktreeExists, hasTranscript) {
|
|
|
4172
6000
|
} catch (e) {
|
|
4173
6001
|
deps.db.updateRun(run.id, { status: "stalled" });
|
|
4174
6002
|
deps.db.addEvent(run.id, "stalled", `\u043F\u043E\u0434\u044A\u0451\u043C \u043F\u043E\u0441\u043B\u0435 \u0448\u0442\u043E\u0440\u043C\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
|
|
6003
|
+
narrate(deps, `\u043F\u043E\u0434\u044A\u0451\u043C ${droneLabel(run)} \u043F\u043E\u0441\u043B\u0435 \u0448\u0442\u043E\u0440\u043C\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F \u2014 \u043E\u0441\u0442\u0430\u0432\u043B\u044F\u044E \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0443.`);
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
async function handleCiRed(deps, run, now, worktreeExists, hasTranscript, droneCap) {
|
|
6008
|
+
if (run.pid != null && deps.isAlive(run.pid)) return;
|
|
6009
|
+
if (deps.resourcesOk && !deps.resourcesOk()) return;
|
|
6010
|
+
if (countActiveDrones(deps.db) >= droneCap) return;
|
|
6011
|
+
const stats = ciWakeStats(deps.db, run.id);
|
|
6012
|
+
if (stats.count >= CI_WAKE_LIMIT) {
|
|
6013
|
+
if (!stats.escalated) {
|
|
6014
|
+
deps.db.addEvent(run.id, "ci_wake", `${CI_WAKE_ESCALATION_MARK}: ${CI_WAKE_LIMIT} \u043F\u043E\u0431\u0443\u0434(\u043E\u043A) \u043D\u0435 \u0445\u0432\u0430\u0442\u0438\u043B\u043E \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
|
|
6015
|
+
narrate(deps, `CI \u0443 ${droneLabel(run)} \u0443\u043F\u043E\u0440\u043D\u043E \u043A\u0440\u0430\u0441\u043D\u044B\u0439, \u043F\u043E\u0431\u0443\u0434\u043A\u0438 \u043D\u0435 \u043F\u043E\u043C\u043E\u0433\u043B\u0438 \u2014 \u0441\u0434\u0430\u044E\u0441\u044C, \u0434\u0430\u043B\u044C\u0448\u0435 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440.`);
|
|
6016
|
+
}
|
|
6017
|
+
return;
|
|
6018
|
+
}
|
|
6019
|
+
if (stats.lastMs != null && now - stats.lastMs < CI_WAKE_COOLDOWN_MS) return;
|
|
6020
|
+
const lastCheck = lastCiCheckMs(deps.db, run.id);
|
|
6021
|
+
if (lastCheck != null && now - lastCheck < CI_CHECK_COOLDOWN_MS) return;
|
|
6022
|
+
const wt = run.worktree_path;
|
|
6023
|
+
if (!wt || !worktreeExists(wt) || !hasTranscript(wt)) return;
|
|
6024
|
+
const ci = await deps.ci.checks(run.pr_url).catch(() => null);
|
|
6025
|
+
if (ci?.bucket !== "fail") {
|
|
6026
|
+
deps.db.addEvent(run.id, "ci_checked", `CI ${ci?.bucket ?? "\u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D"} \u2014 \u043D\u0435 \u0431\u0443\u0436\u0443`);
|
|
6027
|
+
return;
|
|
6028
|
+
}
|
|
6029
|
+
const summary = await deps.ci.failureSummary(ci.failing, ci.failingRunIds).catch(() => null) ?? `\u274C CI \u0443\u043F\u0430\u043B: ${ci.failing.join(", ") || "(\u0441\u043C. gh pr checks)"}`;
|
|
6030
|
+
try {
|
|
6031
|
+
await deps.respawn(run, "continue", buildCiWakeNudge(summary));
|
|
6032
|
+
deps.db.addEvent(run.id, "ci_wake", `CI \u0443\u043F\u0430\u043B (${ci.failing.join(", ") || "?"}) \u2014 \u0431\u0443\u0436\u0443 \u0434\u0440\u043E\u043D\u0430 \u0441\u043E \u0441\u0436\u0430\u0442\u044B\u043C summary`);
|
|
6033
|
+
narrate(deps, `CI \u0443 ${droneLabel(run)} \u043F\u043E\u043A\u0440\u0430\u0441\u043D\u0435\u043B \u2014 \u0431\u0443\u0436\u0443 \u0434\u0440\u043E\u043D\u0430 \u0447\u0438\u043D\u0438\u0442\u044C.`);
|
|
6034
|
+
} catch (e) {
|
|
6035
|
+
deps.db.addEvent(run.id, "ci_wake", `\u043F\u043E\u0431\u0443\u0434\u043A\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C: ${String(e)}`);
|
|
6036
|
+
narrate(deps, `\u043F\u043E\u0431\u0443\u0434\u043A\u0430 ${droneLabel(run)} \u043D\u0430 \u043A\u0440\u0430\u0441\u043D\u044B\u0439 CI \u0441\u043E\u0440\u0432\u0430\u043B\u0430\u0441\u044C.`);
|
|
6037
|
+
}
|
|
6038
|
+
}
|
|
6039
|
+
async function trySpawnQueuedRun(deps, run) {
|
|
6040
|
+
const mergedUrl = doneMergedPrForSlug(deps.db, run.task_slug) ?? (deps.mergedPrForBranch ? await deps.mergedPrForBranch(run.task_slug, run.repo || "").catch(() => null) : null);
|
|
6041
|
+
if (mergedUrl) {
|
|
6042
|
+
await closeDuplicateTaskRun(deps, run, mergedUrl);
|
|
6043
|
+
await deps.finishTask(run.task_slug).catch(() => {
|
|
6044
|
+
});
|
|
6045
|
+
narrate(deps, `${droneLabel(run)} \u2014 \u0437\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u0430 \u0434\u0440\u0443\u0433\u0438\u043C \u0440\u0430\u043D\u043E\u043C, \u043D\u0435 \u0441\u043F\u0430\u0432\u043D\u044E.`);
|
|
6046
|
+
return "closed";
|
|
6047
|
+
}
|
|
6048
|
+
if (deps.db.unmetDeps(run.depends_on).length > 0) return "skipped";
|
|
6049
|
+
try {
|
|
6050
|
+
await deps.spawn(run);
|
|
6051
|
+
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)}.`);
|
|
6052
|
+
return "spawned";
|
|
6053
|
+
} catch (e) {
|
|
6054
|
+
if (e instanceof AreaBusyError) return "skipped";
|
|
6055
|
+
deps.db.updateRun(run.id, { status: "failed" });
|
|
6056
|
+
deps.db.addEvent(run.id, "failed", String(e));
|
|
6057
|
+
return "failed";
|
|
6058
|
+
}
|
|
6059
|
+
}
|
|
6060
|
+
async function batchSpawnQueue(deps, droneCap) {
|
|
6061
|
+
for (const run of deps.db.listRuns("queued")) {
|
|
6062
|
+
if (countActiveDrones(deps.db) >= droneCap) break;
|
|
6063
|
+
await trySpawnQueuedRun(deps, run);
|
|
6064
|
+
}
|
|
6065
|
+
}
|
|
6066
|
+
async function manageRelief(deps, now, droneCap, stormActive) {
|
|
6067
|
+
const state = deps.relief ?? (deps.relief = {});
|
|
6068
|
+
const cfg = state.config ? resolveReliefConfig(state.config) : DEFAULT_RELIEF_CONFIG;
|
|
6069
|
+
const needsRelief = deps.reliefNeeded ? deps.reliefNeeded() : false;
|
|
6070
|
+
const resourcesOk2 = deps.resourcesOk();
|
|
6071
|
+
state.reliefSince = needsRelief ? state.reliefSince ?? now : null;
|
|
6072
|
+
if (!resourcesOk2 || needsRelief) state.overloadedAt = now;
|
|
6073
|
+
if (needsRelief && deps.kill && pauseDue(state.reliefSince, state.lastPauseAt ?? null, now, cfg)) {
|
|
6074
|
+
const candidate = pickPauseCandidate(deps.db.listRuns("running"));
|
|
6075
|
+
if (candidate) {
|
|
6076
|
+
try {
|
|
6077
|
+
deps.kill(candidate.pid);
|
|
6078
|
+
} catch {
|
|
6079
|
+
}
|
|
6080
|
+
deps.db.updateRun(candidate.id, { status: "paused", stop_reason: RELIEF_PAUSE_REASON });
|
|
6081
|
+
deps.db.addEvent(candidate.id, "paused", RELIEF_PAUSE_REASON);
|
|
6082
|
+
state.lastPauseAt = now;
|
|
6083
|
+
narrate(deps, `\u0425\u043E\u0441\u0442 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D \u2014 \u0441\u0442\u0430\u0432\u043B\u044E ${droneLabel(candidate)} \u043D\u0430 \u043F\u0430\u0443\u0437\u0443 (\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443, TASWM-151).`);
|
|
6084
|
+
}
|
|
6085
|
+
return;
|
|
6086
|
+
}
|
|
6087
|
+
if (stormActive) return;
|
|
6088
|
+
if (!resourcesOk2) {
|
|
6089
|
+
if (deps.db.listRuns("queued").some((r) => deps.db.unmetDeps(r.depends_on).length === 0))
|
|
6090
|
+
console.error("[overmind] \u0441\u043F\u0430\u0432\u043D \u043E\u0442\u043B\u043E\u0436\u0435\u043D: \u043C\u0430\u0448\u0438\u043D\u0430 \u043F\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043D\u0430 (\u0440\u0435\u0441\u0443\u0440\u0441-\u0433\u0435\u0439\u0442 TASWM-151)");
|
|
6091
|
+
return;
|
|
6092
|
+
}
|
|
6093
|
+
if (countActiveDrones(deps.db) >= droneCap) return;
|
|
6094
|
+
const pausedForRelief = deps.db.listRuns("paused").filter((r) => r.stop_reason === RELIEF_PAUSE_REASON);
|
|
6095
|
+
const recovering = pausedForRelief.length > 0 || state.overloadedAt != null && now - state.overloadedAt < cfg.rampWindowMs;
|
|
6096
|
+
if (!recovering) {
|
|
6097
|
+
await batchSpawnQueue(deps, droneCap);
|
|
6098
|
+
return;
|
|
6099
|
+
}
|
|
6100
|
+
if (!rampDue(state.lastRampAt ?? null, now, cfg)) return;
|
|
6101
|
+
const resumeCandidate = pickResumeCandidate(pausedForRelief);
|
|
6102
|
+
if (resumeCandidate) {
|
|
6103
|
+
if (resumeCandidate.pid != null && deps.isAlive(resumeCandidate.pid)) return;
|
|
6104
|
+
if (!deps.respawn) return;
|
|
6105
|
+
const wt = resumeCandidate.worktree_path;
|
|
6106
|
+
const worktreeExists = deps.worktreeExists ?? existsSync4;
|
|
6107
|
+
const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
|
|
6108
|
+
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
6109
|
+
const mode = canContinue ? "continue" : "fresh";
|
|
6110
|
+
try {
|
|
6111
|
+
await deps.respawn(resumeCandidate, mode);
|
|
6112
|
+
deps.db.updateRun(resumeCandidate.id, { stop_reason: null });
|
|
6113
|
+
deps.db.addEvent(
|
|
6114
|
+
resumeCandidate.id,
|
|
6115
|
+
"resumed",
|
|
6116
|
+
mode === "continue" ? "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043F\u0443\u0441\u0442\u0438\u043B\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443 (--continue: \u0440\u0430\u0431\u043E\u0442\u0430 \u0432 origin + \u0441\u0435\u0441\u0441\u0438\u044F \u0446\u0435\u043B\u044B)" : "\u0440\u0435\u0441\u0443\u0440\u0441-\u0440\u0430\u0437\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043F\u0443\u0441\u0442\u0438\u043B\u0430 \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u043F\u043E \u043E\u0434\u043D\u043E\u043C\u0443, \u0441\u0442\u0430\u0440\u0442 \u0437\u0430\u043D\u043E\u0432\u043E (\u0441\u0435\u0441\u0441\u0438\u044F/worktree \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B)"
|
|
6117
|
+
);
|
|
6118
|
+
narrate(deps, `\u041F\u043E\u043B\u0435\u0433\u0447\u0430\u043B\u043E \u2014 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E ${droneLabel(resumeCandidate)} \u0438\u0437 \u043F\u0430\u0443\u0437\u044B.`);
|
|
6119
|
+
state.lastRampAt = now;
|
|
6120
|
+
} catch (e) {
|
|
6121
|
+
deps.db.updateRun(resumeCandidate.id, { status: "stalled" });
|
|
6122
|
+
deps.db.addEvent(resumeCandidate.id, "stalled", `\u0432\u043E\u0437\u0432\u0440\u0430\u0442 \u0438\u0437 \u0440\u0435\u0441\u0443\u0440\u0441-\u043F\u0430\u0443\u0437\u044B \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
|
|
4175
6123
|
}
|
|
6124
|
+
return;
|
|
6125
|
+
}
|
|
6126
|
+
for (const run of deps.db.listRuns("queued")) {
|
|
6127
|
+
const outcome = await trySpawnQueuedRun(deps, run);
|
|
6128
|
+
if (outcome === "skipped" || outcome === "closed") continue;
|
|
6129
|
+
state.lastRampAt = now;
|
|
6130
|
+
return;
|
|
4176
6131
|
}
|
|
4177
6132
|
}
|
|
4178
6133
|
async function tickOnce(deps) {
|
|
4179
6134
|
const now = deps.now ? deps.now() : Date.now();
|
|
4180
|
-
const worktreeExists = deps.worktreeExists ??
|
|
6135
|
+
const worktreeExists = deps.worktreeExists ?? existsSync4;
|
|
4181
6136
|
const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
|
|
6137
|
+
if (deps.rehydrate) {
|
|
6138
|
+
const rh = deps.rehydrate;
|
|
6139
|
+
const sinceLast = rh.lastAt == null ? Infinity : now - rh.lastAt;
|
|
6140
|
+
if (sinceLast >= rh.intervalMs && rh.ready()) {
|
|
6141
|
+
rh.lastAt = now;
|
|
6142
|
+
try {
|
|
6143
|
+
await rh.run();
|
|
6144
|
+
} catch (e) {
|
|
6145
|
+
console.error("[overmind] re-hydrate \u0437\u0435\u0440\u043A\u0430\u043B\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
6146
|
+
}
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
if (deps.heartbeat) {
|
|
6150
|
+
const hb = deps.heartbeat;
|
|
6151
|
+
const sinceLast = hb.lastAt == null ? Infinity : now - hb.lastAt;
|
|
6152
|
+
if (sinceLast >= hb.intervalMs) {
|
|
6153
|
+
hb.lastAt = now;
|
|
6154
|
+
await hb.push();
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
6157
|
+
if (deps.rehydrate && deps.kill) {
|
|
6158
|
+
const kill = deps.kill;
|
|
6159
|
+
for (const run of deps.db.listRuns()) {
|
|
6160
|
+
if (!isTerminalStatus(run.status) || run.pid == null || !deps.isAlive(run.pid)) continue;
|
|
6161
|
+
console.log(`[overmind] reap: \u043F\u0440\u043E\u0433\u043E\u043D ${run.name} (#${run.id}) \u043F\u043E\u043C\u0435\u0447\u0435\u043D ${run.status} \u0438\u0437\u0432\u043D\u0435 \u2014 \u0433\u0430\u0448\u0443 pid ${run.pid}`);
|
|
6162
|
+
try {
|
|
6163
|
+
kill(run.pid);
|
|
6164
|
+
} catch {
|
|
6165
|
+
}
|
|
6166
|
+
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
6167
|
+
});
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
6170
|
+
if (deps.sweep) {
|
|
6171
|
+
const sw = deps.sweep;
|
|
6172
|
+
const sinceLast = sw.lastAt == null ? Infinity : now - sw.lastAt;
|
|
6173
|
+
if (sinceLast >= sw.intervalMs) {
|
|
6174
|
+
sw.lastAt = now;
|
|
6175
|
+
try {
|
|
6176
|
+
await sw.run();
|
|
6177
|
+
} catch (e) {
|
|
6178
|
+
console.error("[overmind] \u0437\u0430\u0449\u0438\u0442\u043D\u044B\u0439 sweep \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
6179
|
+
}
|
|
6180
|
+
}
|
|
6181
|
+
}
|
|
4182
6182
|
let stormActive = false;
|
|
4183
6183
|
if (deps.storm) {
|
|
4184
6184
|
for (const run of deps.db.listRuns("running")) {
|
|
4185
|
-
if (run.resolves_run_id == null && run.pid != null && !deps.isAlive(run.pid)) {
|
|
6185
|
+
if (run.resolves_run_id == null && run.reviews_run_id == null && run.pid != null && !deps.isAlive(run.pid)) {
|
|
4186
6186
|
deps.db.recordDeath(run.id, `\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440 (pid ${run.pid} \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442)`);
|
|
4187
6187
|
}
|
|
4188
6188
|
}
|
|
4189
6189
|
stormActive = await manageStorm(deps, now, worktreeExists, hasTranscript);
|
|
4190
6190
|
}
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
} catch (e) {
|
|
4197
|
-
if (e instanceof AreaBusyError) continue;
|
|
4198
|
-
deps.db.updateRun(run.id, { status: "failed" });
|
|
4199
|
-
deps.db.addEvent(run.id, "failed", String(e));
|
|
4200
|
-
}
|
|
4201
|
-
}
|
|
6191
|
+
const droneCap = deps.maxDrones ? await deps.maxDrones() : Infinity;
|
|
6192
|
+
if (deps.resourcesOk) {
|
|
6193
|
+
await manageRelief(deps, now, droneCap, stormActive);
|
|
6194
|
+
} else if (!stormActive) {
|
|
6195
|
+
await batchSpawnQueue(deps, droneCap);
|
|
4202
6196
|
}
|
|
4203
6197
|
for (const run of deps.db.listRuns("review")) {
|
|
4204
6198
|
try {
|
|
@@ -4210,20 +6204,31 @@ async function tickOnce(deps) {
|
|
|
4210
6204
|
deps.db.addEvent(run.resolves_run_id, "conflict", `\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u0441 main \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D \u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440\u043E\u043C ${run.name} \u2014 PR \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D`);
|
|
4211
6205
|
continue;
|
|
4212
6206
|
}
|
|
4213
|
-
if (
|
|
4214
|
-
const st = await deps.prStatus(run.pr_url);
|
|
4215
|
-
if (st.merged) {
|
|
4216
|
-
await deps.finishTask(run.task_slug);
|
|
6207
|
+
if (run.reviews_run_id != null) {
|
|
4217
6208
|
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
4218
6209
|
});
|
|
4219
|
-
deps.db.addEvent(run.id, "merged", run.pr_url);
|
|
4220
6210
|
deps.db.updateRun(run.id, { status: "done" });
|
|
4221
|
-
deps.db.addEvent(run.id, "done");
|
|
6211
|
+
deps.db.addEvent(run.id, "done", "\u0440\u0435\u0432\u044C\u044E \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E, \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0432\u044B\u043D\u0435\u0441\u0435\u043D");
|
|
6212
|
+
narrate(deps, `${droneLabel(run)} \u0432\u044B\u043D\u0435\u0441 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043F\u043E \u0440\u0435\u0432\u044C\u044E \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442.`);
|
|
6213
|
+
continue;
|
|
6214
|
+
}
|
|
6215
|
+
if (!run.pr_url) continue;
|
|
6216
|
+
const st = await deps.prStatus(run.pr_url);
|
|
6217
|
+
if (st.merged) {
|
|
6218
|
+
await finalizeMergedReview(deps, run);
|
|
6219
|
+
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.`);
|
|
4222
6220
|
continue;
|
|
4223
6221
|
}
|
|
4224
6222
|
if (st.conflicting && deps.resolveConflict && !stormActive) {
|
|
6223
|
+
if (deps.resourcesOk && !deps.resourcesOk()) continue;
|
|
6224
|
+
if (countActiveDrones(deps.db) >= droneCap) continue;
|
|
4225
6225
|
if (deps.db.activeResolverFor(run.id)) continue;
|
|
4226
|
-
if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT)
|
|
6226
|
+
if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) {
|
|
6227
|
+
if (!deps.db.events(run.id).some((e) => e.kind === "conflict" && (e.text ?? "").includes(RESOLVE_ESCALATION_MARK))) {
|
|
6228
|
+
deps.db.addEvent(run.id, "conflict", `${RESOLVE_ESCALATION_MARK}: \u0430\u0432\u0442\u043E-\u0440\u0435\u0437\u043E\u043B\u0432\u0435\u0440 \u043D\u0435 \u0441\u043F\u0440\u0430\u0432\u0438\u043B\u0441\u044F \u0437\u0430 ${RESOLVE_LIMIT} \u043F\u043E\u043F\u044B\u0442(\u043E\u043A) \u2014 \u043D\u0443\u0436\u0435\u043D \u0447\u0435\u043B\u043E\u0432\u0435\u043A`);
|
|
6229
|
+
}
|
|
6230
|
+
continue;
|
|
6231
|
+
}
|
|
4227
6232
|
const last = lastConflictAttemptMs(deps.db, run.id);
|
|
4228
6233
|
if (last != null && now - last < CONFLICT_RESOLVE_COOLDOWN_MS) continue;
|
|
4229
6234
|
deps.db.addEvent(run.id, "conflict", "PR \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u0443\u0435\u0442 \u0441 main \u2014 \u0440\u043E\u0439 \u0432\u043B\u0438\u0432\u0430\u0435\u0442 main (merge-main-in)");
|
|
@@ -4232,15 +6237,75 @@ async function tickOnce(deps) {
|
|
|
4232
6237
|
} catch (e) {
|
|
4233
6238
|
deps.db.addEvent(run.id, "conflict", `merge-main-in \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
|
|
4234
6239
|
}
|
|
6240
|
+
} else if (!st.conflicting && deps.ci && deps.respawn && !stormActive) {
|
|
6241
|
+
await handleCiRed(deps, run, now, worktreeExists, hasTranscript, droneCap);
|
|
6242
|
+
}
|
|
6243
|
+
if (!st.conflicting && !st.isDraft && deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt) && deps.db.reviewerCountFor(run.id) === 0) {
|
|
6244
|
+
const attempts = deps.db.reviewerAttemptCountFor(run.id);
|
|
6245
|
+
let verdictFromComment = null;
|
|
6246
|
+
if (attempts > 0 && deps.verdictComment) {
|
|
6247
|
+
const lastAttempt = deps.db.lastReviewerAttemptFor(run.id);
|
|
6248
|
+
verdictFromComment = await deps.verdictComment(run.pr_url, lastAttempt?.created_at ?? run.created_at).catch(() => null);
|
|
6249
|
+
}
|
|
6250
|
+
if (verdictFromComment) {
|
|
6251
|
+
const verdictText = verdictLabel(verdictFromComment.verdict);
|
|
6252
|
+
deps.db.updateRun(run.id, { review_verdict: verdictFromComment.verdict });
|
|
6253
|
+
deps.db.addEvent(run.id, "step", `\u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u0440\u0435\u0432\u044C\u044E \u043F\u043E\u0434\u0445\u0432\u0430\u0447\u0435\u043D \u0438\u0437 PR-\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430 (done() \u0431\u044B\u043B \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0443): ${verdictText}`);
|
|
6254
|
+
narrate(deps, `\u0420\u0435\u0432\u044C\u044E ${droneLabel(run)} \u2014 \u0432\u0435\u0440\u0434\u0438\u043A\u0442 \u043D\u0430\u0448\u0451\u043B\u0441\u044F \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0445 PR (${verdictText}), \u0440\u0435\u0441\u043F\u0430\u0432\u043D \u043D\u0435 \u043D\u0443\u0436\u0435\u043D.`);
|
|
6255
|
+
} else if (attempts >= REVIEW_SPAWN_LIMIT) {
|
|
6256
|
+
if (!deps.db.events(run.id).some((e) => e.kind === "step" && (e.text ?? "").includes(REVIEW_ESCALATION_MARK))) {
|
|
6257
|
+
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`);
|
|
6258
|
+
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.`);
|
|
6259
|
+
}
|
|
6260
|
+
} else {
|
|
6261
|
+
const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
|
|
6262
|
+
const leavesSlotForDrones = countActiveDrones(deps.db) < droneCap - 1;
|
|
6263
|
+
if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk && leavesSlotForDrones) {
|
|
6264
|
+
const freshSt = await deps.prStatus(run.pr_url);
|
|
6265
|
+
if (freshSt.merged) {
|
|
6266
|
+
await finalizeMergedReview(deps, run);
|
|
6267
|
+
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.`);
|
|
6268
|
+
} else if (freshSt.state === "CLOSED") {
|
|
6269
|
+
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
6270
|
+
});
|
|
6271
|
+
deps.db.updateRun(run.id, { status: "done" });
|
|
6272
|
+
deps.db.addEvent(run.id, "done", "PR \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E");
|
|
6273
|
+
narrate(deps, `PR ${droneLabel(run)} \u0437\u0430\u043A\u0440\u044B\u0442 \u0431\u0435\u0437 \u043C\u0435\u0440\u0436\u0430, \u043F\u043E\u043A\u0430 \u0436\u0434\u0430\u043B \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 \u2014 \u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043D\u0443\u0436\u043D\u043E.`);
|
|
6274
|
+
} else if (freshSt.conflicting || freshSt.isDraft) {
|
|
6275
|
+
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");
|
|
6276
|
+
} else {
|
|
6277
|
+
try {
|
|
6278
|
+
await deps.spawnReviewer(run, st.files ?? []);
|
|
6279
|
+
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.`);
|
|
6280
|
+
} catch (e) {
|
|
6281
|
+
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)}`);
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
6284
|
+
}
|
|
6285
|
+
}
|
|
4235
6286
|
}
|
|
4236
6287
|
} catch (e) {
|
|
4237
6288
|
console.error(`[overmind] \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B review-\u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} (${run.task_slug}) \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
|
|
4238
6289
|
}
|
|
4239
6290
|
}
|
|
6291
|
+
await closeMergedTaskRuns(deps);
|
|
6292
|
+
let liveChildPpids;
|
|
6293
|
+
try {
|
|
6294
|
+
liveChildPpids = deps.livePpids?.() ?? null;
|
|
6295
|
+
} catch (e) {
|
|
6296
|
+
liveChildPpids = null;
|
|
6297
|
+
logTransientSpawnError("ps (livePpids)", e);
|
|
6298
|
+
}
|
|
4240
6299
|
for (const run of [...deps.db.listRuns("running"), ...deps.db.listRuns("blocked")]) {
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
6300
|
+
const alive = run.pid != null && deps.isAlive(run.pid);
|
|
6301
|
+
if (!alive && deps.accountCost) {
|
|
6302
|
+
try {
|
|
6303
|
+
deps.accountCost(run);
|
|
6304
|
+
} catch (e) {
|
|
6305
|
+
console.error(`[overmind] cost-\u0443\u0447\u0451\u0442 \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id} \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:`, e);
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
if (stormActive && run.resolves_run_id == null && run.reviews_run_id == null) {
|
|
4244
6309
|
if (!alive) {
|
|
4245
6310
|
deps.db.updateRun(run.id, { status: "storm_dead" });
|
|
4246
6311
|
deps.db.addEvent(run.id, "storm_dead", STORM_DEAD_TEXT);
|
|
@@ -4272,7 +6337,7 @@ async function tickOnce(deps) {
|
|
|
4272
6337
|
}
|
|
4273
6338
|
continue;
|
|
4274
6339
|
}
|
|
4275
|
-
if (!alive && run.status === "running" && run.resolves_run_id == null && !run.pr_url && deps.openPrsForBranch) {
|
|
6340
|
+
if (!alive && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && !run.pr_url && deps.openPrsForBranch) {
|
|
4276
6341
|
const branch = run.branch || run.task_slug;
|
|
4277
6342
|
const prs = await deps.openPrsForBranch(branch, run.repo).catch(() => []);
|
|
4278
6343
|
if (prs.length > 0) {
|
|
@@ -4283,7 +6348,7 @@ async function tickOnce(deps) {
|
|
|
4283
6348
|
continue;
|
|
4284
6349
|
}
|
|
4285
6350
|
}
|
|
4286
|
-
if (!alive && run.status === "running" && run.resolves_run_id == null && deps.respawn && run.respawns < RESPAWN_LIMIT) {
|
|
6351
|
+
if (!alive && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && deps.respawn && run.respawns < RESPAWN_LIMIT) {
|
|
4287
6352
|
const wt = run.worktree_path;
|
|
4288
6353
|
const wtAlive = !!wt && worktreeExists(wt);
|
|
4289
6354
|
const canContinue = wtAlive && hasTranscript(wt);
|
|
@@ -4296,11 +6361,16 @@ async function tickOnce(deps) {
|
|
|
4296
6361
|
"respawned",
|
|
4297
6362
|
mode === "continue" ? `\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 #${n}: --continue (worktree \u0438 \u0442\u0440\u0430\u043D\u0441\u043A\u0440\u0438\u043F\u0442 \u0436\u0438\u0432\u044B, \u0441\u043F\u0430\u0441\u0430\u0435\u043C \u0440\u0430\u0431\u043E\u0442\u0443)` : `\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 #${n}: \u0447\u0438\u0441\u0442\u044B\u0439 \u0441\u0442\u0430\u0440\u0442 (\u043C\u043E\u043B\u043E\u0434\u0430\u044F \u0441\u043C\u0435\u0440\u0442\u044C <${Math.round(RESPAWN_YOUNG_MS / 1e3)}\u0441 \u2014 \u0432\u0435\u0440\u043E\u044F\u0442\u043D\u043E \u0432\u043E\u043F\u0440\u043E\u0441-\u043D\u0430-\u0441\u0442\u0430\u0440\u0442\u0435, \u0441\u0435\u0441\u0441\u0438\u044E \u043D\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u043C)`
|
|
4298
6363
|
);
|
|
6364
|
+
narrate(
|
|
6365
|
+
deps,
|
|
6366
|
+
mode === "continue" ? `${droneLabel(run)} \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u2014 \u043F\u043E\u0434\u043D\u0438\u043C\u0430\u044E \u0441 \u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442\u0430, \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0435 \u0442\u0435\u0440\u044F\u0435\u043C.` : `${droneLabel(run)} \u0443\u043C\u0435\u0440 \u043D\u0430 \u0441\u0442\u0430\u0440\u0442\u0435 \u2014 \u043F\u0435\u0440\u0435\u0441\u043F\u0430\u0432\u043D\u0438\u0432\u0430\u044E \u0441 \u043D\u0443\u043B\u044F.`
|
|
6367
|
+
);
|
|
4299
6368
|
try {
|
|
4300
6369
|
await deps.respawn(run, mode);
|
|
4301
6370
|
} catch (e) {
|
|
4302
6371
|
deps.db.updateRun(run.id, { status: "stalled" });
|
|
4303
6372
|
deps.db.addEvent(run.id, "stalled", `\u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C: ${String(e)}`);
|
|
6373
|
+
narrate(deps, `\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0435 ${droneLabel(run)} \u0441\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C \u2014 \u0441\u0434\u0430\u044E\u0441\u044C, \u0434\u0430\u043B\u044C\u0448\u0435 \u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0437\u0430 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u043E\u043C.`);
|
|
4304
6374
|
}
|
|
4305
6375
|
continue;
|
|
4306
6376
|
}
|
|
@@ -4308,8 +6378,10 @@ async function tickOnce(deps) {
|
|
|
4308
6378
|
let reason = null;
|
|
4309
6379
|
if (!alive) {
|
|
4310
6380
|
reason = run.respawns >= RESPAWN_LIMIT ? `\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440: \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u043B\u0438\u043C\u0438\u0442 \u0430\u0432\u0442\u043E-\u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u0439 (${RESPAWN_LIMIT})` : "\u0434\u0440\u043E\u043D \u0443\u043C\u0435\u0440: pid \u043D\u0435 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442";
|
|
4311
|
-
|
|
6381
|
+
narrate(deps, `${droneLabel(run)} \u043D\u0435 \u043F\u0440\u0438\u0445\u043E\u0434\u0438\u0442 \u0432 \u0441\u0435\u0431\u044F \u2014 \u0441\u0434\u0430\u044E\u0441\u044C, \u0434\u0430\u043B\u044C\u0448\u0435 \u043D\u0443\u0436\u0435\u043D \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440.`);
|
|
6382
|
+
} else if (run.status === "running" && now - await livenessMs(deps, run, now, liveChildPpids) > STALL_MS) {
|
|
4312
6383
|
reason = `\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441: \u043C\u043E\u043B\u0447\u0438\u0442 > ${Math.round(STALL_MS / 6e4)} \u043C\u0438\u043D`;
|
|
6384
|
+
narrate(deps, `${droneLabel(run)} \u0437\u0430\u0442\u0438\u0445 \u0434\u043E\u043B\u044C\u0448\u0435 ${Math.round(STALL_MS / 6e4)} \u043C\u0438\u043D \u2014 \u043F\u043E\u043C\u0435\u0447\u0430\u044E \u0437\u0430\u0432\u0438\u0441\u0448\u0438\u043C, \u0433\u043B\u044F\u043D\u0443, \u0447\u0442\u043E \u0442\u0430\u043C.`);
|
|
4313
6385
|
}
|
|
4314
6386
|
if (!reason) continue;
|
|
4315
6387
|
deps.db.updateRun(run.id, { status: "stalled" });
|
|
@@ -4324,6 +6396,8 @@ async function tickOnce(deps) {
|
|
|
4324
6396
|
if (deps.respawn && !stormActive) {
|
|
4325
6397
|
for (const run of deps.db.listRuns("paused")) {
|
|
4326
6398
|
if (run.kind === "hero") continue;
|
|
6399
|
+
if (run.reviews_run_id != null) continue;
|
|
6400
|
+
if (run.stop_reason === RELIEF_PAUSE_REASON) continue;
|
|
4327
6401
|
if (run.pid != null && deps.isAlive(run.pid)) continue;
|
|
4328
6402
|
const wt = run.worktree_path;
|
|
4329
6403
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
@@ -4341,10 +6415,42 @@ async function tickOnce(deps) {
|
|
|
4341
6415
|
}
|
|
4342
6416
|
}
|
|
4343
6417
|
}
|
|
4344
|
-
if (deps.
|
|
6418
|
+
if (deps.anomalies) {
|
|
6419
|
+
const az = deps.anomalies;
|
|
6420
|
+
let eligible = [];
|
|
6421
|
+
try {
|
|
6422
|
+
const candidates = buildAnomalyCandidates(deps.db, now, az.thresholds, {
|
|
6423
|
+
autoReviewEnabled: az.autoReviewEnabled,
|
|
6424
|
+
...az.outboxPendingCount ? { outboxPendingCount: az.outboxPendingCount() } : {}
|
|
6425
|
+
});
|
|
6426
|
+
eligible = az.tracker.eligible(now, candidates);
|
|
6427
|
+
} catch (e) {
|
|
6428
|
+
console.error("[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0434\u0435\u0442\u0435\u043A\u0442 \u0430\u043D\u043E\u043C\u0430\u043B\u0438\u0439 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
6429
|
+
}
|
|
6430
|
+
for (const candidate of eligible) {
|
|
6431
|
+
const signal = { key: candidate.key, ...candidate.build() };
|
|
6432
|
+
try {
|
|
6433
|
+
await az.notify(signal);
|
|
6434
|
+
} catch (e) {
|
|
6435
|
+
console.error(`[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u043D\u043E\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \xAB${signal.key}\xBB \u043D\u0435 \u0434\u043E\u043B\u0435\u0442\u0435\u043B\u0430, \u043F\u043E\u043F\u0440\u043E\u0431\u0443\u044E \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0442\u0438\u043A\u043E\u043C:`, e);
|
|
6436
|
+
continue;
|
|
6437
|
+
}
|
|
6438
|
+
az.tracker.commit(candidate.key, now);
|
|
6439
|
+
if (signal.runId != null) {
|
|
6440
|
+
try {
|
|
6441
|
+
deps.db.addEvent(signal.runId, "anomaly", signal.body);
|
|
6442
|
+
} catch (e) {
|
|
6443
|
+
console.error(`[overmind] \u043D\u0430\u0434\u0437\u043E\u0440: \u0432\u0435\u0445\u0430 \xAB${signal.key}\xBB \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043B\u0430\u0441\u044C:`, e);
|
|
6444
|
+
}
|
|
6445
|
+
}
|
|
6446
|
+
narrate(deps, `\u26A0\uFE0F ${signal.title}`);
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
if (deps.intake && !stormActive && deps.intake.maxInFlight > 0 && (!deps.resourcesOk || deps.resourcesOk())) {
|
|
4345
6450
|
const it = deps.intake;
|
|
6451
|
+
const claimCap = deps.maxDrones ? droneCap : it.maxInFlight;
|
|
4346
6452
|
const sinceLastClaim = it.lastClaimAt == null ? Infinity : now - it.lastClaimAt;
|
|
4347
|
-
if (sinceLastClaim >= it.intervalMs && countInFlight(deps.db) <
|
|
6453
|
+
if (sinceLastClaim >= it.intervalMs && countInFlight(deps.db) < claimCap) {
|
|
4348
6454
|
it.lastClaimAt = now;
|
|
4349
6455
|
try {
|
|
4350
6456
|
await it.claim();
|
|
@@ -4427,45 +6533,140 @@ async function resolveConflict(deps, reviewRun) {
|
|
|
4427
6533
|
await deps.git.removeWorktree(repo, wt).catch(() => {
|
|
4428
6534
|
});
|
|
4429
6535
|
deps.db.updateRun(resolver.id, { status: "failed" });
|
|
4430
|
-
|
|
6536
|
+
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";
|
|
6537
|
+
deps.db.addEvent(resolver.id, "failed", `${marker}: ${String(e)}`);
|
|
6538
|
+
throw e;
|
|
6539
|
+
}
|
|
6540
|
+
}
|
|
6541
|
+
async function spawnReviewer(deps, reviewRun, files) {
|
|
6542
|
+
const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
|
|
6543
|
+
const branch = reviewRun.branch || reviewRun.task_slug;
|
|
6544
|
+
const checklist = buildReviewChecklist(reviewRun, files);
|
|
6545
|
+
const reviewer = deps.db.createRun({
|
|
6546
|
+
task_slug: reviewRun.task_slug,
|
|
6547
|
+
repo,
|
|
6548
|
+
kind: "reviewer",
|
|
6549
|
+
reviews_run_id: reviewRun.id
|
|
6550
|
+
});
|
|
6551
|
+
const wt = join7(deps.worktreesDir, basename3(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
|
|
6552
|
+
try {
|
|
6553
|
+
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
6554
|
+
await deps.git.setWorktreeIdentity(wt, reviewer.name);
|
|
6555
|
+
const prompt = buildReviewerPrompt(reviewRun, reviewer, checklist, branch);
|
|
6556
|
+
const pid = deps.launch({ worktree: wt, run: reviewer, prompt });
|
|
6557
|
+
deps.db.updateRun(reviewer.id, { status: "running", worktree_path: wt, branch, pid });
|
|
6558
|
+
deps.db.addEvent(reviewRun.id, "step", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E: \u043F\u043E\u0434\u043D\u044F\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440 ${reviewer.name} (#${reviewer.id})`);
|
|
6559
|
+
return deps.db.getRun(reviewer.id);
|
|
6560
|
+
} catch (e) {
|
|
6561
|
+
await deps.git.removeWorktree(repo, wt).catch(() => {
|
|
6562
|
+
});
|
|
6563
|
+
deps.db.updateRun(reviewer.id, { status: "failed" });
|
|
6564
|
+
deps.db.addEvent(reviewer.id, "failed", `\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u043E\u0441\u044C: ${String(e)}`);
|
|
4431
6565
|
throw e;
|
|
4432
6566
|
}
|
|
4433
6567
|
}
|
|
4434
|
-
async function respawnRun(deps, run, mode) {
|
|
6568
|
+
async function respawnRun(deps, run, mode, nudge) {
|
|
4435
6569
|
const wt = run.worktree_path;
|
|
4436
6570
|
if (!wt) throw new Error(`\u043D\u0435\u0442 worktree_path \u0434\u043B\u044F \u043E\u0436\u0438\u0432\u043B\u0435\u043D\u0438\u044F \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}`);
|
|
4437
6571
|
const repo = normalizeRepo(run.repo || deps.repoFallback);
|
|
4438
6572
|
if (mode === "continue") {
|
|
4439
|
-
const
|
|
4440
|
-
const pid2 = deps.launch({ worktree: wt, run, prompt:
|
|
6573
|
+
const prompt2 = nudge ?? (run.kind === "hero" ? HERO_CONTINUE_NUDGE : CONTINUE_NUDGE);
|
|
6574
|
+
const pid2 = deps.launch({ worktree: wt, run, prompt: prompt2, continueSession: true });
|
|
4441
6575
|
deps.db.updateRun(run.id, { status: "running", pid: pid2 });
|
|
4442
6576
|
return;
|
|
4443
6577
|
}
|
|
4444
|
-
if (!
|
|
6578
|
+
if (!existsSync4(wt)) {
|
|
4445
6579
|
const branch = run.branch || run.task_slug;
|
|
4446
6580
|
const source = await addWorktree(repo, wt, branch);
|
|
4447
6581
|
await setWorktreeIdentity(wt, run.name);
|
|
4448
6582
|
const sourceMsg = worktreeSourceMessage(branch, source);
|
|
4449
6583
|
if (sourceMsg) deps.db.addEvent(run.id, "step", sourceMsg);
|
|
4450
6584
|
}
|
|
4451
|
-
const task = await
|
|
4452
|
-
|
|
6585
|
+
const [task, verifyMode] = await Promise.all([
|
|
6586
|
+
deps.getTask(run.task_slug),
|
|
6587
|
+
deps.verifyMode ? deps.verifyMode(repo) : Promise.resolve(void 0)
|
|
6588
|
+
]);
|
|
6589
|
+
const taskContext = task && deps.getTaskContext ? await deps.getTaskContext(task) : void 0;
|
|
6590
|
+
const prompt = run.kind === "hero" ? buildHeroPrompt(task, run, taskContext) : buildPrompt(task, run, verifyMode, taskContext);
|
|
4453
6591
|
const pid = deps.launch({ worktree: wt, run, prompt });
|
|
4454
6592
|
deps.db.updateRun(run.id, { status: "running", pid });
|
|
4455
6593
|
}
|
|
6594
|
+
async function syncCrashedOnStartup(deps) {
|
|
6595
|
+
let orphaned = 0;
|
|
6596
|
+
let merged = 0;
|
|
6597
|
+
for (const run of [...deps.db.listRuns("running"), ...deps.db.listRuns("blocked")]) {
|
|
6598
|
+
if (run.pid != null) continue;
|
|
6599
|
+
if (run.pr_url && deps.prMerged) {
|
|
6600
|
+
const isMerged = await deps.prMerged(run.pr_url).catch(() => false);
|
|
6601
|
+
if (isMerged) {
|
|
6602
|
+
if (deps.finishTask) await deps.finishTask(run.task_slug).catch(() => {
|
|
6603
|
+
});
|
|
6604
|
+
deps.db.updateRun(run.id, { status: "done" });
|
|
6605
|
+
deps.db.addEvent(run.id, "done", "PR \u0443\u0436\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D \u2014 \u043F\u0440\u043E\u0433\u043E\u043D \u0437\u0430\u043A\u0440\u044B\u0442 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u0435\u0439 \u043F\u043E\u0441\u043B\u0435 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 (TASWM-128)");
|
|
6606
|
+
merged++;
|
|
6607
|
+
continue;
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
deps.db.updateRun(run.id, { status: "stalled" });
|
|
6611
|
+
deps.db.addEvent(run.id, "stalled", "\u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B \u043F\u043E\u0441\u043B\u0435 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0430 \u0434\u0435\u043C\u043E\u043D\u0430 (\u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0430 \u043D\u0435\u0442) \u2014 \u0436\u0434\u0451\u0442 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430 (swarm wake)");
|
|
6612
|
+
orphaned++;
|
|
6613
|
+
}
|
|
6614
|
+
return { orphaned, merged };
|
|
6615
|
+
}
|
|
4456
6616
|
async function makeStore(repo) {
|
|
4457
|
-
if (SWARM_BACKEND !== "client") return new SwarmDb(DB_PATH, repo);
|
|
6617
|
+
if (SWARM_BACKEND !== "client") return { store: new SwarmDb(DB_PATH, repo), reconciled: true };
|
|
4458
6618
|
const failing = async () => ({ isError: true });
|
|
4459
|
-
const call = await
|
|
4460
|
-
|
|
6619
|
+
const [call, repoRegistry] = await Promise.all([
|
|
6620
|
+
connectSwarmMcp().then((c) => c ?? failing),
|
|
6621
|
+
buildRepoRegistry(SWARM_REPO_PATHS)
|
|
6622
|
+
]);
|
|
6623
|
+
if (SWARM_REPO_PATHS.length > 0 && repoRegistry.length < SWARM_REPO_PATHS.length) {
|
|
6624
|
+
console.warn(
|
|
6625
|
+
`[overmind] \u0440\u0435\u043F\u043E-\u0440\u0435\u0435\u0441\u0442\u0440: ${repoRegistry.length}/${SWARM_REPO_PATHS.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)`
|
|
6626
|
+
);
|
|
6627
|
+
}
|
|
6628
|
+
const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo, repoRegistry });
|
|
4461
6629
|
await client.hydrate();
|
|
4462
|
-
|
|
4463
|
-
|
|
6630
|
+
const rec = client.reconcileRuntime({ worktreesDir: WORKTREES_DIR, listProcesses: listDroneProcesses });
|
|
6631
|
+
if (rec.reconciled) {
|
|
6632
|
+
console.log(`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: \u0436\u0438\u0432\u044B\u0445 ${rec.alive}, \u043C\u0451\u0440\u0442\u0432\u044B\u0445 ${rec.dead}`);
|
|
6633
|
+
} else {
|
|
6634
|
+
console.warn(
|
|
6635
|
+
`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`
|
|
6636
|
+
);
|
|
6637
|
+
}
|
|
6638
|
+
return { store: client, reconciled: rec.reconciled };
|
|
6639
|
+
}
|
|
6640
|
+
var crashGuardsInstalled = false;
|
|
6641
|
+
function installCrashGuards() {
|
|
6642
|
+
if (crashGuardsInstalled) return;
|
|
6643
|
+
crashGuardsInstalled = true;
|
|
6644
|
+
process.on("uncaughtException", (e) => {
|
|
6645
|
+
console.error("[overmind] uncaughtException \u2014 \u0434\u0435\u043C\u043E\u043D \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0442 \u0440\u0430\u0431\u043E\u0442\u0443:", e);
|
|
6646
|
+
});
|
|
6647
|
+
process.on("unhandledRejection", (e) => {
|
|
6648
|
+
console.error("[overmind] unhandledRejection \u2014 \u0434\u0435\u043C\u043E\u043D \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0435\u0442 \u0440\u0430\u0431\u043E\u0442\u0443:", e);
|
|
6649
|
+
});
|
|
4464
6650
|
}
|
|
4465
6651
|
async function runDaemon(opts) {
|
|
4466
|
-
|
|
6652
|
+
installCrashGuards();
|
|
6653
|
+
const { store: db, reconciled } = await makeStore(opts.repo);
|
|
4467
6654
|
const tl = await connectTaskless();
|
|
4468
|
-
|
|
6655
|
+
if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
|
|
6656
|
+
if (reconciled) {
|
|
6657
|
+
const sync = await syncCrashedOnStartup({
|
|
6658
|
+
db,
|
|
6659
|
+
prMerged: (url) => prStatus(url).then((s) => s.merged),
|
|
6660
|
+
finishTask: (slug) => tl ? tl.finishTask(slug) : Promise.resolve()
|
|
6661
|
+
});
|
|
6662
|
+
if (sync.orphaned || sync.merged) {
|
|
6663
|
+
console.log(`[overmind] startup-sync (TASWM-128): \u043E\u0441\u0438\u0440\u043E\u0442\u0435\u043B\u043E ${sync.orphaned} \u2192 stalled, \u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E ${sync.merged} \u2192 done`);
|
|
6664
|
+
}
|
|
6665
|
+
} else {
|
|
6666
|
+
console.warn("[overmind] startup-sync (TASWM-128) \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D \u2014 \u0440\u0435\u043A\u043E\u043D\u0441\u0430\u0439\u043B \u0440\u0430\u043D\u0442\u0430\u0439\u043C\u0430 \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F, \u0436\u0438\u0432\u044B\u0445 \u0434\u0440\u043E\u043D\u043E\u0432 \u043D\u0435 \u0445\u043E\u0440\u043E\u043D\u0438\u043C \u043F\u043E \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u043E\u0441\u0442\u0438");
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
6669
|
+
const launch = makeLauncher({ overmindEntry: THIS_FILE, dbPath: DB_PATH, runLogsDir: RUN_LOGS_DIR });
|
|
4469
6670
|
const baseSpawn = {
|
|
4470
6671
|
db,
|
|
4471
6672
|
repo: opts.repo,
|
|
@@ -4478,25 +6679,141 @@ async function runDaemon(opts) {
|
|
|
4478
6679
|
const client = db;
|
|
4479
6680
|
intake = { claim: () => client.claim(), maxInFlight: SWARM_MAX_IN_FLIGHT, intervalMs: SWARM_CLAIM_INTERVAL_MS };
|
|
4480
6681
|
}
|
|
6682
|
+
let rehydrate;
|
|
6683
|
+
if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_REHYDRATE_MS > 0) {
|
|
6684
|
+
const client = db;
|
|
6685
|
+
rehydrate = { run: () => client.hydrate(), ready: () => client.pendingCount() === 0, intervalMs: SWARM_REHYDRATE_MS };
|
|
6686
|
+
}
|
|
6687
|
+
let heartbeat;
|
|
6688
|
+
if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_PROD_HEARTBEAT_MS > 0) {
|
|
6689
|
+
const client = db;
|
|
6690
|
+
heartbeat = { push: () => client.heartbeat(), intervalMs: SWARM_PROD_HEARTBEAT_MS };
|
|
6691
|
+
}
|
|
6692
|
+
let anomalies;
|
|
6693
|
+
if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
|
|
6694
|
+
const client = db;
|
|
6695
|
+
anomalies = {
|
|
6696
|
+
tracker: new AnomalyTracker(SWARM_ANOMALY_RENOTIFY_MS),
|
|
6697
|
+
thresholds: {
|
|
6698
|
+
failedSpikeCount: SWARM_ANOMALY_FAILED_SPIKE_COUNT,
|
|
6699
|
+
failedSpikeWindowMs: SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
|
|
6700
|
+
queueStalledMs: SWARM_ANOMALY_QUEUE_STALLED_MS,
|
|
6701
|
+
prStuckMergeMs: SWARM_ANOMALY_PR_STUCK_MERGE_MS,
|
|
6702
|
+
prNoReviewerMs: SWARM_ANOMALY_PR_NO_REVIEWER_MS,
|
|
6703
|
+
questionUnansweredMs: SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
|
|
6704
|
+
outboxStuckMs: SWARM_ANOMALY_OUTBOX_STUCK_MS
|
|
6705
|
+
},
|
|
6706
|
+
autoReviewEnabled: SWARM_AUTO_REVIEW,
|
|
6707
|
+
outboxPendingCount: () => client.pendingCount(),
|
|
6708
|
+
notify: (signal) => client.notifyAnomaly(signal)
|
|
6709
|
+
};
|
|
6710
|
+
}
|
|
6711
|
+
let maxDrones;
|
|
6712
|
+
if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
|
|
6713
|
+
const client = db;
|
|
6714
|
+
maxDrones = makeMaxDronesReader({
|
|
6715
|
+
fetch: () => client.ownerMaxDrones(),
|
|
6716
|
+
fallback: SWARM_MAX_IN_FLIGHT > 0 ? SWARM_MAX_IN_FLIGHT : 3,
|
|
6717
|
+
ttlMs: 1e4
|
|
6718
|
+
});
|
|
6719
|
+
}
|
|
6720
|
+
let verifyModeReader;
|
|
6721
|
+
if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
|
|
6722
|
+
const client = db;
|
|
6723
|
+
verifyModeReader = makeVerifyModeReader({
|
|
6724
|
+
fetch: async (repo) => {
|
|
6725
|
+
const repoFullName = await remoteRepoFullName(repo).catch(() => null);
|
|
6726
|
+
return repoFullName ? client.repoVerifyMode(repoFullName) : null;
|
|
6727
|
+
},
|
|
6728
|
+
fallback: SWARM_VERIFY,
|
|
6729
|
+
ttlMs: 6e4
|
|
6730
|
+
});
|
|
6731
|
+
}
|
|
6732
|
+
let spawnReviewerFn;
|
|
6733
|
+
let verdictCommentFn;
|
|
6734
|
+
let reviewCutoffAtValue;
|
|
6735
|
+
if (SWARM_AUTO_REVIEW) {
|
|
6736
|
+
spawnReviewerFn = (reviewRun, files) => spawnReviewer(
|
|
6737
|
+
{
|
|
6738
|
+
db,
|
|
6739
|
+
repoFallback: opts.repo,
|
|
6740
|
+
worktreesDir: WORKTREES_DIR,
|
|
6741
|
+
launch,
|
|
6742
|
+
git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree }
|
|
6743
|
+
},
|
|
6744
|
+
reviewRun,
|
|
6745
|
+
files
|
|
6746
|
+
).then(() => {
|
|
6747
|
+
});
|
|
6748
|
+
verdictCommentFn = (prUrl, sinceIso) => latestVerdictComment(prUrl, sinceIso);
|
|
6749
|
+
reviewCutoffAtValue = reviewCutoffAt();
|
|
6750
|
+
}
|
|
6751
|
+
const narrator = makeOvermindNarrator();
|
|
4481
6752
|
const deps = {
|
|
4482
6753
|
db,
|
|
6754
|
+
narrate: narrator.narrate,
|
|
4483
6755
|
spawn: async (run) => {
|
|
4484
|
-
const task =
|
|
4485
|
-
|
|
6756
|
+
const [task, verifyMode] = await Promise.all([
|
|
6757
|
+
tl ? tl.getTask(run.task_slug) : Promise.resolve(null),
|
|
6758
|
+
verifyModeReader ? verifyModeReader(normalizeRepo(run.repo || opts.repo)) : Promise.resolve(void 0)
|
|
6759
|
+
]);
|
|
6760
|
+
const taskContext = task && tl ? await fetchTaskContext(tl, task) : void 0;
|
|
6761
|
+
const prompt = run.kind === "hero" ? buildHeroPrompt(task, run, taskContext) : buildPrompt(task, run, verifyMode, taskContext);
|
|
4486
6762
|
await spawnDrone({ ...baseSpawn, buildPrompt: () => prompt }, run);
|
|
4487
6763
|
if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
|
|
4488
6764
|
if (tl) await tl.startTask(run.task_slug);
|
|
4489
6765
|
},
|
|
4490
|
-
prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting })),
|
|
6766
|
+
prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files, state: s.state })),
|
|
4491
6767
|
// Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
|
|
4492
6768
|
// (фолбэк — дефолт-репо роя). Реальный gh; логика усыновления — в tickOnce (тестируется).
|
|
4493
6769
|
openPrsForBranch: (branch, repoDir) => openPrsForBranch(branch, repoDir || opts.repo),
|
|
6770
|
+
// Смерженный PR ветки-слага (TASWM-206) — тот же фолбэк на дефолт-репо роя, что и
|
|
6771
|
+
// openPrsForBranch выше; реальный gh, логика закрытия дублей — в tickOnce (тестируется).
|
|
6772
|
+
mergedPrForBranch: (branch, repoDir) => mergedPrForBranch(branch, repoDir || opts.repo),
|
|
4494
6773
|
finishTask: (slug) => tl ? tl.finishTask(slug) : Promise.resolve(),
|
|
4495
6774
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p),
|
|
4496
6775
|
isAlive: pidAlive,
|
|
6776
|
+
// Учёт cost/usage (TASWM-188): реальный аккаунтант поверх RUN_LOGS_DIR + db.
|
|
6777
|
+
accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db }),
|
|
6778
|
+
// Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
|
|
6779
|
+
livePpids,
|
|
6780
|
+
// Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
|
|
6781
|
+
// POST /swarm/kill → stopped в проде → overmind гасит живой pid на реконсайле).
|
|
6782
|
+
kill: (pid) => killProcessGroup(pid, "SIGTERM"),
|
|
6783
|
+
// Защитный sweep осиротевших процессов/docker-контейнеров (TASWM-210): активные
|
|
6784
|
+
// worktree-пути и docker-префиксы — из ВСЕХ ранов в БД (не только running/blocked —
|
|
6785
|
+
// paused/stalled/queued/review/storm_dead тоже держат свой worktree/контейнеры, их
|
|
6786
|
+
// сносить рано), терминальные (done/failed/stopped/superseded) — кандидаты на снос.
|
|
6787
|
+
sweep: {
|
|
6788
|
+
run: () => {
|
|
6789
|
+
const runsWithWorktree = db.listRuns().filter((r) => r.worktree_path);
|
|
6790
|
+
return sweepOrphans({
|
|
6791
|
+
worktreesDir: WORKTREES_DIR,
|
|
6792
|
+
activeWorktreePaths: new Set(runsWithWorktree.filter((r) => !isTerminalStatus(r.status)).map((r) => r.worktree_path)),
|
|
6793
|
+
containerRunPrefixes: runsWithWorktree.map((r) => ({
|
|
6794
|
+
prefix: composeProjectPrefix(basename3(r.worktree_path)),
|
|
6795
|
+
active: !isTerminalStatus(r.status)
|
|
6796
|
+
}))
|
|
6797
|
+
}).then(() => {
|
|
6798
|
+
});
|
|
6799
|
+
},
|
|
6800
|
+
intervalMs: SWARM_SWEEP_INTERVAL_MS,
|
|
6801
|
+
lastAt: null
|
|
6802
|
+
},
|
|
4497
6803
|
isDirty: (p) => worktreeDirty(p),
|
|
4498
6804
|
// git-активность как liveness (TASWM-67): последний коммит worktree — реальный git-вызов.
|
|
4499
6805
|
lastCommitMs: (p) => worktreeLastCommitMs(p),
|
|
6806
|
+
// mtime транскрипта claude-сессии как liveness (TASWM-136): точнее коммитов — обновляется
|
|
6807
|
+
// на каждое действие дрона (Edit/Read/tool-use), не только на коммит.
|
|
6808
|
+
transcriptMtimeMs: (p) => transcriptMtimeMs(p),
|
|
6809
|
+
// Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
|
|
6810
|
+
resourcesOk: hostResourcesOk,
|
|
6811
|
+
// Ресурс-гейт v2 (TASWM-151): жёсткий порог активной разгрузки — те же сырые показания
|
|
6812
|
+
// хоста (общий кэш в resources.ts), но заведомо строже настроенные пороги.
|
|
6813
|
+
reliefNeeded: hostReliefNeeded,
|
|
6814
|
+
// Состояние градуального контроллера (окна ramp/pause, reliefSince) — заводим ОДИН раз
|
|
6815
|
+
// на цикл демона, tickOnce (manageRelief) мутирует эти же поля каждый тик.
|
|
6816
|
+
relief: {},
|
|
4500
6817
|
// merge-main-in: review-PR конфликтует с main → влить main в ветку PR, не переделывая
|
|
4501
6818
|
// фичу. Реальные git-примитивы + launch; логика — в resolveConflict (тестируется).
|
|
4502
6819
|
resolveConflict: (reviewRun) => resolveConflict(
|
|
@@ -4512,13 +6829,30 @@ async function runDaemon(opts) {
|
|
|
4512
6829
|
}),
|
|
4513
6830
|
// Авто-оживление / пробуждение прогона в его же worktree — общий примитив respawnRun
|
|
4514
6831
|
// (kind-aware: герой ← HERO_CONTINUE_NUDGE/buildHeroPrompt). Лимит — RESPAWN_LIMIT, считает tickOnce.
|
|
4515
|
-
|
|
4516
|
-
|
|
6832
|
+
// nudge прокинут для CI-побудки (TASWM-102): tickOnce передаёт buildCiWakeNudge(summary).
|
|
6833
|
+
respawn: (run, mode, nudge) => respawnRun(
|
|
6834
|
+
{
|
|
6835
|
+
db,
|
|
6836
|
+
launch,
|
|
6837
|
+
repoFallback: opts.repo,
|
|
6838
|
+
worktreesDir: WORKTREES_DIR,
|
|
6839
|
+
getTask: (slug) => tl ? tl.getTask(slug) : Promise.resolve(null),
|
|
6840
|
+
...verifyModeReader ? { verifyMode: verifyModeReader } : {},
|
|
6841
|
+
getTaskContext: (task) => tl ? fetchTaskContext(tl, task) : Promise.resolve(void 0)
|
|
6842
|
+
},
|
|
4517
6843
|
run,
|
|
4518
|
-
mode
|
|
6844
|
+
mode,
|
|
6845
|
+
nudge
|
|
4519
6846
|
),
|
|
4520
|
-
worktreeExists: (p) =>
|
|
6847
|
+
worktreeExists: (p) => existsSync4(p),
|
|
4521
6848
|
hasTranscript: (p) => sessionTranscriptExists(p),
|
|
6849
|
+
// CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
|
|
6850
|
+
// (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
|
|
6851
|
+
// строится над failing/failingRunIds ИЗ checks — без повторного `gh pr checks`.
|
|
6852
|
+
ci: {
|
|
6853
|
+
checks: (url) => prCiStatus(url),
|
|
6854
|
+
failureSummary: (failing, failingRunIds) => ciFailureSummary(failing, failingRunIds)
|
|
6855
|
+
},
|
|
4522
6856
|
// Штормовой режим (TASWM-23): health-probe на отбой (реальный claude) + внешний
|
|
4523
6857
|
// сигнал SWARM_STORM_URL (2-й слой, если задан). Логика/пороги — в manageStorm/storm.ts.
|
|
4524
6858
|
storm: {
|
|
@@ -4527,8 +6861,20 @@ async function runDaemon(opts) {
|
|
|
4527
6861
|
// заданном SWARM_STORM_URL, иначе опускаем — не передаём undefined.
|
|
4528
6862
|
...SWARM_STORM_URL ? { externalDown: () => fetchExternalStorm(SWARM_STORM_URL) } : {}
|
|
4529
6863
|
},
|
|
6864
|
+
// Живой потолок дронов (client-режим, TASWM-126): ключ есть только когда сконфигурен.
|
|
6865
|
+
...maxDrones ? { maxDrones } : {},
|
|
4530
6866
|
// Авто-интейк (client-режим): ключ есть только когда intake сконфигурен (exactOptionalPropertyTypes).
|
|
4531
|
-
...intake ? { intake } : {}
|
|
6867
|
+
...intake ? { intake } : {},
|
|
6868
|
+
// Периодический re-hydrate зеркала (client-режим): ключ есть только когда сконфигурен.
|
|
6869
|
+
...rehydrate ? { rehydrate } : {},
|
|
6870
|
+
// Пульс overmind в прод (client-режим, TASWM-129): ключ есть только когда сконфигурен.
|
|
6871
|
+
...heartbeat ? { heartbeat } : {},
|
|
6872
|
+
// Встроенный надзор (client-режим, TASWM-194): ключ есть только когда сконфигурен.
|
|
6873
|
+
...anomalies ? { anomalies } : {},
|
|
6874
|
+
// Авто-ревью (TASWM-148): ключи есть только когда SWARM_AUTO_REVIEW включён (дефолт да) —
|
|
6875
|
+
// spawnReviewerFn/verdictCommentFn/reviewCutoffAtValue выставляются В ОДНОМ if(SWARM_AUTO_REVIEW)
|
|
6876
|
+
// блоке выше, поэтому один и тот же гейт на все три (без отдельных тернарных условий).
|
|
6877
|
+
...spawnReviewerFn ? { spawnReviewer: spawnReviewerFn, verdictComment: verdictCommentFn, reviewCap: SWARM_REVIEW_CAP, reviewCutoffAt: reviewCutoffAtValue } : {}
|
|
4532
6878
|
};
|
|
4533
6879
|
const web = startWeb({
|
|
4534
6880
|
db,
|
|
@@ -4536,10 +6882,12 @@ async function runDaemon(opts) {
|
|
|
4536
6882
|
// дефолт роя в whitelist repo для /spawn
|
|
4537
6883
|
taskUrlBase: TASKLESS_TASK_URL_BASE,
|
|
4538
6884
|
// фолбэк-база ссылки на задачу (прогоны без снимка task_url)
|
|
6885
|
+
narratorLog: narrator.log,
|
|
6886
|
+
// overmind narration-лог (TASWM-123) — тот же нарратор, что deps.narrate
|
|
4539
6887
|
replay: (qid, chosen) => replayDecision(
|
|
4540
6888
|
{
|
|
4541
6889
|
db,
|
|
4542
|
-
kill: (pid) =>
|
|
6890
|
+
kill: (pid) => killProcessGroup(pid, "SIGTERM"),
|
|
4543
6891
|
closePR: (u) => closePR(u),
|
|
4544
6892
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
|
|
4545
6893
|
},
|
|
@@ -4551,7 +6899,7 @@ async function runDaemon(opts) {
|
|
|
4551
6899
|
stop: (slug, reason) => cancelRun(
|
|
4552
6900
|
{
|
|
4553
6901
|
db,
|
|
4554
|
-
kill: (pid) =>
|
|
6902
|
+
kill: (pid) => killProcessGroup(pid, "SIGTERM"),
|
|
4555
6903
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p)
|
|
4556
6904
|
},
|
|
4557
6905
|
slug,
|
|
@@ -4565,6 +6913,10 @@ async function runDaemon(opts) {
|
|
|
4565
6913
|
console.log(`\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430: http://127.0.0.1:${web.port} \u2190 \u043E\u0442\u043A\u0440\u043E\u0439 \u0438 \u0432\u043E\u0439\u0434\u0438 \u0447\u0435\u0440\u0435\u0437 Taskless`);
|
|
4566
6914
|
const interval = opts.intervalMs ?? 4e3;
|
|
4567
6915
|
writeHeartbeat(HEARTBEAT_PATH, Date.now());
|
|
6916
|
+
if (deps.sweep) {
|
|
6917
|
+
deps.sweep.lastAt = Date.now();
|
|
6918
|
+
await deps.sweep.run().catch((e) => console.error("[overmind] \u0441\u0442\u0430\u0440\u0442\u043E\u0432\u044B\u0439 sweep \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e));
|
|
6919
|
+
}
|
|
4568
6920
|
for (; ; ) {
|
|
4569
6921
|
try {
|
|
4570
6922
|
writeHeartbeat(HEARTBEAT_PATH, Date.now());
|
|
@@ -4593,22 +6945,38 @@ if (isEntrypoint) {
|
|
|
4593
6945
|
}
|
|
4594
6946
|
}
|
|
4595
6947
|
export {
|
|
6948
|
+
CI_CHECK_COOLDOWN_MS,
|
|
6949
|
+
CI_WAKE_COOLDOWN_MS,
|
|
6950
|
+
CI_WAKE_ESCALATION_MARK,
|
|
6951
|
+
CI_WAKE_LIMIT,
|
|
4596
6952
|
CONFLICT_RESOLVE_COOLDOWN_MS,
|
|
4597
6953
|
CONTINUE_NUDGE,
|
|
4598
6954
|
HEADLESS_BLOCK,
|
|
4599
6955
|
HERO_CONTINUE_NUDGE,
|
|
4600
6956
|
OVERMIND_ENTRY,
|
|
6957
|
+
RESOLVE_ESCALATION_MARK,
|
|
4601
6958
|
RESOLVE_LIMIT,
|
|
4602
6959
|
RESPAWN_LIMIT,
|
|
4603
6960
|
RESPAWN_YOUNG_MS,
|
|
6961
|
+
REVIEW_ESCALATION_MARK,
|
|
6962
|
+
REVIEW_SPAWN_LIMIT,
|
|
4604
6963
|
STALL_MS,
|
|
6964
|
+
buildCiWakeNudge,
|
|
4605
6965
|
buildHeroPrompt,
|
|
4606
6966
|
buildPrompt,
|
|
4607
6967
|
buildResolverPrompt,
|
|
6968
|
+
buildReviewChecklist,
|
|
6969
|
+
buildReviewerPrompt,
|
|
4608
6970
|
cancelRun,
|
|
6971
|
+
hasLiveDescendant,
|
|
6972
|
+
listDroneProcesses,
|
|
6973
|
+
livePpids,
|
|
6974
|
+
parseLivePpids,
|
|
4609
6975
|
replayDecision,
|
|
4610
6976
|
resolveConflict,
|
|
4611
6977
|
respawnRun,
|
|
4612
6978
|
runDaemon,
|
|
6979
|
+
spawnReviewer,
|
|
6980
|
+
syncCrashedOnStartup,
|
|
4613
6981
|
tickOnce
|
|
4614
6982
|
};
|