@taskless-app/swarm 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/dist/cli.js +1694 -297
- package/dist/overmind.js +1932 -439
- package/docs/HERO.md +3 -0
- package/package.json +17 -15
package/dist/overmind.js
CHANGED
|
@@ -8,20 +8,382 @@ 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
|
+
}
|
|
96
|
+
}
|
|
97
|
+
push(laneEnd);
|
|
98
|
+
return segs;
|
|
99
|
+
}
|
|
100
|
+
function computeBlocker(status, stopReason, lastStep, pr, ci, deps, openQ) {
|
|
101
|
+
switch (status) {
|
|
102
|
+
case "queued": {
|
|
103
|
+
const unmet = deps.filter((d) => !d.met);
|
|
104
|
+
if (unmet.length) {
|
|
105
|
+
const who = unmet.map((d) => d.name ? `${d.slug}\xB7${d.name}` : d.slug).join(", ");
|
|
106
|
+
return { icon: "\u26D4", text: `\u0436\u0434\u0451\u0442 ${who}`, tone: "dep" };
|
|
107
|
+
}
|
|
108
|
+
return { icon: "\u23F8", text: "\u0436\u0434\u0451\u0442 \u0441\u043B\u043E\u0442\u0430 / \u0441\u043F\u0430\u0432\u043D\u0430", tone: "queued" };
|
|
109
|
+
}
|
|
110
|
+
case "running":
|
|
111
|
+
return { icon: "\u25B6", text: lastStep ? `\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442: ${lastStep}` : "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", tone: "run" };
|
|
112
|
+
case "blocked":
|
|
113
|
+
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" };
|
|
114
|
+
case "paused":
|
|
115
|
+
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: "paused" };
|
|
116
|
+
case "storm_dead":
|
|
117
|
+
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" };
|
|
118
|
+
case "review": {
|
|
119
|
+
if (!pr) return { icon: "\u{1F440}", text: "review \u0431\u0435\u0437 PR", tone: "review" };
|
|
120
|
+
if (!ci) return { icon: "\u23F3", text: "CI: \u0436\u0434\u0443 \u0441\u0442\u0430\u0442\u0443\u0441\u2026", tone: "review" };
|
|
121
|
+
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" };
|
|
122
|
+
return { icon: ci.tone === "fail" ? "\u274C" : "\u23F3", text: ci.text.replace(/^[^ ]+ /, ""), tone: ci.tone };
|
|
123
|
+
}
|
|
124
|
+
case "stalled":
|
|
125
|
+
return { icon: "\u26A0\uFE0F", text: stopReason || (lastStep ? `\u043C\u043E\u043B\u0447\u0438\u0442: ${lastStep}` : "\u043C\u043E\u043B\u0447\u0438\u0442"), tone: "stalled" };
|
|
126
|
+
case "failed":
|
|
127
|
+
return { icon: "\u274C", text: stopReason || "\u0443\u043F\u0430\u043B", tone: "fail" };
|
|
128
|
+
case "stopped":
|
|
129
|
+
return { icon: "\u23F9", text: stopReason || "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", tone: "queued" };
|
|
130
|
+
case "done":
|
|
131
|
+
return { icon: "\u2705", text: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", tone: "done" };
|
|
132
|
+
default:
|
|
133
|
+
return { icon: "\u2022", text: status, tone: "queued" };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function pickHeadline(lanes) {
|
|
137
|
+
const live = lanes.filter((l2) => l2.live && !l2.obsolete);
|
|
138
|
+
const by = (pred) => live.find(pred);
|
|
139
|
+
const fmt = (l2, extra) => `${l2.name} \xB7 ${l2.slug} \u2014 ${extra}`;
|
|
140
|
+
let l = by((x) => x.status === "review" && x.ci?.tone === "fail");
|
|
141
|
+
if (l) return { icon: "\u274C", text: fmt(l, "CI \u0443\u043F\u0430\u043B \u2014 \u0447\u0438\u043D\u0438\u0442\u044C"), tone: "fail" };
|
|
142
|
+
l = by((x) => x.status === "blocked");
|
|
143
|
+
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" };
|
|
144
|
+
l = by((x) => x.status === "stalled");
|
|
145
|
+
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" };
|
|
146
|
+
l = by((x) => x.status === "review" && x.blocker.tone === "merge");
|
|
147
|
+
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" };
|
|
148
|
+
l = by((x) => x.status === "queued" && x.dependsOn.some((d) => !d.met));
|
|
149
|
+
if (l) {
|
|
150
|
+
const dep = l.dependsOn.find((d) => !d.met);
|
|
151
|
+
return { icon: "\u26D4", text: fmt(l, `\u0436\u0434\u0451\u0442 ${dep.slug}${dep.name ? " \xB7 " + dep.name : ""}`), tone: "dep" };
|
|
152
|
+
}
|
|
153
|
+
l = by((x) => x.status === "review");
|
|
154
|
+
if (l) return { icon: "\u23F3", text: fmt(l, "review \u2014 CI \u0438\u0434\u0451\u0442"), tone: "review" };
|
|
155
|
+
l = by((x) => x.status === "running");
|
|
156
|
+
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" };
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
var DEFAULT_RUN_MS;
|
|
160
|
+
var init_timeline = __esm({
|
|
161
|
+
"../swarm-core/src/timeline.ts"() {
|
|
162
|
+
"use strict";
|
|
163
|
+
DEFAULT_RUN_MS = 25 * 6e4;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ../swarm-core/src/status.ts
|
|
168
|
+
function statusMeta(status) {
|
|
169
|
+
return STATUS_META[status] ?? FALLBACK;
|
|
170
|
+
}
|
|
171
|
+
function isTerminalStatus(status) {
|
|
172
|
+
return statusMeta(status).terminal;
|
|
173
|
+
}
|
|
174
|
+
function statusTuples() {
|
|
175
|
+
const out = {};
|
|
176
|
+
for (const [status, m] of Object.entries(STATUS_META)) {
|
|
177
|
+
out[status] = [m.pill, m.label, m.color];
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
var STATUS_META, FALLBACK, TERMINAL_STATUSES;
|
|
182
|
+
var init_status = __esm({
|
|
183
|
+
"../swarm-core/src/status.ts"() {
|
|
184
|
+
"use strict";
|
|
185
|
+
STATUS_META = {
|
|
186
|
+
queued: { pill: "queued", label: "\u0432 \u043E\u0447\u0435\u0440\u0435\u0434\u0438", color: "--muted-foreground", terminal: false },
|
|
187
|
+
running: { pill: "run", label: "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", color: "--primary", terminal: false },
|
|
188
|
+
blocked: { pill: "blocked", label: "\u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430", color: "--warning", terminal: false },
|
|
189
|
+
paused: { pill: "paused", label: "\u0437\u0430\u043C\u0435\u0440", color: "--warning", terminal: false },
|
|
190
|
+
// overmind был недоступен — ждёт пробуждения (TASWM-41)
|
|
191
|
+
review: { pill: "review", label: "\u043D\u0430 \u0440\u0435\u0432\u044C\u044E", color: "--info", terminal: false },
|
|
192
|
+
done: { pill: "done", label: "\u0441\u043C\u0435\u0440\u0436\u0435\u043D\u043E", color: "--success", terminal: true },
|
|
193
|
+
failed: { pill: "failed", label: "\u0443\u043F\u0430\u043B", color: "--destructive", terminal: true },
|
|
194
|
+
stalled: { pill: "stalled", label: "\u043F\u043E\u0433\u0438\u0431/\u0437\u0430\u0432\u0438\u0441", color: "--destructive", terminal: false },
|
|
195
|
+
// молчит/погиб, но ещё разруливается — не терминал
|
|
196
|
+
storm_dead: { pill: "storm", label: "\u0448\u0442\u043E\u0440\u043C \u2014 \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D", color: "--warning", terminal: false },
|
|
197
|
+
// умер в шторм Anthropic, поднимется после отбоя (TASWM-23)
|
|
198
|
+
stopped: { pill: "stopped", label: "\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", color: "--muted-foreground", terminal: true },
|
|
199
|
+
superseded: { pill: "superseded", label: "\u043F\u0435\u0440\u0435\u0438\u0433\u0440\u0430\u043D", color: "--muted-foreground", terminal: true }
|
|
200
|
+
};
|
|
201
|
+
FALLBACK = STATUS_META.queued;
|
|
202
|
+
TERMINAL_STATUSES = Object.keys(STATUS_META).filter(
|
|
203
|
+
(s) => STATUS_META[s].terminal
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ../swarm-core/src/time.ts
|
|
209
|
+
function stamp(d = /* @__PURE__ */ new Date()) {
|
|
210
|
+
return d.toISOString().slice(0, 19).replace("T", " ");
|
|
211
|
+
}
|
|
212
|
+
function stampFromEpochSec(sec) {
|
|
213
|
+
return stamp(new Date(sec * 1e3));
|
|
214
|
+
}
|
|
215
|
+
function sqliteMs(ts) {
|
|
216
|
+
return Date.parse(ts.replace(" ", "T") + "Z");
|
|
217
|
+
}
|
|
218
|
+
function toStamp(v) {
|
|
219
|
+
if (typeof v !== "string" || !v) return null;
|
|
220
|
+
const d = new Date(v);
|
|
221
|
+
if (Number.isNaN(d.getTime())) return v.includes("T") ? v.slice(0, 19).replace("T", " ") : v;
|
|
222
|
+
return stamp(d);
|
|
223
|
+
}
|
|
224
|
+
var DAY_MS;
|
|
225
|
+
var init_time = __esm({
|
|
226
|
+
"../swarm-core/src/time.ts"() {
|
|
227
|
+
"use strict";
|
|
228
|
+
DAY_MS = 864e5;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ../swarm-core/src/windows.ts
|
|
233
|
+
function resolveRoyWindows(env = {}) {
|
|
234
|
+
return {
|
|
235
|
+
windowMs: Number(env.ROY_WINDOW_H || ROY_WINDOW_DEFAULTS.windowH) * 36e5,
|
|
236
|
+
maxTerminalLanes: Number(env.ROY_MAX_DONE || ROY_WINDOW_DEFAULTS.maxDone),
|
|
237
|
+
maxAxisMs: Number(env.ROY_AXIS_H || ROY_WINDOW_DEFAULTS.axisH) * 36e5
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
var ROY_WINDOW_DEFAULTS;
|
|
241
|
+
var init_windows = __esm({
|
|
242
|
+
"../swarm-core/src/windows.ts"() {
|
|
243
|
+
"use strict";
|
|
244
|
+
ROY_WINDOW_DEFAULTS = { windowH: 5, maxDone: 6, axisH: 6 };
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// ../swarm-core/src/slug.ts
|
|
249
|
+
var init_slug = __esm({
|
|
250
|
+
"../swarm-core/src/slug.ts"() {
|
|
251
|
+
"use strict";
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ../swarm-core/src/text.ts
|
|
256
|
+
function extractText(d) {
|
|
257
|
+
if (typeof d === "string") return d;
|
|
258
|
+
const out = [];
|
|
259
|
+
const walk = (n) => {
|
|
260
|
+
if (!n || typeof n !== "object") return;
|
|
261
|
+
const node = n;
|
|
262
|
+
if (node.text) out.push(node.text);
|
|
263
|
+
(node.content ?? []).forEach(walk);
|
|
264
|
+
};
|
|
265
|
+
walk(d);
|
|
266
|
+
return out.join(" ");
|
|
267
|
+
}
|
|
268
|
+
var init_text = __esm({
|
|
269
|
+
"../swarm-core/src/text.ts"() {
|
|
270
|
+
"use strict";
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ../swarm-core/src/names.ts
|
|
275
|
+
function toRoman(n) {
|
|
276
|
+
if (n < 1 || n > 3999) return String(n);
|
|
277
|
+
const table = [
|
|
278
|
+
[1e3, "M"],
|
|
279
|
+
[900, "CM"],
|
|
280
|
+
[500, "D"],
|
|
281
|
+
[400, "CD"],
|
|
282
|
+
[100, "C"],
|
|
283
|
+
[90, "XC"],
|
|
284
|
+
[50, "L"],
|
|
285
|
+
[40, "XL"],
|
|
286
|
+
[10, "X"],
|
|
287
|
+
[9, "IX"],
|
|
288
|
+
[5, "V"],
|
|
289
|
+
[4, "IV"],
|
|
290
|
+
[1, "I"]
|
|
291
|
+
];
|
|
292
|
+
let out = "";
|
|
293
|
+
for (const [v, sym] of table) while (n >= v) out += sym, n -= v;
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
function genName(base2, g) {
|
|
297
|
+
return g <= 1 ? base2 : `${base2} ${toRoman(g)}`;
|
|
298
|
+
}
|
|
299
|
+
function baseName(name) {
|
|
300
|
+
return name.split(" ")[0];
|
|
301
|
+
}
|
|
302
|
+
var DRONE_POOL, ACTIVE_NAME_STATUSES;
|
|
303
|
+
var init_names = __esm({
|
|
304
|
+
"../swarm-core/src/names.ts"() {
|
|
305
|
+
"use strict";
|
|
306
|
+
DRONE_POOL = [
|
|
307
|
+
// Латинский бестиарий (15)
|
|
308
|
+
"SCARAB",
|
|
309
|
+
"VESPA",
|
|
310
|
+
"MANTIS",
|
|
311
|
+
"HORNET",
|
|
312
|
+
"CICADA",
|
|
313
|
+
"LOCUST",
|
|
314
|
+
"ATTA",
|
|
315
|
+
"FORMICA",
|
|
316
|
+
"MYRMEX",
|
|
317
|
+
"APIS",
|
|
318
|
+
"TERMES",
|
|
319
|
+
"GRYLLUS",
|
|
320
|
+
"LAMPYRIS",
|
|
321
|
+
"ARACHNE",
|
|
322
|
+
"SPHINX",
|
|
323
|
+
// Мифо-стаи (9)
|
|
324
|
+
"TIAMAT",
|
|
325
|
+
"JORMUNGAND",
|
|
326
|
+
"GRENDEL",
|
|
327
|
+
"TYPHON",
|
|
328
|
+
"FENRIS",
|
|
329
|
+
"GARM",
|
|
330
|
+
"KUKULKAN",
|
|
331
|
+
"LEVIATHAN",
|
|
332
|
+
"SURTUR"
|
|
333
|
+
];
|
|
334
|
+
ACTIVE_NAME_STATUSES = ["queued", "running", "blocked", "review"];
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ../swarm-core/src/index.ts
|
|
339
|
+
var init_src = __esm({
|
|
340
|
+
"../swarm-core/src/index.ts"() {
|
|
341
|
+
"use strict";
|
|
342
|
+
init_types();
|
|
343
|
+
init_timeline();
|
|
344
|
+
init_status();
|
|
345
|
+
init_time();
|
|
346
|
+
init_windows();
|
|
347
|
+
init_slug();
|
|
348
|
+
init_text();
|
|
349
|
+
init_names();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
11
353
|
// src/config.ts
|
|
12
354
|
var config_exports = {};
|
|
13
355
|
__export(config_exports, {
|
|
14
356
|
DB_PATH: () => DB_PATH,
|
|
15
357
|
HEARTBEAT_PATH: () => HEARTBEAT_PATH,
|
|
16
358
|
HEARTBEAT_STALE_MS: () => HEARTBEAT_STALE_MS,
|
|
359
|
+
REVIEW_CUTOFF_PATH: () => REVIEW_CUTOFF_PATH,
|
|
360
|
+
RUN_LOGS_DIR: () => RUN_LOGS_DIR,
|
|
361
|
+
SWARM_ANOMALY_FAILED_SPIKE_COUNT: () => SWARM_ANOMALY_FAILED_SPIKE_COUNT,
|
|
362
|
+
SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS: () => SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
|
|
363
|
+
SWARM_ANOMALY_OUTBOX_STUCK_MS: () => SWARM_ANOMALY_OUTBOX_STUCK_MS,
|
|
364
|
+
SWARM_ANOMALY_PR_NO_REVIEWER_MS: () => SWARM_ANOMALY_PR_NO_REVIEWER_MS,
|
|
365
|
+
SWARM_ANOMALY_PR_STUCK_MERGE_MS: () => SWARM_ANOMALY_PR_STUCK_MERGE_MS,
|
|
366
|
+
SWARM_ANOMALY_QUESTION_UNANSWERED_MS: () => SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
|
|
367
|
+
SWARM_ANOMALY_QUEUE_STALLED_MS: () => SWARM_ANOMALY_QUEUE_STALLED_MS,
|
|
368
|
+
SWARM_ANOMALY_RENOTIFY_MS: () => SWARM_ANOMALY_RENOTIFY_MS,
|
|
369
|
+
SWARM_AUTO_REVIEW: () => SWARM_AUTO_REVIEW,
|
|
17
370
|
SWARM_BACKEND: () => SWARM_BACKEND,
|
|
18
371
|
SWARM_CLAIM_INTERVAL_MS: () => SWARM_CLAIM_INTERVAL_MS,
|
|
19
372
|
SWARM_HOME: () => SWARM_HOME,
|
|
373
|
+
SWARM_MAX_FD_LOAD_FACTOR: () => SWARM_MAX_FD_LOAD_FACTOR,
|
|
20
374
|
SWARM_MAX_IN_FLIGHT: () => SWARM_MAX_IN_FLIGHT,
|
|
375
|
+
SWARM_MAX_LOAD_FACTOR: () => SWARM_MAX_LOAD_FACTOR,
|
|
376
|
+
SWARM_MIN_FREE_GB: () => SWARM_MIN_FREE_GB,
|
|
21
377
|
SWARM_OUTBOX: () => SWARM_OUTBOX,
|
|
378
|
+
SWARM_PROD_HEARTBEAT_MS: () => SWARM_PROD_HEARTBEAT_MS,
|
|
379
|
+
SWARM_REHYDRATE_MS: () => SWARM_REHYDRATE_MS,
|
|
22
380
|
SWARM_REPO: () => SWARM_REPO,
|
|
381
|
+
SWARM_REVIEW_CAP: () => SWARM_REVIEW_CAP,
|
|
382
|
+
SWARM_SHOT_TIMEOUT_MS: () => SWARM_SHOT_TIMEOUT_MS,
|
|
23
383
|
SWARM_STORM_URL: () => SWARM_STORM_URL,
|
|
384
|
+
SWARM_VERIFY: () => SWARM_VERIFY,
|
|
24
385
|
TASKLESS_API: () => TASKLESS_API,
|
|
386
|
+
TASKLESS_MCP_PATH: () => TASKLESS_MCP_PATH,
|
|
25
387
|
TASKLESS_TASK_URL_BASE: () => TASKLESS_TASK_URL_BASE,
|
|
26
388
|
TASKLESS_TOKEN: () => TASKLESS_TOKEN,
|
|
27
389
|
TASKLESS_WORKSPACE: () => TASKLESS_WORKSPACE,
|
|
@@ -29,11 +391,14 @@ __export(config_exports, {
|
|
|
29
391
|
WEB_PORT: () => WEB_PORT,
|
|
30
392
|
WEB_REDIRECT_URI: () => WEB_REDIRECT_URI,
|
|
31
393
|
WORKTREES_DIR: () => WORKTREES_DIR,
|
|
32
|
-
|
|
394
|
+
droneWorktreePath: () => droneWorktreePath,
|
|
395
|
+
mcpEndpoint: () => mcpEndpoint,
|
|
396
|
+
normalizeRepo: () => normalizeRepo,
|
|
397
|
+
reviewCutoffAt: () => reviewCutoffAt
|
|
33
398
|
});
|
|
34
399
|
import { homedir } from "os";
|
|
35
|
-
import { join, resolve } from "path";
|
|
36
|
-
import { mkdirSync, readFileSync, realpathSync } from "fs";
|
|
400
|
+
import { basename, join, resolve } from "path";
|
|
401
|
+
import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "fs";
|
|
37
402
|
function normalizeRepo(repo) {
|
|
38
403
|
const r = (repo ?? "").trim();
|
|
39
404
|
if (!r) return "";
|
|
@@ -43,12 +408,20 @@ function normalizeRepo(repo) {
|
|
|
43
408
|
return resolve(r);
|
|
44
409
|
}
|
|
45
410
|
}
|
|
411
|
+
function droneWorktreePath(worktreesDir, repo, slug) {
|
|
412
|
+
return join(worktreesDir, basename(normalizeRepo(repo)), slug);
|
|
413
|
+
}
|
|
46
414
|
function envNum(name, def) {
|
|
47
415
|
const raw = process.env[name];
|
|
48
416
|
if (raw === void 0 || raw === "") return def;
|
|
49
417
|
const n = Number(raw);
|
|
50
418
|
return Number.isFinite(n) && n >= 0 ? n : def;
|
|
51
419
|
}
|
|
420
|
+
function mcpEndpoint(api, path = TASKLESS_MCP_PATH) {
|
|
421
|
+
const base2 = api.replace(/\/$/, "");
|
|
422
|
+
const p = path.startsWith("/") ? path : `/${path}`;
|
|
423
|
+
return `${base2}${p}`;
|
|
424
|
+
}
|
|
52
425
|
function tokenFromFile() {
|
|
53
426
|
try {
|
|
54
427
|
return readFileSync(join(SWARM_HOME, "token"), "utf8").trim();
|
|
@@ -56,60 +429,70 @@ function tokenFromFile() {
|
|
|
56
429
|
return "";
|
|
57
430
|
}
|
|
58
431
|
}
|
|
59
|
-
|
|
432
|
+
function reviewCutoffAt() {
|
|
433
|
+
try {
|
|
434
|
+
const raw = JSON.parse(readFileSync(REVIEW_CUTOFF_PATH, "utf8"));
|
|
435
|
+
if (raw.at) return raw.at;
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
const at = stamp();
|
|
439
|
+
try {
|
|
440
|
+
const tmp = `${REVIEW_CUTOFF_PATH}.tmp`;
|
|
441
|
+
writeFileSync(tmp, JSON.stringify({ at }));
|
|
442
|
+
renameSync(tmp, REVIEW_CUTOFF_PATH);
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
return at;
|
|
446
|
+
}
|
|
447
|
+
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_REHYDRATE_MS, SWARM_PROD_HEARTBEAT_MS, HEARTBEAT_PATH, HEARTBEAT_STALE_MS, SWARM_REPO, 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
448
|
var init_config = __esm({
|
|
61
449
|
"src/config.ts"() {
|
|
62
450
|
"use strict";
|
|
451
|
+
init_src();
|
|
63
452
|
SWARM_HOME = process.env.SWARM_HOME || join(homedir(), ".taskless-swarm");
|
|
64
453
|
mkdirSync(SWARM_HOME, { recursive: true });
|
|
65
454
|
DB_PATH = process.env.SWARM_DB || join(SWARM_HOME, "swarm.db");
|
|
66
455
|
WORKTREES_DIR = process.env.SWARM_WORKTREES || join(SWARM_HOME, "worktrees");
|
|
67
|
-
|
|
456
|
+
RUN_LOGS_DIR = process.env.SWARM_RUN_LOGS || join(SWARM_HOME, "run-logs");
|
|
457
|
+
DIST_BUILD = true;
|
|
458
|
+
SWARM_BACKEND = process.env.SWARM_BACKEND === "client" ? "client" : process.env.SWARM_BACKEND === "sqlite" ? "sqlite" : DIST_BUILD ? "client" : "sqlite";
|
|
68
459
|
SWARM_OUTBOX = process.env.SWARM_OUTBOX || join(SWARM_HOME, "outbox.jsonl");
|
|
69
460
|
SWARM_MAX_IN_FLIGHT = Math.trunc(envNum("SWARM_MAX_IN_FLIGHT", 3));
|
|
70
461
|
SWARM_CLAIM_INTERVAL_MS = envNum("SWARM_CLAIM_INTERVAL_MS", 6e4);
|
|
462
|
+
SWARM_MAX_LOAD_FACTOR = envNum("SWARM_MAX_LOAD_FACTOR", 0.9);
|
|
463
|
+
SWARM_MIN_FREE_GB = envNum("SWARM_MIN_FREE_GB", 1.5);
|
|
464
|
+
SWARM_MAX_FD_LOAD_FACTOR = envNum("SWARM_MAX_FD_LOAD_FACTOR", 0.9);
|
|
465
|
+
SWARM_REHYDRATE_MS = envNum("SWARM_REHYDRATE_MS", 15e3);
|
|
466
|
+
SWARM_PROD_HEARTBEAT_MS = envNum("SWARM_PROD_HEARTBEAT_MS", 15e3);
|
|
71
467
|
HEARTBEAT_PATH = process.env.SWARM_HEARTBEAT || join(SWARM_HOME, "overmind.heartbeat");
|
|
72
468
|
HEARTBEAT_STALE_MS = Number(process.env.SWARM_HEARTBEAT_STALE_MS) || 3e4;
|
|
73
469
|
SWARM_REPO = process.env.SWARM_REPO || "";
|
|
74
470
|
TASKLESS_API = process.env.TASKLESS_API || "https://taskless.ru";
|
|
471
|
+
SWARM_SHOT_TIMEOUT_MS = envNum("SWARM_SHOT_TIMEOUT_MS", 2e4);
|
|
472
|
+
TASKLESS_MCP_PATH = process.env.TASKLESS_MCP_PATH || "/api/mcp";
|
|
75
473
|
TASKLESS_TOKEN = process.env.TASKLESS_TOKEN || tokenFromFile();
|
|
76
474
|
TASKLESS_WORKSPACE = process.env.TASKLESS_WORKSPACE || "";
|
|
77
475
|
TASKLESS_WORKSPACE_SLUG = process.env.TASKLESS_WORKSPACE_SLUG || "";
|
|
78
476
|
TASKLESS_TASK_URL_BASE = process.env.TASKLESS_TASK_URL_BASE || (TASKLESS_WORKSPACE_SLUG ? `${TASKLESS_API.replace(/\/$/, "")}/app/${TASKLESS_WORKSPACE_SLUG}` : "");
|
|
477
|
+
SWARM_VERIFY = process.env.SWARM_VERIFY === "ci" ? "ci" : "local";
|
|
478
|
+
SWARM_AUTO_REVIEW = process.env.SWARM_AUTO_REVIEW !== "0";
|
|
479
|
+
REVIEW_CUTOFF_PATH = process.env.SWARM_REVIEW_CUTOFF || join(SWARM_HOME, "review-cutoff.json");
|
|
480
|
+
SWARM_REVIEW_CAP = Math.trunc(envNum("SWARM_REVIEW_CAP", 1));
|
|
79
481
|
SWARM_STORM_URL = process.env.SWARM_STORM_URL || "";
|
|
80
482
|
WEB_PORT = Number(process.env.SWARM_WEB_PORT) || 7420;
|
|
81
483
|
WEB_REDIRECT_URI = process.env.SWARM_REDIRECT_URI || `http://127.0.0.1:${WEB_PORT}/callback`;
|
|
484
|
+
SWARM_ANOMALY_RENOTIFY_MS = envNum("SWARM_ANOMALY_RENOTIFY_MS", 60 * 6e4);
|
|
485
|
+
SWARM_ANOMALY_FAILED_SPIKE_COUNT = Math.trunc(envNum("SWARM_ANOMALY_FAILED_SPIKE_COUNT", 3));
|
|
486
|
+
SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS = envNum("SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS", 30 * 6e4);
|
|
487
|
+
SWARM_ANOMALY_QUEUE_STALLED_MS = envNum("SWARM_ANOMALY_QUEUE_STALLED_MS", 10 * 6e4);
|
|
488
|
+
SWARM_ANOMALY_PR_STUCK_MERGE_MS = envNum("SWARM_ANOMALY_PR_STUCK_MERGE_MS", 30 * 6e4);
|
|
489
|
+
SWARM_ANOMALY_PR_NO_REVIEWER_MS = envNum("SWARM_ANOMALY_PR_NO_REVIEWER_MS", 20 * 6e4);
|
|
490
|
+
SWARM_ANOMALY_QUESTION_UNANSWERED_MS = envNum("SWARM_ANOMALY_QUESTION_UNANSWERED_MS", 20 * 6e4);
|
|
491
|
+
SWARM_ANOMALY_OUTBOX_STUCK_MS = envNum("SWARM_ANOMALY_OUTBOX_STUCK_MS", 10 * 6e4);
|
|
82
492
|
}
|
|
83
493
|
});
|
|
84
494
|
|
|
85
495
|
// 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
496
|
function pickName(rows) {
|
|
114
497
|
const live = /* @__PURE__ */ new Set();
|
|
115
498
|
const lastUse = /* @__PURE__ */ new Map();
|
|
@@ -129,39 +512,12 @@ function pickName(rows) {
|
|
|
129
512
|
}
|
|
130
513
|
}
|
|
131
514
|
}
|
|
132
|
-
var
|
|
133
|
-
var
|
|
515
|
+
var NAME_LIVE;
|
|
516
|
+
var init_names2 = __esm({
|
|
134
517
|
"src/names.ts"() {
|
|
135
518
|
"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"]);
|
|
519
|
+
init_src();
|
|
520
|
+
NAME_LIVE = new Set(ACTIVE_NAME_STATUSES);
|
|
165
521
|
}
|
|
166
522
|
});
|
|
167
523
|
|
|
@@ -169,9 +525,6 @@ var init_names = __esm({
|
|
|
169
525
|
function resolveStormConfig(cfg) {
|
|
170
526
|
return { ...DEFAULT_STORM_CONFIG, ...cfg ?? {} };
|
|
171
527
|
}
|
|
172
|
-
function sqliteMs(ts) {
|
|
173
|
-
return Date.parse(ts.replace(" ", "T") + "Z");
|
|
174
|
-
}
|
|
175
528
|
function encodeProbe(rec) {
|
|
176
529
|
return JSON.stringify(rec);
|
|
177
530
|
}
|
|
@@ -228,6 +581,7 @@ var numEnv, DEFAULT_STORM_CONFIG, STORM_START_TEXT, STORM_END_TEXT, STORM_DEAD_T
|
|
|
228
581
|
var init_storm = __esm({
|
|
229
582
|
"src/storm.ts"() {
|
|
230
583
|
"use strict";
|
|
584
|
+
init_src();
|
|
231
585
|
numEnv = (name, def) => {
|
|
232
586
|
const v = Number(process.env[name]);
|
|
233
587
|
return Number.isFinite(v) && v > 0 ? v : def;
|
|
@@ -295,6 +649,8 @@ function rowToRun(row) {
|
|
|
295
649
|
respawns: row.respawns ?? 0,
|
|
296
650
|
// старые прогоны без колонки → 0
|
|
297
651
|
resolves_run_id: row.resolves_run_id ?? null,
|
|
652
|
+
reviews_run_id: row.reviews_run_id ?? null,
|
|
653
|
+
review_verdict: row.review_verdict ?? null,
|
|
298
654
|
task_title: row.task_title ?? null,
|
|
299
655
|
task_desc: row.task_desc ?? null,
|
|
300
656
|
task_url: row.task_url ?? null,
|
|
@@ -302,13 +658,16 @@ function rowToRun(row) {
|
|
|
302
658
|
updated_at: row.updated_at
|
|
303
659
|
};
|
|
304
660
|
}
|
|
305
|
-
var SCHEMA, SwarmDb;
|
|
661
|
+
var TERMINAL_SQL, RESOLVER_TERMINAL_SQL, SCHEMA, SwarmDb;
|
|
306
662
|
var init_db = __esm({
|
|
307
663
|
"src/db.ts"() {
|
|
308
664
|
"use strict";
|
|
309
665
|
init_config();
|
|
310
|
-
|
|
666
|
+
init_names2();
|
|
311
667
|
init_storm();
|
|
668
|
+
init_src();
|
|
669
|
+
TERMINAL_SQL = TERMINAL_STATUSES.map((s) => `'${s}'`).join(",");
|
|
670
|
+
RESOLVER_TERMINAL_SQL = [...TERMINAL_STATUSES, "stalled"].map((s) => `'${s}'`).join(",");
|
|
312
671
|
SCHEMA = `
|
|
313
672
|
CREATE TABLE IF NOT EXISTS runs (
|
|
314
673
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -331,6 +690,8 @@ CREATE TABLE IF NOT EXISTS runs (
|
|
|
331
690
|
stop_reason TEXT,
|
|
332
691
|
respawns INTEGER NOT NULL DEFAULT 0,
|
|
333
692
|
resolves_run_id INTEGER,
|
|
693
|
+
reviews_run_id INTEGER,
|
|
694
|
+
review_verdict TEXT,
|
|
334
695
|
task_title TEXT,
|
|
335
696
|
task_desc TEXT,
|
|
336
697
|
task_url TEXT,
|
|
@@ -395,7 +756,11 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
395
756
|
// Иерархия роя (TASWM-78): роль прогона (drone|hero) и ссылка на прогон-героя.
|
|
396
757
|
"ALTER TABLE runs ADD COLUMN kind TEXT NOT NULL DEFAULT 'drone'",
|
|
397
758
|
"ALTER TABLE runs ADD COLUMN hero_run_id INTEGER",
|
|
398
|
-
"ALTER TABLE events ADD COLUMN actor TEXT"
|
|
759
|
+
"ALTER TABLE events ADD COLUMN actor TEXT",
|
|
760
|
+
// Авто-ревью (TASWM-148): прогон-ревьюер (reviews_run_id → review-прогон) и
|
|
761
|
+
// вердикт (approve|changes), который ревьюер пишет на проверяемый прогон.
|
|
762
|
+
"ALTER TABLE runs ADD COLUMN reviews_run_id INTEGER",
|
|
763
|
+
"ALTER TABLE runs ADD COLUMN review_verdict TEXT"
|
|
399
764
|
]) {
|
|
400
765
|
try {
|
|
401
766
|
this.db.exec(alter);
|
|
@@ -415,7 +780,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
415
780
|
createRun(input) {
|
|
416
781
|
const name = input.name ?? pickName(this.db.prepare("SELECT id, name, status FROM runs").all());
|
|
417
782
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
783
|
+
"INSERT INTO runs (task_slug, name, kind, hero_run_id, repo, area, depends_on, model, decision, resolves_run_id, reviews_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
|
419
784
|
).run(
|
|
420
785
|
input.task_slug,
|
|
421
786
|
name,
|
|
@@ -429,10 +794,11 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
429
794
|
input.depends_on && input.depends_on.length ? JSON.stringify(input.depends_on) : null,
|
|
430
795
|
input.model ?? null,
|
|
431
796
|
input.decision ? JSON.stringify(input.decision) : null,
|
|
432
|
-
input.resolves_run_id ?? null
|
|
797
|
+
input.resolves_run_id ?? null,
|
|
798
|
+
input.reviews_run_id ?? null
|
|
433
799
|
);
|
|
434
800
|
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}`;
|
|
801
|
+
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
802
|
this.addEvent(id, "spawned", spawnNote);
|
|
437
803
|
return this.getRun(id);
|
|
438
804
|
}
|
|
@@ -485,7 +851,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
485
851
|
*/
|
|
486
852
|
activeResolverFor(reviewRunId) {
|
|
487
853
|
const row = this.db.prepare(
|
|
488
|
-
|
|
854
|
+
`SELECT * FROM runs WHERE resolves_run_id = ? AND status NOT IN (${RESOLVER_TERMINAL_SQL}) ORDER BY id DESC LIMIT 1`
|
|
489
855
|
).get(reviewRunId);
|
|
490
856
|
return row ? rowToRun(row) : null;
|
|
491
857
|
}
|
|
@@ -494,10 +860,21 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
494
860
|
const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE resolves_run_id = ?").get(reviewRunId);
|
|
495
861
|
return row.n;
|
|
496
862
|
}
|
|
863
|
+
/**
|
|
864
|
+
* Сколько ЖИВЫХ/УСПЕШНЫХ ревьюеров уже поднимали на этот review-PR (авто-ревью, TASWM-148) —
|
|
865
|
+
* one-shot идемпотентность БЕЗ лимита попыток (в отличие от резолвера): >0 → повторно не
|
|
866
|
+
* спавним. failed/stalled ревьюеры (worktree/launch сорвались ДО вердикта) в счёт НЕ идут —
|
|
867
|
+
* иначе один транзиентный сбой спавна (диск/git/gh) навсегда блокировал бы ревью этого PR без
|
|
868
|
+
* единого шанса на повтор — тихо нарушая политику владельца «каждый PR ревьюится».
|
|
869
|
+
*/
|
|
870
|
+
reviewerCountFor(reviewRunId) {
|
|
871
|
+
const row = this.db.prepare("SELECT COUNT(*) n FROM runs WHERE reviews_run_id = ? AND status NOT IN ('failed','stalled')").get(reviewRunId);
|
|
872
|
+
return row.n;
|
|
873
|
+
}
|
|
497
874
|
/** Последний живой (не терминальный) прогон по slug — для swarm cancel. */
|
|
498
875
|
activeRunForSlug(slug) {
|
|
499
876
|
const row = this.db.prepare(
|
|
500
|
-
|
|
877
|
+
`SELECT * FROM runs WHERE task_slug = ? AND status NOT IN (${TERMINAL_SQL}) ORDER BY id DESC LIMIT 1`
|
|
501
878
|
).get(slug);
|
|
502
879
|
return row ? rowToRun(row) : null;
|
|
503
880
|
}
|
|
@@ -729,11 +1106,10 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
729
1106
|
taskUrl: row.taskUrl ?? null
|
|
730
1107
|
});
|
|
731
1108
|
}
|
|
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
1109
|
for (const row of this.db.prepare(
|
|
734
1110
|
"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
1111
|
).all()) {
|
|
736
|
-
if (!
|
|
1112
|
+
if (!isMilestoneKind(row.kind)) continue;
|
|
737
1113
|
const global = row.run_id == null;
|
|
738
1114
|
items.push({
|
|
739
1115
|
kind: "milestone",
|
|
@@ -794,12 +1170,13 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
794
1170
|
return row.n;
|
|
795
1171
|
}
|
|
796
1172
|
/**
|
|
797
|
-
* Спавн-фейлов (событий failed на
|
|
798
|
-
* Резолверные
|
|
1173
|
+
* Спавн-фейлов (событий failed на НЕ-резолверных/НЕ-ревьюерных прогонах) с момента
|
|
1174
|
+
* cutoff. Резолверные (merge-main-in) и ревьюерные (авто-ревью, TASWM-148) failed не
|
|
1175
|
+
* считаем штормовым сигналом.
|
|
799
1176
|
*/
|
|
800
1177
|
spawnFailCountSince(cutoffSeconds) {
|
|
801
1178
|
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')"
|
|
1179
|
+
"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
1180
|
).get(cutoffSeconds);
|
|
804
1181
|
return row.n;
|
|
805
1182
|
}
|
|
@@ -823,7 +1200,7 @@ CREATE TABLE IF NOT EXISTS imports (
|
|
|
823
1200
|
|
|
824
1201
|
// src/oauth.ts
|
|
825
1202
|
import { createHash, randomBytes } from "crypto";
|
|
826
|
-
import { readFileSync as
|
|
1203
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync } from "fs";
|
|
827
1204
|
import { join as join4 } from "path";
|
|
828
1205
|
function makePkce() {
|
|
829
1206
|
const verifier = randomBytes(48).toString("base64url");
|
|
@@ -908,13 +1285,13 @@ function toStore(clientId, t, now) {
|
|
|
908
1285
|
}
|
|
909
1286
|
function loadStore() {
|
|
910
1287
|
try {
|
|
911
|
-
return JSON.parse(
|
|
1288
|
+
return JSON.parse(readFileSync6(STORE_PATH, "utf8"));
|
|
912
1289
|
} catch {
|
|
913
1290
|
return null;
|
|
914
1291
|
}
|
|
915
1292
|
}
|
|
916
1293
|
function saveStore(s) {
|
|
917
|
-
|
|
1294
|
+
writeFileSync5(STORE_PATH, JSON.stringify(s, null, 2), { mode: 384 });
|
|
918
1295
|
}
|
|
919
1296
|
function clearStore() {
|
|
920
1297
|
try {
|
|
@@ -924,7 +1301,7 @@ function clearStore() {
|
|
|
924
1301
|
}
|
|
925
1302
|
function loadClientId() {
|
|
926
1303
|
try {
|
|
927
|
-
return JSON.parse(
|
|
1304
|
+
return JSON.parse(readFileSync6(CLIENT_PATH, "utf8")).clientId;
|
|
928
1305
|
} catch {
|
|
929
1306
|
return null;
|
|
930
1307
|
}
|
|
@@ -933,7 +1310,7 @@ async function ensureClientId(api, redirectUri, fetchFn = fetch) {
|
|
|
933
1310
|
const existing = loadClientId();
|
|
934
1311
|
if (existing) return existing;
|
|
935
1312
|
const clientId = await registerClient({ api, redirectUri }, fetchFn);
|
|
936
|
-
|
|
1313
|
+
writeFileSync5(CLIENT_PATH, JSON.stringify({ clientId }, null, 2), { mode: 384 });
|
|
937
1314
|
return clientId;
|
|
938
1315
|
}
|
|
939
1316
|
async function ensureAccess(store, opts = {}) {
|
|
@@ -966,11 +1343,11 @@ var init_oauth = __esm({
|
|
|
966
1343
|
});
|
|
967
1344
|
|
|
968
1345
|
// src/workspace.ts
|
|
969
|
-
import { readFileSync as
|
|
1346
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2 } from "fs";
|
|
970
1347
|
import { join as join5 } from "path";
|
|
971
1348
|
function loadWorkspace() {
|
|
972
1349
|
try {
|
|
973
|
-
const w = JSON.parse(
|
|
1350
|
+
const w = JSON.parse(readFileSync7(WORKSPACE_PATH, "utf8"));
|
|
974
1351
|
return w && typeof w.id === "string" && w.id ? w : null;
|
|
975
1352
|
} catch {
|
|
976
1353
|
return null;
|
|
@@ -1021,6 +1398,7 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
|
|
|
1021
1398
|
description: extractText(task.description),
|
|
1022
1399
|
url: taskUrlBase ? `${taskUrlBase}/tasks/${slug}` : null,
|
|
1023
1400
|
parentId: task.parentId ?? null,
|
|
1401
|
+
projectId: task.projectId ?? null,
|
|
1024
1402
|
statusCategory: task.status?.category ?? null
|
|
1025
1403
|
};
|
|
1026
1404
|
idCache.set(slug, body.id);
|
|
@@ -1058,11 +1436,13 @@ function makeTasklessClient(call, workspaceId, taskUrlBase) {
|
|
|
1058
1436
|
backlogStatusId,
|
|
1059
1437
|
createTask: async (input) => {
|
|
1060
1438
|
const res = await safeCall("create_task", {
|
|
1061
|
-
parentId: input.parentId,
|
|
1439
|
+
...input.parentId ? { parentId: input.parentId } : {},
|
|
1062
1440
|
title: input.title,
|
|
1063
1441
|
description: input.description,
|
|
1064
|
-
estimateHours: input.estimateHours,
|
|
1065
|
-
statusId: input.statusId,
|
|
1442
|
+
...input.estimateHours != null ? { estimateHours: input.estimateHours } : {},
|
|
1443
|
+
...input.statusId ? { statusId: input.statusId } : {},
|
|
1444
|
+
// TASWM-138: без projectId подзадача — сирота, swarm_claim_work её не возьмёт.
|
|
1445
|
+
...input.projectId ? { projectId: input.projectId } : {},
|
|
1066
1446
|
force: true
|
|
1067
1447
|
});
|
|
1068
1448
|
const task = res?.structuredContent?.task ?? null;
|
|
@@ -1100,7 +1480,7 @@ async function connectTaskless(cfg) {
|
|
|
1100
1480
|
}
|
|
1101
1481
|
if (!token) token = TASKLESS_TOKEN;
|
|
1102
1482
|
if (!token) return null;
|
|
1103
|
-
const url = new URL(
|
|
1483
|
+
const url = new URL(mcpEndpoint(api));
|
|
1104
1484
|
const transport = new StreamableHTTPClientTransport(url, {
|
|
1105
1485
|
requestInit: { headers: { Authorization: `Bearer ${token}` } }
|
|
1106
1486
|
});
|
|
@@ -1131,7 +1511,7 @@ async function connectSwarmMcp(cfg) {
|
|
|
1131
1511
|
}
|
|
1132
1512
|
if (!token) token = TASKLESS_TOKEN;
|
|
1133
1513
|
if (!token) return null;
|
|
1134
|
-
const url = new URL(
|
|
1514
|
+
const url = new URL(mcpEndpoint(api));
|
|
1135
1515
|
const transport = new StreamableHTTPClientTransport(url, {
|
|
1136
1516
|
requestInit: { headers: { Authorization: `Bearer ${token}` } }
|
|
1137
1517
|
});
|
|
@@ -1144,22 +1524,11 @@ async function connectSwarmMcp(cfg) {
|
|
|
1144
1524
|
}
|
|
1145
1525
|
return async (name, args) => await client.callTool({ name, arguments: { ...args, ...ws } });
|
|
1146
1526
|
}
|
|
1147
|
-
function extractText(d) {
|
|
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
1527
|
var SLUG_RE;
|
|
1160
1528
|
var init_taskless = __esm({
|
|
1161
1529
|
"src/taskless.ts"() {
|
|
1162
1530
|
"use strict";
|
|
1531
|
+
init_src();
|
|
1163
1532
|
init_config();
|
|
1164
1533
|
init_oauth();
|
|
1165
1534
|
init_workspace();
|
|
@@ -1169,41 +1538,117 @@ var init_taskless = __esm({
|
|
|
1169
1538
|
|
|
1170
1539
|
// src/overmind.ts
|
|
1171
1540
|
init_db();
|
|
1172
|
-
import { existsSync as
|
|
1541
|
+
import { existsSync as existsSync4, readFileSync as readFileSync8 } from "fs";
|
|
1173
1542
|
import { basename as basename3, dirname as dirname2, join as join7 } from "path";
|
|
1174
1543
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
1544
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1175
1545
|
|
|
1176
1546
|
// src/swarmClient.ts
|
|
1177
1547
|
init_config();
|
|
1178
|
-
|
|
1179
|
-
init_storm();
|
|
1180
|
-
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
1548
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1181
1549
|
import { dirname } from "path";
|
|
1182
1550
|
import { randomUUID } from "crypto";
|
|
1183
|
-
|
|
1184
|
-
|
|
1551
|
+
|
|
1552
|
+
// src/resources.ts
|
|
1553
|
+
init_config();
|
|
1554
|
+
import os from "os";
|
|
1555
|
+
import { execFileSync } from "child_process";
|
|
1556
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1557
|
+
function isTransientResourceError(e) {
|
|
1558
|
+
const code = e?.code;
|
|
1559
|
+
return code === "ENFILE" || code === "EMFILE" || code === "EAGAIN";
|
|
1560
|
+
}
|
|
1561
|
+
function logTransientSpawnError(label, e) {
|
|
1562
|
+
if (isTransientResourceError(e)) {
|
|
1563
|
+
const code = e.code;
|
|
1564
|
+
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`);
|
|
1565
|
+
} else {
|
|
1566
|
+
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)}`);
|
|
1567
|
+
}
|
|
1185
1568
|
}
|
|
1186
|
-
function
|
|
1187
|
-
|
|
1569
|
+
function resourcesOk(load1, cpus, freeGb, maxLoadFactor = SWARM_MAX_LOAD_FACTOR, minFreeGb = SWARM_MIN_FREE_GB, fdLoadRatio = null, maxFdLoadFactor = SWARM_MAX_FD_LOAD_FACTOR) {
|
|
1570
|
+
const cores = cpus > 0 ? cpus : 1;
|
|
1571
|
+
const fdsOk = fdLoadRatio == null || fdLoadRatio <= maxFdLoadFactor;
|
|
1572
|
+
return load1 <= cores * maxLoadFactor && freeGb >= minFreeGb && fdsOk;
|
|
1573
|
+
}
|
|
1574
|
+
function parseVmStatAvailableBytes(vmStatOut) {
|
|
1575
|
+
const page = Number(vmStatOut.match(/page size of (\d+) bytes/)?.[1]) || 4096;
|
|
1576
|
+
const pages = (label) => {
|
|
1577
|
+
const m = vmStatOut.match(new RegExp(`Pages ${label}:\\s+(\\d+)`));
|
|
1578
|
+
return m ? Number(m[1]) : 0;
|
|
1579
|
+
};
|
|
1580
|
+
return (pages("free") + pages("inactive") + pages("speculative") + pages("purgeable")) * page;
|
|
1581
|
+
}
|
|
1582
|
+
function availableMemBytes() {
|
|
1583
|
+
if (process.platform === "darwin") {
|
|
1584
|
+
try {
|
|
1585
|
+
const bytes = parseVmStatAvailableBytes(execFileSync("vm_stat", { encoding: "utf8" }));
|
|
1586
|
+
if (bytes > 0) return bytes;
|
|
1587
|
+
} catch (e) {
|
|
1588
|
+
logTransientSpawnError("vm_stat", e);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
return os.freemem();
|
|
1592
|
+
}
|
|
1593
|
+
function parseSysctlInt(out) {
|
|
1594
|
+
const n = Number(out.trim());
|
|
1595
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
1596
|
+
}
|
|
1597
|
+
function readSysctlPair(nameA, nameB) {
|
|
1598
|
+
try {
|
|
1599
|
+
const lines = execFileSync("sysctl", ["-n", nameA, nameB], { encoding: "utf8" }).trim().split("\n");
|
|
1600
|
+
return [parseSysctlInt(lines[0] ?? ""), parseSysctlInt(lines[1] ?? "")];
|
|
1601
|
+
} catch (e) {
|
|
1602
|
+
logTransientSpawnError(`sysctl ${nameA}/${nameB}`, e);
|
|
1603
|
+
return [null, null];
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function fileDescriptorLoadRatio() {
|
|
1607
|
+
if (process.platform === "darwin") {
|
|
1608
|
+
const [max, used] = readSysctlPair("kern.maxfiles", "kern.num_files");
|
|
1609
|
+
return max != null && used != null ? used / max : null;
|
|
1610
|
+
}
|
|
1611
|
+
if (process.platform === "linux") {
|
|
1612
|
+
try {
|
|
1613
|
+
const [allocated, , max] = readFileSync2("/proc/sys/fs/file-nr", "utf8").trim().split(/\s+/).map(Number);
|
|
1614
|
+
if (Number.isFinite(allocated ?? NaN) && Number.isFinite(max ?? NaN) && max) return allocated / max;
|
|
1615
|
+
} catch {
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return null;
|
|
1619
|
+
}
|
|
1620
|
+
var RESOURCES_CACHE_MS = 250;
|
|
1621
|
+
var resourcesCache = null;
|
|
1622
|
+
function hostResourcesOk() {
|
|
1623
|
+
const now = Date.now();
|
|
1624
|
+
if (resourcesCache && now - resourcesCache.at < RESOURCES_CACHE_MS) return resourcesCache.ok;
|
|
1625
|
+
const ok = resourcesOk(os.loadavg()[0] ?? 0, os.cpus().length, availableMemBytes() / 1e9, void 0, void 0, fileDescriptorLoadRatio());
|
|
1626
|
+
resourcesCache = { at: now, ok };
|
|
1627
|
+
return ok;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/swarmClient.ts
|
|
1631
|
+
init_names2();
|
|
1632
|
+
init_storm();
|
|
1633
|
+
init_src();
|
|
1634
|
+
var RESOLVER_TERMINAL = /* @__PURE__ */ new Set([...TERMINAL_STATUSES, "stalled"]);
|
|
1635
|
+
var SLUG_TERMINAL = new Set(TERMINAL_STATUSES);
|
|
1636
|
+
function runRef(runKey) {
|
|
1637
|
+
return { __runRef: runKey };
|
|
1638
|
+
}
|
|
1639
|
+
function isRunRef(v) {
|
|
1640
|
+
return typeof v === "object" && v !== null && "__runRef" in v;
|
|
1641
|
+
}
|
|
1642
|
+
function jsonFromTextContent(res) {
|
|
1643
|
+
const text = res?.content?.find((c) => c.type === "text")?.text;
|
|
1644
|
+
if (text) {
|
|
1645
|
+
try {
|
|
1646
|
+
return JSON.parse(text);
|
|
1647
|
+
} catch {
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
return res?.structuredContent ?? null;
|
|
1188
1651
|
}
|
|
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
1652
|
var SwarmClient = class _SwarmClient {
|
|
1208
1653
|
call;
|
|
1209
1654
|
defaultRepo;
|
|
@@ -1227,6 +1672,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
1227
1672
|
// Outbox / flush.
|
|
1228
1673
|
outboxPath;
|
|
1229
1674
|
mapPath;
|
|
1675
|
+
stormPath;
|
|
1676
|
+
// durable-сайдкар глобальных (штормовых) событий
|
|
1230
1677
|
pending = [];
|
|
1231
1678
|
nextSeq = 1;
|
|
1232
1679
|
flushing = false;
|
|
@@ -1241,6 +1688,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
1241
1688
|
this.listLimit = opts.listLimit ?? 200;
|
|
1242
1689
|
this.outboxPath = opts.outboxPath;
|
|
1243
1690
|
this.mapPath = `${opts.outboxPath}.prodmap.json`;
|
|
1691
|
+
this.stormPath = `${opts.outboxPath}.storm.jsonl`;
|
|
1244
1692
|
mkdirSync2(dirname(this.outboxPath), { recursive: true });
|
|
1245
1693
|
this.recover();
|
|
1246
1694
|
}
|
|
@@ -1248,13 +1696,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
1248
1696
|
recover() {
|
|
1249
1697
|
if (existsSync2(this.mapPath)) {
|
|
1250
1698
|
try {
|
|
1251
|
-
const obj = JSON.parse(
|
|
1699
|
+
const obj = JSON.parse(readFileSync3(this.mapPath, "utf8"));
|
|
1252
1700
|
for (const [k, v] of Object.entries(obj)) this.prodIdByKey.set(k, v);
|
|
1253
1701
|
} catch {
|
|
1254
1702
|
}
|
|
1255
1703
|
}
|
|
1256
1704
|
if (existsSync2(this.outboxPath)) {
|
|
1257
|
-
const txt =
|
|
1705
|
+
const txt = readFileSync3(this.outboxPath, "utf8");
|
|
1258
1706
|
for (const line of txt.split("\n")) {
|
|
1259
1707
|
const s = line.trim();
|
|
1260
1708
|
if (!s) continue;
|
|
@@ -1266,6 +1714,42 @@ var SwarmClient = class _SwarmClient {
|
|
|
1266
1714
|
this.nextSeq = this.pending.reduce((m, e) => Math.max(m, e.seq), 0) + 1;
|
|
1267
1715
|
if (this.autoFlush && this.pending.length) this.scheduleFlush();
|
|
1268
1716
|
}
|
|
1717
|
+
this.replayGlobalEvents();
|
|
1718
|
+
}
|
|
1719
|
+
// Реплей глобальных (штормовых) событий из сайдкара в зеркало на старте. Идут в
|
|
1720
|
+
// eventsLog ПЕРВЫМИ (до hydrate), в порядке файла (= хронологический); id
|
|
1721
|
+
// переприсваиваем локально (id событий — только порядок, не корреляция с продом).
|
|
1722
|
+
replayGlobalEvents() {
|
|
1723
|
+
if (!existsSync2(this.stormPath)) return;
|
|
1724
|
+
let txt;
|
|
1725
|
+
try {
|
|
1726
|
+
txt = readFileSync3(this.stormPath, "utf8");
|
|
1727
|
+
} catch {
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
for (const line of txt.split("\n")) {
|
|
1731
|
+
const s = line.trim();
|
|
1732
|
+
if (!s) continue;
|
|
1733
|
+
try {
|
|
1734
|
+
const g = JSON.parse(s);
|
|
1735
|
+
this.eventsLog.push({
|
|
1736
|
+
id: this.nextEventId++,
|
|
1737
|
+
run_id: null,
|
|
1738
|
+
kind: g.kind,
|
|
1739
|
+
text: g.text ?? null,
|
|
1740
|
+
actor: g.actor ?? null,
|
|
1741
|
+
at: g.at
|
|
1742
|
+
});
|
|
1743
|
+
} catch {
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
// Дописать глобальное событие в durable-сайдкар (append-only, синхронно — как outbox).
|
|
1748
|
+
persistGlobalEvent(ev) {
|
|
1749
|
+
try {
|
|
1750
|
+
appendFileSync(this.stormPath, JSON.stringify(ev) + "\n");
|
|
1751
|
+
} catch {
|
|
1752
|
+
}
|
|
1269
1753
|
}
|
|
1270
1754
|
// ── зеркало: наложить рантайм на durable-прогон ──────────────────────────────
|
|
1271
1755
|
mat(d) {
|
|
@@ -1276,14 +1760,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
1276
1760
|
runKeyFor(localId) {
|
|
1277
1761
|
return this.keyByLocal.get(localId);
|
|
1278
1762
|
}
|
|
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>`).
|
|
1763
|
+
// cuid прода → локальный id (по runKey гидрированного прогона `p:<cuid>`).
|
|
1287
1764
|
// Для маппинга прод heroId → локальный hero_run_id при гидрации. Резолвится, когда
|
|
1288
1765
|
// прогон-герой присутствует в зеркале; иначе null (уточнится следующим рефрешем).
|
|
1289
1766
|
localIdForProdId(prodId) {
|
|
@@ -1300,9 +1777,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
1300
1777
|
this.scheduleFlush();
|
|
1301
1778
|
}
|
|
1302
1779
|
// Прод-патч прогона: локальные поля Run → поля swarm_run_upsert (см. TASWM-74).
|
|
1303
|
-
// pid/worktree_path (рантайм),
|
|
1304
|
-
//
|
|
1305
|
-
//
|
|
1780
|
+
// pid/worktree_path (рантайм), respawns (нет в схеме тула) и снимок задачи (task_*,
|
|
1781
|
+
// прод берёт join'ом из Task) в прод НЕ уходят. kind → role (плоский enum). Self-ref
|
|
1782
|
+
// прогонов hero_run_id/resolves_run_id/superseded_by → heroId/resolvesRunId/
|
|
1783
|
+
// supersededById как ОТЛОЖЕННЫЕ ссылки (RunRef) — cuid резолвится на флаше.
|
|
1306
1784
|
runFields(patch) {
|
|
1307
1785
|
const out = {};
|
|
1308
1786
|
if ("status" in patch && patch.status !== void 0) out.status = patch.status;
|
|
@@ -1317,15 +1795,26 @@ var SwarmClient = class _SwarmClient {
|
|
|
1317
1795
|
if ("depends_on" in patch && patch.depends_on) out.dependsOn = patch.depends_on;
|
|
1318
1796
|
if ("decision" in patch) out.decision = patch.decision ? JSON.stringify(patch.decision) : null;
|
|
1319
1797
|
if ("kind" in patch && patch.kind !== void 0) out.role = patch.kind;
|
|
1320
|
-
if ("
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1798
|
+
if ("review_verdict" in patch) out.reviewVerdict = patch.review_verdict ?? null;
|
|
1799
|
+
this.setRunRefField(out, "heroId", patch, "hero_run_id");
|
|
1800
|
+
this.setRunRefField(out, "resolvesRunId", patch, "resolves_run_id");
|
|
1801
|
+
this.setRunRefField(out, "supersededById", patch, "superseded_by");
|
|
1802
|
+
this.setRunRefField(out, "reviewsRunId", patch, "reviews_run_id");
|
|
1327
1803
|
return out;
|
|
1328
1804
|
}
|
|
1805
|
+
// Положить self-ref прогона в прод-патч: локальный number → отложенная ссылка
|
|
1806
|
+
// {__runRef: runKey} (резолв в cuid на флаше, resolveArgs/materializeRefs). Явный
|
|
1807
|
+
// null → очистка поля. Локальный id без runKey (прогон не в зеркале) — пропускаем.
|
|
1808
|
+
setRunRefField(out, prodKey, patch, localKey) {
|
|
1809
|
+
if (!(localKey in patch)) return;
|
|
1810
|
+
const localId = patch[localKey];
|
|
1811
|
+
if (localId == null) {
|
|
1812
|
+
out[prodKey] = null;
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
const key = this.keyByLocal.get(localId);
|
|
1816
|
+
if (key) out[prodKey] = runRef(key);
|
|
1817
|
+
}
|
|
1329
1818
|
enqueueRun(run, fields) {
|
|
1330
1819
|
if (Object.keys(fields).length === 0) return;
|
|
1331
1820
|
this.enqueue({ t: "run", runKey: run.runKey, taskRef: run.task_slug, fields });
|
|
@@ -1358,6 +1847,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
1358
1847
|
stop_reason: null,
|
|
1359
1848
|
respawns: 0,
|
|
1360
1849
|
resolves_run_id: input.resolves_run_id ?? null,
|
|
1850
|
+
reviews_run_id: input.reviews_run_id ?? null,
|
|
1851
|
+
review_verdict: null,
|
|
1361
1852
|
task_title: null,
|
|
1362
1853
|
task_desc: null,
|
|
1363
1854
|
task_url: null,
|
|
@@ -1375,9 +1866,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
1375
1866
|
depends_on: d.depends_on,
|
|
1376
1867
|
decision: d.decision,
|
|
1377
1868
|
kind: d.kind,
|
|
1378
|
-
hero_run_id: d.hero_run_id
|
|
1869
|
+
hero_run_id: d.hero_run_id,
|
|
1870
|
+
resolves_run_id: d.resolves_run_id,
|
|
1871
|
+
// резолвер конфликта → связь на review-прогон
|
|
1872
|
+
reviews_run_id: d.reviews_run_id
|
|
1873
|
+
// ревьюер (авто-ревью, TASWM-148) → связь на review-прогон
|
|
1379
1874
|
}));
|
|
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}`;
|
|
1875
|
+
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
1876
|
this.addEvent(id, "spawned", spawnNote);
|
|
1382
1877
|
return this.mat(d);
|
|
1383
1878
|
}
|
|
@@ -1431,6 +1926,12 @@ var SwarmClient = class _SwarmClient {
|
|
|
1431
1926
|
resolverCountFor(reviewRunId) {
|
|
1432
1927
|
return [...this.runs.values()].filter((r) => r.resolves_run_id === reviewRunId).length;
|
|
1433
1928
|
}
|
|
1929
|
+
// failed/stalled ревьюеры (сорвался спавн ДО вердикта) в счёт НЕ идут — см. db.ts:reviewerCountFor.
|
|
1930
|
+
reviewerCountFor(reviewRunId) {
|
|
1931
|
+
return [...this.runs.values()].filter(
|
|
1932
|
+
(r) => r.reviews_run_id === reviewRunId && r.status !== "failed" && r.status !== "stalled"
|
|
1933
|
+
).length;
|
|
1934
|
+
}
|
|
1434
1935
|
activeRunForSlug(slug) {
|
|
1435
1936
|
const cands = [...this.runs.values()].filter((r) => r.task_slug === slug && !SLUG_TERMINAL.has(r.status)).sort((a, b) => b.id - a.id);
|
|
1436
1937
|
return cands.length ? this.mat(cands[0]) : null;
|
|
@@ -1528,7 +2029,10 @@ var SwarmClient = class _SwarmClient {
|
|
|
1528
2029
|
addEvent(run_id, kind, text, actor = null) {
|
|
1529
2030
|
const ev = { id: this.nextEventId++, run_id, kind, text: text ?? null, actor: actor ?? null, at: stamp() };
|
|
1530
2031
|
this.eventsLog.push(ev);
|
|
1531
|
-
if (run_id == null)
|
|
2032
|
+
if (run_id == null) {
|
|
2033
|
+
this.persistGlobalEvent({ kind, text: ev.text, actor: ev.actor, at: ev.at });
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
1532
2036
|
const key = this.runKeyFor(run_id);
|
|
1533
2037
|
if (key) this.enqueue({ t: "event", runKey: key, kind, text: text ?? null, actor: actor ?? null });
|
|
1534
2038
|
}
|
|
@@ -1561,7 +2065,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
1561
2065
|
});
|
|
1562
2066
|
}
|
|
1563
2067
|
for (const e of this.eventsLog) {
|
|
1564
|
-
if (!
|
|
2068
|
+
if (!isMilestoneKind(e.kind)) continue;
|
|
1565
2069
|
const global = e.run_id == null;
|
|
1566
2070
|
const r = runById(e.run_id);
|
|
1567
2071
|
items.push({
|
|
@@ -1618,7 +2122,7 @@ var SwarmClient = class _SwarmClient {
|
|
|
1618
2122
|
return this.eventsLog.filter((e) => {
|
|
1619
2123
|
if (e.kind !== "failed" || e.run_id == null || e.at < cutoff) return false;
|
|
1620
2124
|
const r = this.runs.get(e.run_id);
|
|
1621
|
-
return !!r && r.resolves_run_id == null;
|
|
2125
|
+
return !!r && r.resolves_run_id == null && r.reviews_run_id == null;
|
|
1622
2126
|
}).length;
|
|
1623
2127
|
}
|
|
1624
2128
|
recordStormProbe(probe, ext) {
|
|
@@ -1642,6 +2146,100 @@ var SwarmClient = class _SwarmClient {
|
|
|
1642
2146
|
if (res?.isError) return;
|
|
1643
2147
|
const runs = res.structuredContent?.runs ?? [];
|
|
1644
2148
|
for (const pr of runs) this.hydrateRun(pr);
|
|
2149
|
+
for (const pr of runs) this.linkRefs(pr);
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Реконсайл рантайма после рестарта демона (client-режим). pid/worktree_path живут в
|
|
2153
|
+
* ОТДЕЛЬНОЙ Map (машинное — в прод не уходят) и рестарт демона НЕ переживают: гидрация
|
|
2154
|
+
* из прода приносит running/blocked-прогоны с pid=null. Но демон и дроны — РАЗНЫЕ
|
|
2155
|
+
* detached-процессы (launcher: detached+unref), поэтому рестарт демона дронов НЕ убивает
|
|
2156
|
+
* — часть их ещё жива. Сопоставляем каждый running/blocked-прогон зеркала с живым
|
|
2157
|
+
* процессом (ps, injected listProcesses) по детерминированному worktree
|
|
2158
|
+
* (<worktreesDir>/<repo-basename>/<slug>):
|
|
2159
|
+
* • нашёлся живой процесс → восстанавливаем pid+worktree_path (дрон жив — overmind его
|
|
2160
|
+
* НЕ похоронит и не заспавнит второго поверх);
|
|
2161
|
+
* • не нашёлся → дрон не пережил рестарт: восстанавливаем worktree_path, если каталог
|
|
2162
|
+
* цел (для respawn --continue), pid оставляем null. overmind (tickOnce шаг 3) ловит
|
|
2163
|
+
* running/blocked с pid=null как смерть и оживляет/усыновляет/стопорит по общей ветке.
|
|
2164
|
+
* Рантайм пишем ПРЯМО в runtime-Map (в обход updateRun) — чисто локально, без outbox и
|
|
2165
|
+
* без прод-раунд-трипа. Идемпотентно; вызывается один раз на старте после hydrate().
|
|
2166
|
+
* Возвращает счётчики (для лога демона) + reconciled (TASWM-145): false → listProcesses()
|
|
2167
|
+
* бросил (ENFILE и т.п.), реконсайл этого запуска ПРОПУЩЕН целиком (не «confirmed пусто» —
|
|
2168
|
+
* см. коммент в catch ниже). Вызывающий (runDaemon) ОБЯЗАН пропустить startup-sync
|
|
2169
|
+
* (syncCrashedOnStartup) при reconciled=false: та трактует pid==null как «не пережил
|
|
2170
|
+
* рестарт» и безусловно хоронит (stalled/done) — при пропущенном реконсайле это massively
|
|
2171
|
+
* похоронило бы ВСЕХ running/blocked, включая реально живых detached-дронов, тем же
|
|
2172
|
+
* транзиентным сбоем ОС, который этот PR должен переживать, а не превращать в мор.
|
|
2173
|
+
*/
|
|
2174
|
+
reconcileRuntime(opts) {
|
|
2175
|
+
const wtExists = opts.worktreeExists ?? existsSync2;
|
|
2176
|
+
const pidByWorktree = /* @__PURE__ */ new Map();
|
|
2177
|
+
try {
|
|
2178
|
+
for (const p of opts.listProcesses()) if (p.worktree) pidByWorktree.set(p.worktree, p.pid);
|
|
2179
|
+
} catch (e) {
|
|
2180
|
+
logTransientSpawnError("ps (reconcileRuntime)", e);
|
|
2181
|
+
return { alive: 0, dead: 0, reconciled: false };
|
|
2182
|
+
}
|
|
2183
|
+
let alive = 0;
|
|
2184
|
+
let dead = 0;
|
|
2185
|
+
for (const d of this.runs.values()) {
|
|
2186
|
+
if (d.status !== "running" && d.status !== "blocked") continue;
|
|
2187
|
+
if (this.runtime.get(d.id)?.pid != null) continue;
|
|
2188
|
+
const workerWt = droneWorktreePath(opts.worktreesDir, d.repo || this.defaultRepo, d.task_slug);
|
|
2189
|
+
let matchWt = null;
|
|
2190
|
+
let matchPid = null;
|
|
2191
|
+
if (d.resolves_run_id == null && d.reviews_run_id == null) {
|
|
2192
|
+
const p = pidByWorktree.get(workerWt);
|
|
2193
|
+
if (p != null) {
|
|
2194
|
+
matchWt = workerWt;
|
|
2195
|
+
matchPid = p;
|
|
2196
|
+
}
|
|
2197
|
+
} else if (d.resolves_run_id != null) {
|
|
2198
|
+
const prefix = `${workerWt}-resolve-`;
|
|
2199
|
+
for (const [wt, p] of pidByWorktree) {
|
|
2200
|
+
if (wt.startsWith(prefix)) {
|
|
2201
|
+
matchWt = wt;
|
|
2202
|
+
matchPid = p;
|
|
2203
|
+
break;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
} else {
|
|
2207
|
+
const reviewed = this.runs.get(d.reviews_run_id);
|
|
2208
|
+
const reviewedWt = reviewed ? droneWorktreePath(opts.worktreesDir, reviewed.repo || this.defaultRepo, reviewed.task_slug) : workerWt;
|
|
2209
|
+
const prefix = `${reviewedWt}-review-`;
|
|
2210
|
+
for (const [wt, p] of pidByWorktree) {
|
|
2211
|
+
if (wt.startsWith(prefix)) {
|
|
2212
|
+
matchWt = wt;
|
|
2213
|
+
matchPid = p;
|
|
2214
|
+
break;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
if (matchPid != null && matchWt) {
|
|
2219
|
+
this.runtime.set(d.id, { pid: matchPid, worktree_path: matchWt });
|
|
2220
|
+
alive++;
|
|
2221
|
+
} else {
|
|
2222
|
+
const wt = d.resolves_run_id == null && d.reviews_run_id == null && wtExists(workerWt) ? workerWt : null;
|
|
2223
|
+
this.runtime.set(d.id, { pid: null, worktree_path: wt });
|
|
2224
|
+
dead++;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
return { alive, dead, reconciled: true };
|
|
2228
|
+
}
|
|
2229
|
+
// Долинковать self-ref прогона по прод-cuid → локальному id (после того как все
|
|
2230
|
+
// прогоны гидрированы). Идемпотентно: ставит найденное, иначе не трогает.
|
|
2231
|
+
linkRefs(pr) {
|
|
2232
|
+
const localId = this.localIdForProdId(String(pr.id ?? ""));
|
|
2233
|
+
const local = localId === void 0 ? void 0 : this.runs.get(localId);
|
|
2234
|
+
if (!local) return;
|
|
2235
|
+
const hero = pr.heroId ?? null;
|
|
2236
|
+
if (hero) local.hero_run_id = this.localIdForProdId(hero) ?? local.hero_run_id;
|
|
2237
|
+
const res = pr.resolvesRunId ?? null;
|
|
2238
|
+
if (res) local.resolves_run_id = this.localIdForProdId(res) ?? local.resolves_run_id;
|
|
2239
|
+
const sup = pr.supersededById ?? null;
|
|
2240
|
+
if (sup) local.superseded_by = this.localIdForProdId(sup) ?? local.superseded_by;
|
|
2241
|
+
const rev = pr.reviewsRunId ?? null;
|
|
2242
|
+
if (rev) local.reviews_run_id = this.localIdForProdId(rev) ?? local.reviews_run_id;
|
|
1645
2243
|
}
|
|
1646
2244
|
/**
|
|
1647
2245
|
* Авто-интейк (TASWM-100): взять следующую claimable-задачу владельца токена через
|
|
@@ -1668,12 +2266,76 @@ var SwarmClient = class _SwarmClient {
|
|
|
1668
2266
|
const local = this.hydrateRun(sc.run);
|
|
1669
2267
|
return local ? this.mat(local) : null;
|
|
1670
2268
|
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Живой потолок дронов владельца из Taskless (TASWM-126): читает who_am_i.maxDrones
|
|
2271
|
+
* (личная настройка uiPrefs.swarm.maxDrones, дефолт 3). overmind капит им ВСЕ спавны —
|
|
2272
|
+
* очередь (шаг 1), резолвер (шаг 2), авто-claim (шаг 5) — чтобы активных дронов не было
|
|
2273
|
+
* больше числа из UI (инцидент 15.07: 4-6 дронов разом уронили ноут). who_am_i отдаёт
|
|
2274
|
+
* плоский JSON в text-контенте (не {display,data}). Прод недоступен / нет валидного числа
|
|
2275
|
+
* → null: ридер (makeMaxDronesReader) берёт env-фолбэк.
|
|
2276
|
+
*/
|
|
2277
|
+
async ownerMaxDrones() {
|
|
2278
|
+
let res;
|
|
2279
|
+
try {
|
|
2280
|
+
res = await this.call("who_am_i", {});
|
|
2281
|
+
} catch {
|
|
2282
|
+
return null;
|
|
2283
|
+
}
|
|
2284
|
+
if (res?.isError) return null;
|
|
2285
|
+
const obj = jsonFromTextContent(res);
|
|
2286
|
+
const n = obj?.maxDrones;
|
|
2287
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Пульс overmind в прод (TASWM-129): swarm_heartbeat обновляет User.swarmHeartbeatAt —
|
|
2291
|
+
* дашборд /swarm сравнивает свежесть со своим порогом устаревания («нет связи с
|
|
2292
|
+
* овермайндом» вместо устаревшего «живого» роя). Best-effort, как ownerMaxDrones/claim:
|
|
2293
|
+
* прод недоступен/тул отверг → просто не обновили в этом тике, следующий ретраит —
|
|
2294
|
+
* не роняем тик демона из-за упавшего пульса.
|
|
2295
|
+
*/
|
|
2296
|
+
async heartbeat() {
|
|
2297
|
+
try {
|
|
2298
|
+
await this.call("swarm_heartbeat", {});
|
|
2299
|
+
} catch {
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Встроенный надзор (TASWM-194): нотификация аномалии в инбокс владельца — прод-MCP
|
|
2304
|
+
* `swarm_notify`, ПРЯМОЙ вызов (НЕ outbox): аномалия — «сообщи СЕЙЧАС», не durable-факт,
|
|
2305
|
+
* который обязан пережить рестарт демона в неизменном порядке (как run-апдейты/события) —
|
|
2306
|
+
* задержка на цикл outbox-флаша (обычно доли секунды, но в худшем случае — до
|
|
2307
|
+
* восстановления сети) тут не оправдана.
|
|
2308
|
+
* runId (локальный numeric) резолвим в прод-cuid по той же карте, что flush аутбокса
|
|
2309
|
+
* (runKeyFor→prodIdByKey); прогон ещё не успел долететь до прода (cuid неизвестен) →
|
|
2310
|
+
* шлём без runId (нотификация всё равно уйдёт, просто без SwarmEvent-вехи на прогоне).
|
|
2311
|
+
*
|
|
2312
|
+
* БРОСАЕТ при сбое (транспорт ИЛИ isError:true от тула) — НЕ глотает, в отличие от
|
|
2313
|
+
* heartbeat() выше (code-review TASWM-194): вызывающий (overmind.ts tickOnce) коммитит
|
|
2314
|
+
* AnomalyTracker ТОЛЬКО после успешного return — молча проглоченная ошибка означала бы,
|
|
2315
|
+
* что трекер считает недоставленную нотификацию отправленной и молчит renotifyCooldownMs
|
|
2316
|
+
* (по умолчанию час) без единой реальной попытки. `res?.isError` — тот же паттерн, что
|
|
2317
|
+
* ownerMaxDrones() чуть выше: registerTool ловит throw внутри run() и превращает его в
|
|
2318
|
+
* `{isError:true}`, а не в отклонённый промис — без этой проверки схемная/бизнес-ошибка
|
|
2319
|
+
* тула (напр. TASWM-194 review: body > zod max) молча резолвилась бы успехом.
|
|
2320
|
+
*/
|
|
2321
|
+
async notifyAnomaly(signal) {
|
|
2322
|
+
const prodRunId = signal.runId != null ? this.prodIdByKey.get(this.runKeyFor(signal.runId) ?? "") : void 0;
|
|
2323
|
+
const res = await this.call("swarm_notify", {
|
|
2324
|
+
tag: signal.key,
|
|
2325
|
+
title: signal.title,
|
|
2326
|
+
body: signal.body,
|
|
2327
|
+
...signal.href ? { href: signal.href } : {},
|
|
2328
|
+
...prodRunId ? { runId: prodRunId } : {}
|
|
2329
|
+
});
|
|
2330
|
+
if (res?.isError) throw new Error(`swarm_notify \u043E\u0442\u0432\u0435\u0440\u0433 \xAB${signal.key}\xBB`);
|
|
2331
|
+
}
|
|
1671
2332
|
hydrateRun(pr) {
|
|
1672
2333
|
const prodId = String(pr.id ?? "");
|
|
1673
2334
|
if (!prodId) return null;
|
|
1674
2335
|
const runKey = `p:${prodId}`;
|
|
1675
2336
|
this.prodIdByKey.set(runKey, prodId);
|
|
1676
2337
|
let local = [...this.runs.values()].find((r) => r.runKey === runKey);
|
|
2338
|
+
if (!local) local = [...this.runs.values()].find((r) => this.prodIdByKey.get(r.runKey) === prodId);
|
|
1677
2339
|
if (!local) {
|
|
1678
2340
|
const id = this.nextRunId++;
|
|
1679
2341
|
local = {
|
|
@@ -1696,6 +2358,8 @@ var SwarmClient = class _SwarmClient {
|
|
|
1696
2358
|
stop_reason: null,
|
|
1697
2359
|
respawns: 0,
|
|
1698
2360
|
resolves_run_id: null,
|
|
2361
|
+
reviews_run_id: null,
|
|
2362
|
+
review_verdict: null,
|
|
1699
2363
|
task_title: null,
|
|
1700
2364
|
task_desc: null,
|
|
1701
2365
|
task_url: null,
|
|
@@ -1707,10 +2371,19 @@ var SwarmClient = class _SwarmClient {
|
|
|
1707
2371
|
}
|
|
1708
2372
|
const slug = pr.taskSlug ?? pr.task?.slug ?? pr.taskId ?? local.task_slug;
|
|
1709
2373
|
local.task_slug = slug || local.task_slug;
|
|
2374
|
+
local.task_title = pr.taskTitle ?? local.task_title;
|
|
2375
|
+
local.task_desc = pr.taskDesc ?? local.task_desc;
|
|
1710
2376
|
local.name = pr.droneName ?? local.name;
|
|
1711
2377
|
local.kind = pr.role ?? local.kind;
|
|
1712
2378
|
const heroProd = pr.heroId ?? null;
|
|
1713
|
-
local.hero_run_id =
|
|
2379
|
+
if (heroProd) local.hero_run_id = this.localIdForProdId(heroProd) ?? local.hero_run_id;
|
|
2380
|
+
const resProd = pr.resolvesRunId ?? null;
|
|
2381
|
+
if (resProd) local.resolves_run_id = this.localIdForProdId(resProd) ?? local.resolves_run_id;
|
|
2382
|
+
const supProd = pr.supersededById ?? null;
|
|
2383
|
+
if (supProd) local.superseded_by = this.localIdForProdId(supProd) ?? local.superseded_by;
|
|
2384
|
+
const revProd = pr.reviewsRunId ?? null;
|
|
2385
|
+
if (revProd) local.reviews_run_id = this.localIdForProdId(revProd) ?? local.reviews_run_id;
|
|
2386
|
+
local.review_verdict = pr.reviewVerdict ?? local.review_verdict;
|
|
1714
2387
|
local.repo = pr.repo ?? "";
|
|
1715
2388
|
local.branch = pr.branch ?? null;
|
|
1716
2389
|
local.status = pr.status ?? local.status;
|
|
@@ -1779,14 +2452,31 @@ var SwarmClient = class _SwarmClient {
|
|
|
1779
2452
|
}
|
|
1780
2453
|
resolveArgs(e) {
|
|
1781
2454
|
if (e.t === "run") {
|
|
2455
|
+
const fields = this.materializeRefs(e.fields);
|
|
1782
2456
|
const prodId2 = this.prodIdByKey.get(e.runKey);
|
|
1783
|
-
return prodId2 ? { tool: "swarm_run_upsert", args: { runId: prodId2, ...
|
|
2457
|
+
return prodId2 ? { tool: "swarm_run_upsert", args: { runId: prodId2, ...fields } } : { tool: "swarm_run_upsert", args: { taskId: e.taskRef, ...fields } };
|
|
1784
2458
|
}
|
|
1785
2459
|
const prodId = this.prodIdByKey.get(e.runKey);
|
|
1786
2460
|
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
2461
|
if (e.t === "event") return { tool: "swarm_event_append", args: { runId: prodId, kind: e.kind, text: e.text, actor: e.actor } };
|
|
1788
2462
|
return { tool: "swarm_ask", args: { runId: prodId, text: e.text } };
|
|
1789
2463
|
}
|
|
2464
|
+
// Резолв отложенных self-ref ({__runRef: runKey}) в прод-cuid на флаше. Цель уже
|
|
2465
|
+
// создана в проде (её create-запись с меньшим seq флашится раньше) → cuid известен.
|
|
2466
|
+
// Не резолвится (ещё не создан) — поле выкидываем: лучше не слать, чем слать мусор;
|
|
2467
|
+
// догонит следующий апдейт/реконсайл. Обычные значения (null/строки) — как есть.
|
|
2468
|
+
materializeRefs(fields) {
|
|
2469
|
+
const out = {};
|
|
2470
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
2471
|
+
if (isRunRef(v)) {
|
|
2472
|
+
const cuid = this.prodIdByKey.get(v.__runRef);
|
|
2473
|
+
if (cuid) out[k] = cuid;
|
|
2474
|
+
} else {
|
|
2475
|
+
out[k] = v;
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
return out;
|
|
2479
|
+
}
|
|
1790
2480
|
/** Слить накопленное в прод. Возвращает число успешно отправленных операций.
|
|
1791
2481
|
* Порядок сохраняется; ошибка/недоступность → стоп, буфер остаётся, retry-backoff. */
|
|
1792
2482
|
async flush() {
|
|
@@ -1815,6 +2505,9 @@ var SwarmClient = class _SwarmClient {
|
|
|
1815
2505
|
this.prodIdByKey.set(e.runKey, run.id);
|
|
1816
2506
|
this.persistMap();
|
|
1817
2507
|
}
|
|
2508
|
+
} else {
|
|
2509
|
+
const evId = res.structuredContent?.event?.id;
|
|
2510
|
+
if (evId) this.seenProdEventIds.add(evId);
|
|
1818
2511
|
}
|
|
1819
2512
|
this.pending.shift();
|
|
1820
2513
|
sent++;
|
|
@@ -1833,13 +2526,13 @@ var SwarmClient = class _SwarmClient {
|
|
|
1833
2526
|
}
|
|
1834
2527
|
persistPending() {
|
|
1835
2528
|
try {
|
|
1836
|
-
|
|
2529
|
+
writeFileSync2(this.outboxPath, this.pending.map((e) => JSON.stringify(e)).join("\n") + (this.pending.length ? "\n" : ""));
|
|
1837
2530
|
} catch {
|
|
1838
2531
|
}
|
|
1839
2532
|
}
|
|
1840
2533
|
persistMap() {
|
|
1841
2534
|
try {
|
|
1842
|
-
|
|
2535
|
+
writeFileSync2(this.mapPath, JSON.stringify(Object.fromEntries(this.prodIdByKey)));
|
|
1843
2536
|
} catch {
|
|
1844
2537
|
}
|
|
1845
2538
|
}
|
|
@@ -1863,26 +2556,20 @@ function parseDecision(v) {
|
|
|
1863
2556
|
return null;
|
|
1864
2557
|
}
|
|
1865
2558
|
}
|
|
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
2559
|
|
|
1873
2560
|
// src/overmind.ts
|
|
1874
2561
|
init_config();
|
|
1875
2562
|
|
|
1876
2563
|
// src/heartbeat.ts
|
|
1877
|
-
import { readFileSync as
|
|
2564
|
+
import { readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1878
2565
|
function writeHeartbeat(path, now) {
|
|
1879
2566
|
const tmp = `${path}.tmp`;
|
|
1880
|
-
|
|
1881
|
-
|
|
2567
|
+
writeFileSync3(tmp, String(now));
|
|
2568
|
+
renameSync2(tmp, path);
|
|
1882
2569
|
}
|
|
1883
2570
|
function readHeartbeat(path) {
|
|
1884
2571
|
try {
|
|
1885
|
-
const ms = Number(
|
|
2572
|
+
const ms = Number(readFileSync4(path, "utf8").trim());
|
|
1886
2573
|
return Number.isFinite(ms) ? ms : null;
|
|
1887
2574
|
} catch {
|
|
1888
2575
|
return null;
|
|
@@ -1896,47 +2583,210 @@ function overmindAlive(path, now, staleMs) {
|
|
|
1896
2583
|
|
|
1897
2584
|
// src/launcher.ts
|
|
1898
2585
|
import { spawn } from "child_process";
|
|
1899
|
-
import { readdirSync, writeFileSync as
|
|
2586
|
+
import { closeSync, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync, readdirSync, statSync, writeFileSync as writeFileSync4 } from "fs";
|
|
1900
2587
|
import { homedir as homedir2 } from "os";
|
|
1901
2588
|
import { join as join2 } from "path";
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2589
|
+
var ISOLATION_FLAGS = ["--setting-sources", "project", "--strict-mcp-config"];
|
|
2590
|
+
function claudeArgs(cfgPath, prompt, continueSession = false, model, pluginDirs = []) {
|
|
2591
|
+
const pluginFlags = pluginDirs.flatMap((dir) => ["--plugin-dir", dir]);
|
|
2592
|
+
const base2 = [
|
|
2593
|
+
"--dangerously-skip-permissions",
|
|
2594
|
+
...ISOLATION_FLAGS,
|
|
2595
|
+
"--mcp-config",
|
|
2596
|
+
cfgPath,
|
|
2597
|
+
...pluginFlags,
|
|
2598
|
+
"--output-format",
|
|
2599
|
+
"json",
|
|
2600
|
+
"-p",
|
|
2601
|
+
prompt
|
|
2602
|
+
];
|
|
2603
|
+
const withModel = model ? ["--model", model, ...base2] : base2;
|
|
2604
|
+
return continueSession ? ["--continue", ...withModel] : withModel;
|
|
2605
|
+
}
|
|
2606
|
+
function roleDir(worktree, kind) {
|
|
2607
|
+
return join2(worktree, ".claude", "roles", kind);
|
|
2608
|
+
}
|
|
2609
|
+
function transcriptDir(worktree, projectsDir) {
|
|
2610
|
+
return join2(projectsDir, worktree.replace(/[/.]/g, "-"));
|
|
1905
2611
|
}
|
|
1906
2612
|
function sessionTranscriptExists(worktree, projectsDir = join2(homedir2(), ".claude", "projects")) {
|
|
1907
|
-
const dir = join2(projectsDir, worktree.replace(/[/.]/g, "-"));
|
|
1908
2613
|
try {
|
|
1909
|
-
return readdirSync(
|
|
2614
|
+
return readdirSync(transcriptDir(worktree, projectsDir)).some((f) => f.endsWith(".jsonl"));
|
|
1910
2615
|
} catch {
|
|
1911
2616
|
return false;
|
|
1912
2617
|
}
|
|
1913
2618
|
}
|
|
2619
|
+
function transcriptMtimeMs(worktree, projectsDir = join2(homedir2(), ".claude", "projects")) {
|
|
2620
|
+
const dir = transcriptDir(worktree, projectsDir);
|
|
2621
|
+
let files;
|
|
2622
|
+
try {
|
|
2623
|
+
files = readdirSync(dir);
|
|
2624
|
+
} catch {
|
|
2625
|
+
return null;
|
|
2626
|
+
}
|
|
2627
|
+
let latest = null;
|
|
2628
|
+
for (const f of files) {
|
|
2629
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
2630
|
+
try {
|
|
2631
|
+
const mtime2 = statSync(join2(dir, f)).mtimeMs;
|
|
2632
|
+
if (latest === null || mtime2 > latest) latest = mtime2;
|
|
2633
|
+
} catch {
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
return latest;
|
|
2637
|
+
}
|
|
2638
|
+
function nextRunLogPath(runLogsDir, runId) {
|
|
2639
|
+
const dir = join2(runLogsDir, String(runId));
|
|
2640
|
+
mkdirSync3(dir, { recursive: true });
|
|
2641
|
+
let files = [];
|
|
2642
|
+
try {
|
|
2643
|
+
files = readdirSync(dir);
|
|
2644
|
+
} catch {
|
|
2645
|
+
}
|
|
2646
|
+
let next = 0;
|
|
2647
|
+
for (const f of files) {
|
|
2648
|
+
const m = /^(\d+)\.json/.exec(f);
|
|
2649
|
+
if (m) next = Math.max(next, Number(m[1]) + 1);
|
|
2650
|
+
}
|
|
2651
|
+
return join2(dir, `${next}.json`);
|
|
2652
|
+
}
|
|
2653
|
+
function openNextRunLog(runLogsDir, runId) {
|
|
2654
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
2655
|
+
try {
|
|
2656
|
+
return openSync(nextRunLogPath(runLogsDir, runId), "wx");
|
|
2657
|
+
} catch (e) {
|
|
2658
|
+
if (e.code !== "EEXIST") throw e;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
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?)`);
|
|
2662
|
+
}
|
|
1914
2663
|
function makeLauncher(cfg) {
|
|
1915
2664
|
return ({ worktree, run, prompt, continueSession }) => {
|
|
2665
|
+
const isCompiled = cfg.overmindEntry.endsWith(".js");
|
|
2666
|
+
if (!isCompiled && !cfg.overmindEntry.endsWith(".ts")) {
|
|
2667
|
+
throw new Error(`overmindEntry \u043D\u0438 .ts, \u043D\u0438 .js: ${cfg.overmindEntry}`);
|
|
2668
|
+
}
|
|
1916
2669
|
const mcpConfig = {
|
|
1917
2670
|
mcpServers: {
|
|
1918
2671
|
overmind: {
|
|
1919
|
-
command: "bun",
|
|
1920
|
-
args: ["run", cfg.overmindEntry, "--drone", "--run", String(run.id)],
|
|
2672
|
+
command: isCompiled ? process.execPath : "bun",
|
|
2673
|
+
args: isCompiled ? [cfg.overmindEntry, "--drone", "--run", String(run.id)] : ["run", cfg.overmindEntry, "--drone", "--run", String(run.id)],
|
|
1921
2674
|
env: { SWARM_DB: cfg.dbPath }
|
|
1922
2675
|
}
|
|
1923
2676
|
}
|
|
1924
2677
|
};
|
|
1925
2678
|
const cfgPath = join2(worktree, ".swarm-mcp.json");
|
|
1926
|
-
|
|
1927
|
-
const
|
|
2679
|
+
writeFileSync4(cfgPath, JSON.stringify(mcpConfig, null, 2));
|
|
2680
|
+
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? void 0;
|
|
2681
|
+
const dir = roleDir(worktree, run.kind);
|
|
2682
|
+
const pluginDirs = existsSync3(dir) ? [dir] : [];
|
|
2683
|
+
let stdio = "ignore";
|
|
2684
|
+
let outFd;
|
|
2685
|
+
if (cfg.runLogsDir) {
|
|
2686
|
+
outFd = openNextRunLog(cfg.runLogsDir, run.id);
|
|
2687
|
+
stdio = ["ignore", outFd, "ignore"];
|
|
2688
|
+
}
|
|
2689
|
+
const child = spawn(cfg.claudeBin ?? "claude", claudeArgs(cfgPath, prompt, continueSession, model, pluginDirs), {
|
|
1928
2690
|
cwd: worktree,
|
|
1929
2691
|
detached: true,
|
|
1930
|
-
stdio
|
|
2692
|
+
stdio
|
|
1931
2693
|
});
|
|
2694
|
+
if (outFd !== void 0) closeSync(outFd);
|
|
1932
2695
|
child.unref();
|
|
1933
2696
|
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
2697
|
return child.pid;
|
|
1935
2698
|
};
|
|
1936
2699
|
}
|
|
1937
2700
|
|
|
1938
|
-
// src/
|
|
1939
|
-
import {
|
|
2701
|
+
// src/cost.ts
|
|
2702
|
+
import { readFileSync as readFileSync5, readdirSync as readdirSync2, renameSync as renameSync3 } from "fs";
|
|
2703
|
+
import { join as join3 } from "path";
|
|
2704
|
+
|
|
2705
|
+
// src/pricing.ts
|
|
2706
|
+
var MODEL_PRICING = {
|
|
2707
|
+
"claude-sonnet-5": { inputPerMTok: 3, outputPerMTok: 15 },
|
|
2708
|
+
"claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
|
|
2709
|
+
"claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
|
|
2710
|
+
};
|
|
2711
|
+
var CACHE_READ_FACTOR = 0.1;
|
|
2712
|
+
var CACHE_WRITE_5M_FACTOR = 1.25;
|
|
2713
|
+
function costFromUsage(usage, model) {
|
|
2714
|
+
const pricing = model ? MODEL_PRICING[model] : void 0;
|
|
2715
|
+
if (!pricing) {
|
|
2716
|
+
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` };
|
|
2717
|
+
}
|
|
2718
|
+
const input = usage.input_tokens ?? 0;
|
|
2719
|
+
const output = usage.output_tokens ?? 0;
|
|
2720
|
+
const cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
2721
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
2722
|
+
const costUsd = (input * pricing.inputPerMTok + output * pricing.outputPerMTok + cacheWrite * pricing.inputPerMTok * CACHE_WRITE_5M_FACTOR + cacheRead * pricing.inputPerMTok * CACHE_READ_FACTOR) / 1e6;
|
|
2723
|
+
return { costUsd };
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// src/claudeResult.ts
|
|
2727
|
+
function parseClaudeResultJson(raw) {
|
|
2728
|
+
const trimmed = raw.trim();
|
|
2729
|
+
if (!trimmed) return null;
|
|
2730
|
+
try {
|
|
2731
|
+
const parsed = JSON.parse(trimmed);
|
|
2732
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
2733
|
+
} catch {
|
|
2734
|
+
return null;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
function launchCostUsd(result, model) {
|
|
2738
|
+
if (typeof result.total_cost_usd === "number" && result.total_cost_usd > 0) {
|
|
2739
|
+
return { costUsd: result.total_cost_usd };
|
|
2740
|
+
}
|
|
2741
|
+
if (!result.usage) return { costUsd: 0 };
|
|
2742
|
+
return costFromUsage(result.usage, model);
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
// src/cost.ts
|
|
2746
|
+
var UNPROCESSED_LOG_RE = /^\d+\.json$/;
|
|
2747
|
+
function unprocessedLogFiles(dir) {
|
|
2748
|
+
try {
|
|
2749
|
+
return readdirSync2(dir).filter((f) => UNPROCESSED_LOG_RE.test(f));
|
|
2750
|
+
} catch {
|
|
2751
|
+
return [];
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
function accountRunCost(deps, run) {
|
|
2755
|
+
const dir = join3(deps.runLogsDir, String(run.id));
|
|
2756
|
+
const files = unprocessedLogFiles(dir);
|
|
2757
|
+
if (files.length === 0) return;
|
|
2758
|
+
const model = run.model ?? process.env.SWARM_DRONE_MODEL ?? null;
|
|
2759
|
+
let added = 0;
|
|
2760
|
+
const processed = [];
|
|
2761
|
+
for (const f of files) {
|
|
2762
|
+
const full = join3(dir, f);
|
|
2763
|
+
let raw = null;
|
|
2764
|
+
try {
|
|
2765
|
+
raw = readFileSync5(full, "utf8");
|
|
2766
|
+
} catch {
|
|
2767
|
+
continue;
|
|
2768
|
+
}
|
|
2769
|
+
const result = parseClaudeResultJson(raw);
|
|
2770
|
+
if (!result) {
|
|
2771
|
+
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?)`);
|
|
2772
|
+
} else {
|
|
2773
|
+
const est = launchCostUsd(result, model);
|
|
2774
|
+
if (est.warning) console.warn(`[overmind] cost \u043F\u0440\u043E\u0433\u043E\u043D\u0430 #${run.id}: ${est.warning}`);
|
|
2775
|
+
added += est.costUsd;
|
|
2776
|
+
}
|
|
2777
|
+
processed.push(full);
|
|
2778
|
+
}
|
|
2779
|
+
if (added > 0) deps.db.updateRun(run.id, { cost_usd: run.cost_usd + added });
|
|
2780
|
+
for (const full of processed) {
|
|
2781
|
+
try {
|
|
2782
|
+
renameSync3(full, `${full}.done`);
|
|
2783
|
+
} catch {
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
function makeCostAccountant(deps) {
|
|
2788
|
+
return (run) => accountRunCost(deps, run);
|
|
2789
|
+
}
|
|
1940
2790
|
|
|
1941
2791
|
// src/git.ts
|
|
1942
2792
|
import { $ } from "execa";
|
|
@@ -2050,7 +2900,7 @@ async function spawnDrone(deps, run) {
|
|
|
2050
2900
|
throw new AreaBusyError(run.area);
|
|
2051
2901
|
}
|
|
2052
2902
|
const branch = run.task_slug;
|
|
2053
|
-
const worktree =
|
|
2903
|
+
const worktree = droneWorktreePath(deps.worktreesDir, repo, run.task_slug);
|
|
2054
2904
|
const source = await addWorktree(repo, worktree, branch);
|
|
2055
2905
|
await setWorktreeIdentity(worktree, run.name);
|
|
2056
2906
|
const sourceMsg = worktreeSourceMessage(branch, source);
|
|
@@ -2070,13 +2920,13 @@ function isGithubPrUrl(url) {
|
|
|
2070
2920
|
return GH_PR_RE.test(url.trim());
|
|
2071
2921
|
}
|
|
2072
2922
|
function softStatus() {
|
|
2073
|
-
return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false };
|
|
2923
|
+
return { merged: false, state: "UNKNOWN", mergeable: "UNKNOWN", mergeStateStatus: "UNKNOWN", conflicting: false, isDraft: false, files: [] };
|
|
2074
2924
|
}
|
|
2075
2925
|
async function prStatus(prUrl, run = realRunner) {
|
|
2076
2926
|
if (!isGithubPrUrl(prUrl)) return softStatus();
|
|
2077
2927
|
let out;
|
|
2078
2928
|
try {
|
|
2079
|
-
out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus"]);
|
|
2929
|
+
out = await run(["pr", "view", prUrl, "--json", "state,mergedAt,mergeable,mergeStateStatus,isDraft,files"]);
|
|
2080
2930
|
} catch {
|
|
2081
2931
|
return softStatus();
|
|
2082
2932
|
}
|
|
@@ -2095,7 +2945,9 @@ async function prStatus(prUrl, run = realRunner) {
|
|
|
2095
2945
|
mergeable,
|
|
2096
2946
|
mergeStateStatus: j.mergeStateStatus ?? "UNKNOWN",
|
|
2097
2947
|
// не смержен, открыт и GitHub уверенно говорит CONFLICTING
|
|
2098
|
-
conflicting: !merged && state === "OPEN" && mergeable === "CONFLICTING"
|
|
2948
|
+
conflicting: !merged && state === "OPEN" && mergeable === "CONFLICTING",
|
|
2949
|
+
isDraft: j.isDraft === true,
|
|
2950
|
+
files: Array.isArray(j.files) ? j.files.map((f) => f.path).filter((p) => typeof p === "string") : []
|
|
2099
2951
|
};
|
|
2100
2952
|
}
|
|
2101
2953
|
async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
|
|
@@ -2111,6 +2963,78 @@ async function openPrsForBranch(branch, repoDir, run = realRepoRunner) {
|
|
|
2111
2963
|
async function closePR(prUrl, run = realRunner) {
|
|
2112
2964
|
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
2965
|
}
|
|
2966
|
+
var realChecksRunner = async (pr) => (await $2({ reject: false, timeout: 15e3 })`gh pr checks ${pr} --json state,name,bucket,link`).stdout;
|
|
2967
|
+
async function prCheckRows(pr, run) {
|
|
2968
|
+
try {
|
|
2969
|
+
const out = await run(pr);
|
|
2970
|
+
const rows = JSON.parse(out);
|
|
2971
|
+
if (!Array.isArray(rows) || rows.length === 0) return null;
|
|
2972
|
+
return rows.map((r) => ({ name: r.name ?? "?", bucket: (r.bucket ?? "").toLowerCase(), link: r.link ?? "" }));
|
|
2973
|
+
} catch {
|
|
2974
|
+
return null;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
async function ghChecks(pr, run = realChecksRunner) {
|
|
2978
|
+
const rows = await prCheckRows(pr, run);
|
|
2979
|
+
if (!rows) return null;
|
|
2980
|
+
let pass = 0, fail = 0, pending2 = 0;
|
|
2981
|
+
for (const r of rows) {
|
|
2982
|
+
if (r.bucket === "pass") pass++;
|
|
2983
|
+
else if (r.bucket === "fail" || r.bucket === "cancel") fail++;
|
|
2984
|
+
else if (r.bucket === "pending") pending2++;
|
|
2985
|
+
}
|
|
2986
|
+
const total = rows.length;
|
|
2987
|
+
let bucket, text, tone;
|
|
2988
|
+
if (fail > 0) {
|
|
2989
|
+
bucket = "fail";
|
|
2990
|
+
tone = "fail";
|
|
2991
|
+
text = `\u274C CI \u0443\u043F\u0430\u043B (${fail}/${total})`;
|
|
2992
|
+
} else if (pending2 > 0) {
|
|
2993
|
+
bucket = "pending";
|
|
2994
|
+
tone = "review";
|
|
2995
|
+
text = `\u23F3 CI \u0438\u0434\u0451\u0442 (${pass}/${total})`;
|
|
2996
|
+
} else {
|
|
2997
|
+
bucket = "pass";
|
|
2998
|
+
tone = "merge";
|
|
2999
|
+
text = `\u2705 CI \u0437\u0435\u043B\u0451\u043D\u044B\u0439 \u2014 \u043A \u043C\u0435\u0440\u0436\u0443`;
|
|
3000
|
+
}
|
|
3001
|
+
return { bucket, text, tone, total, pass, fail, pending: pending2 };
|
|
3002
|
+
}
|
|
3003
|
+
function runIdFromLink(link) {
|
|
3004
|
+
const m = link.match(/\/actions\/runs\/(\d+)/);
|
|
3005
|
+
return m ? m[1] : null;
|
|
3006
|
+
}
|
|
3007
|
+
async function prCiStatus(pr, run = realChecksRunner) {
|
|
3008
|
+
const rows = await prCheckRows(pr, run);
|
|
3009
|
+
if (!rows) return null;
|
|
3010
|
+
const failingRows = rows.filter((r) => r.bucket === "fail" || r.bucket === "cancel");
|
|
3011
|
+
if (failingRows.length > 0) {
|
|
3012
|
+
const failing = failingRows.map((r) => r.name);
|
|
3013
|
+
const failingRunIds = [...new Set(failingRows.map((r) => runIdFromLink(r.link)).filter((id) => id != null))];
|
|
3014
|
+
return { bucket: "fail", failing, failingRunIds };
|
|
3015
|
+
}
|
|
3016
|
+
if (rows.some((r) => r.bucket === "pending")) return { bucket: "pending", failing: [], failingRunIds: [] };
|
|
3017
|
+
return { bucket: "pass", failing: [], failingRunIds: [] };
|
|
3018
|
+
}
|
|
3019
|
+
var realRunLogRunner = async (runId) => (await $2({ reject: false, timeout: 3e4, maxBuffer: 10 * 1024 * 1024 })`gh run view ${runId} --log-failed`).stdout;
|
|
3020
|
+
var CI_SUMMARY_LOG_CHARS = 4e3;
|
|
3021
|
+
var CI_SUMMARY_MAX_RUNS = 2;
|
|
3022
|
+
async function ciFailureSummary(failing, failingRunIds, logRun = realRunLogRunner) {
|
|
3023
|
+
if (!failing.length) return null;
|
|
3024
|
+
const logs = [];
|
|
3025
|
+
for (const runId of failingRunIds.slice(0, CI_SUMMARY_MAX_RUNS)) {
|
|
3026
|
+
try {
|
|
3027
|
+
const out = await logRun(runId);
|
|
3028
|
+
if (out.trim()) logs.push(out.length > CI_SUMMARY_LOG_CHARS ? `\u2026
|
|
3029
|
+
${out.slice(-CI_SUMMARY_LOG_CHARS)}` : out);
|
|
3030
|
+
} catch {
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
const header = `\u274C CI \u0443\u043F\u0430\u043B: ${failing.join(", ")}`;
|
|
3034
|
+
return logs.length ? `${header}
|
|
3035
|
+
|
|
3036
|
+
${logs.join("\n---\n")}` : header;
|
|
3037
|
+
}
|
|
2114
3038
|
|
|
2115
3039
|
// src/droneServer.ts
|
|
2116
3040
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -2208,15 +3132,49 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
2208
3132
|
);
|
|
2209
3133
|
server.tool(
|
|
2210
3134
|
"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
|
-
|
|
3135
|
+
"\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.",
|
|
3136
|
+
{
|
|
3137
|
+
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)"),
|
|
3138
|
+
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."),
|
|
3139
|
+
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 REV-\u0437\u0430\u0434\u0430\u0447\u0443 \u0438 \u0437\u0430\u0434\u0430\u0447\u0443-\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A.")
|
|
3140
|
+
},
|
|
3141
|
+
async ({ pr_url, verdict, summary }) => {
|
|
3142
|
+
const run = db.getRun(runId);
|
|
3143
|
+
if (run?.kind === "hero") {
|
|
2215
3144
|
return {
|
|
2216
3145
|
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
3146
|
isError: true
|
|
2218
3147
|
};
|
|
2219
3148
|
}
|
|
3149
|
+
if (run?.reviews_run_id != null) {
|
|
3150
|
+
if (verdict !== "approve" && verdict !== "changes") {
|
|
3151
|
+
return {
|
|
3152
|
+
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).' }],
|
|
3153
|
+
isError: true
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3156
|
+
const reviewed = db.getRun(run.reviews_run_id);
|
|
3157
|
+
const verdictText = verdict === "approve" ? "\u2705 APPROVE" : "\u274C REQUEST CHANGES";
|
|
3158
|
+
db.updateRun(run.reviews_run_id, { review_verdict: verdict });
|
|
3159
|
+
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}` : ""}`);
|
|
3160
|
+
if (opts.taskless) {
|
|
3161
|
+
const note = `${verdictText} \u043E\u0442 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430 ${run.name} (\u043F\u0440\u043E\u0433\u043E\u043D #${runId})${summary ? `
|
|
3162
|
+
|
|
3163
|
+
${summary}` : ""}`;
|
|
3164
|
+
await Promise.all([
|
|
3165
|
+
opts.taskless.addComment(run.task_slug, note).catch(() => {
|
|
3166
|
+
}),
|
|
3167
|
+
// REV-задача
|
|
3168
|
+
reviewed ? opts.taskless.addComment(reviewed.task_slug, note).catch(() => {
|
|
3169
|
+
}) : Promise.resolve()
|
|
3170
|
+
// задача-источник
|
|
3171
|
+
]);
|
|
3172
|
+
}
|
|
3173
|
+
const trustedPrUrl = reviewed?.pr_url ?? pr_url;
|
|
3174
|
+
db.updateRun(runId, { status: "review", pr_url: trustedPrUrl });
|
|
3175
|
+
db.addEvent(runId, "pr_opened", trustedPrUrl);
|
|
3176
|
+
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" }] };
|
|
3177
|
+
}
|
|
2220
3178
|
if (isGithubPrUrl(pr_url)) {
|
|
2221
3179
|
db.updateRun(runId, { status: "review", pr_url });
|
|
2222
3180
|
db.addEvent(runId, "pr_opened", pr_url);
|
|
@@ -2260,7 +3218,9 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
2260
3218
|
title,
|
|
2261
3219
|
description: description_md + footer,
|
|
2262
3220
|
estimateHours: estimate_hours,
|
|
2263
|
-
statusId
|
|
3221
|
+
statusId,
|
|
3222
|
+
projectId: ep.projectId
|
|
3223
|
+
// TASWM-138: наследуем проект эпика — иначе сирота не claimable
|
|
2264
3224
|
});
|
|
2265
3225
|
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
3226
|
db.addEvent(runId, "step", `\u043F\u043E\u0434\u0437\u0430\u0434\u0430\u0447\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0430: ${created.slug}`);
|
|
@@ -2338,156 +3298,53 @@ function buildDroneServer(db, runId, opts = {}) {
|
|
|
2338
3298
|
if (!mine) return errText(`\u0432\u043E\u043F\u0440\u043E\u0441 #${question_id} \u043D\u0435 \u0438\u0437 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 \u0432\u043E\u043F\u0440\u043E\u0441\u043E\u0432 \u0442\u0432\u043E\u0435\u0433\u043E \u043E\u0442\u0440\u044F\u0434\u0430 \u2014 \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0435\u043B\u044C\u0437\u044F.`);
|
|
2339
3299
|
const answered = db.answer(question_id, text, "hero");
|
|
2340
3300
|
if (!answered || answered.status !== "answered") return errText(`\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u0432\u0435\u0442\u0438\u0442\u044C \u043D\u0430 \u0432\u043E\u043F\u0440\u043E\u0441 #${question_id}.`);
|
|
2341
|
-
return { content: [{ type: "text", text: `\u043E\u0442\u0432\u0435\u0442 \u0434\u0440\u043E\u043D\u0443 (\u0432\u043E\u043F\u0440\u043E\u0441 #${question_id}) \u0437\u0430\u043F\u0438\u0441\u0430\u043D, \u0434\u0440\u043E\u043D \u0440\u0430\u0437\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D` }] };
|
|
2342
|
-
}
|
|
2343
|
-
);
|
|
2344
|
-
}
|
|
2345
|
-
return server;
|
|
2346
|
-
}
|
|
2347
|
-
async function runDroneServer(runId) {
|
|
2348
|
-
const { SwarmDb: SwarmDb2 } = await Promise.resolve().then(() => (init_db(), db_exports));
|
|
2349
|
-
const { DB_PATH: DB_PATH2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
2350
|
-
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
2351
|
-
const db = new SwarmDb2(DB_PATH2);
|
|
2352
|
-
const taskless = db.getRun(runId)?.kind === "hero" ? await (await Promise.resolve().then(() => (init_taskless(), taskless_exports))).connectTaskless() : null;
|
|
2353
|
-
const server = buildDroneServer(db, runId, { taskless });
|
|
2354
|
-
await server.connect(new StdioServerTransport());
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
// src/overmind.ts
|
|
2358
|
-
init_taskless();
|
|
2359
|
-
|
|
2360
|
-
// src/web.ts
|
|
2361
|
-
import { randomBytes as randomBytes2 } from "crypto";
|
|
2362
|
-
import { basename as basename2 } from "path";
|
|
2363
|
-
import { serve } from "@hono/node-server";
|
|
2364
|
-
import { $ as $4 } from "execa";
|
|
2365
|
-
|
|
2366
|
-
// ../swarm-core/src/timeline.ts
|
|
2367
|
-
function buildSegments(created, events, laneEnd) {
|
|
2368
|
-
const segs = [];
|
|
2369
|
-
let cursor = created;
|
|
2370
|
-
let phase = "queued";
|
|
2371
|
-
const push = (to) => {
|
|
2372
|
-
if (to > cursor) segs.push({ phase, from: cursor, to });
|
|
2373
|
-
cursor = to;
|
|
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;
|
|
2408
|
-
}
|
|
2409
|
-
}
|
|
2410
|
-
push(laneEnd);
|
|
2411
|
-
return segs;
|
|
2412
|
-
}
|
|
2413
|
-
function computeBlocker(status, stopReason, lastStep, pr, ci, deps, openQ) {
|
|
2414
|
-
switch (status) {
|
|
2415
|
-
case "queued": {
|
|
2416
|
-
const unmet = deps.filter((d) => !d.met);
|
|
2417
|
-
if (unmet.length) {
|
|
2418
|
-
const who = unmet.map((d) => d.name ? `${d.slug}\xB7${d.name}` : d.slug).join(", ");
|
|
2419
|
-
return { icon: "\u26D4", text: `\u0436\u0434\u0451\u0442 ${who}`, tone: "dep" };
|
|
2420
|
-
}
|
|
2421
|
-
return { icon: "\u23F8", text: "\u0436\u0434\u0451\u0442 \u0441\u043B\u043E\u0442\u0430 / \u0441\u043F\u0430\u0432\u043D\u0430", tone: "queued" };
|
|
2422
|
-
}
|
|
2423
|
-
case "running":
|
|
2424
|
-
return { icon: "\u25B6", text: lastStep ? `\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442: ${lastStep}` : "\u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", tone: "run" };
|
|
2425
|
-
case "blocked":
|
|
2426
|
-
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" };
|
|
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 };
|
|
2434
|
-
}
|
|
2435
|
-
case "stalled":
|
|
2436
|
-
return { icon: "\u26A0\uFE0F", text: stopReason || (lastStep ? `\u043C\u043E\u043B\u0447\u0438\u0442: ${lastStep}` : "\u043C\u043E\u043B\u0447\u0438\u0442"), tone: "stalled" };
|
|
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" };
|
|
3301
|
+
return { content: [{ type: "text", text: `\u043E\u0442\u0432\u0435\u0442 \u0434\u0440\u043E\u043D\u0443 (\u0432\u043E\u043F\u0440\u043E\u0441 #${question_id}) \u0437\u0430\u043F\u0438\u0441\u0430\u043D, \u0434\u0440\u043E\u043D \u0440\u0430\u0437\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u043D` }] };
|
|
3302
|
+
}
|
|
3303
|
+
);
|
|
2445
3304
|
}
|
|
3305
|
+
return server;
|
|
2446
3306
|
}
|
|
2447
|
-
function
|
|
2448
|
-
const
|
|
2449
|
-
const
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
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;
|
|
3307
|
+
async function runDroneServer(runId) {
|
|
3308
|
+
const { SwarmDb: SwarmDb2 } = await Promise.resolve().then(() => (init_db(), db_exports));
|
|
3309
|
+
const { DB_PATH: DB_PATH2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
3310
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
3311
|
+
const db = new SwarmDb2(DB_PATH2);
|
|
3312
|
+
const run = db.getRun(runId);
|
|
3313
|
+
const taskless = run?.kind === "hero" || run?.reviews_run_id != null ? await (await Promise.resolve().then(() => (init_taskless(), taskless_exports))).connectTaskless() : null;
|
|
3314
|
+
const server = buildDroneServer(db, runId, { taskless });
|
|
3315
|
+
await server.connect(new StdioServerTransport());
|
|
2469
3316
|
}
|
|
2470
3317
|
|
|
2471
|
-
//
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
3318
|
+
// src/overmind.ts
|
|
3319
|
+
init_taskless();
|
|
3320
|
+
|
|
3321
|
+
// src/maxDrones.ts
|
|
3322
|
+
function makeMaxDronesReader(opts) {
|
|
3323
|
+
const ttl = opts.ttlMs ?? 1e4;
|
|
3324
|
+
const now = opts.now ?? Date.now;
|
|
3325
|
+
let cached = null;
|
|
3326
|
+
let cachedAt = -Infinity;
|
|
3327
|
+
return async () => {
|
|
3328
|
+
const t = now();
|
|
3329
|
+
if (cached != null && t - cachedAt < ttl) return cached;
|
|
3330
|
+
const live = await opts.fetch().catch(() => null);
|
|
3331
|
+
if (live != null && Number.isFinite(live) && live > 0) {
|
|
3332
|
+
cached = Math.trunc(live);
|
|
3333
|
+
cachedAt = t;
|
|
3334
|
+
return cached;
|
|
3335
|
+
}
|
|
3336
|
+
return opts.fallback;
|
|
3337
|
+
};
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
// src/web.ts
|
|
3341
|
+
init_src();
|
|
3342
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
3343
|
+
import { basename as basename2 } from "path";
|
|
3344
|
+
import { serve } from "@hono/node-server";
|
|
2488
3345
|
|
|
2489
3346
|
// src/diff.ts
|
|
2490
|
-
import { statSync } from "fs";
|
|
3347
|
+
import { statSync as statSync2 } from "fs";
|
|
2491
3348
|
import { join as join6 } from "path";
|
|
2492
3349
|
import { $ as $3 } from "execa";
|
|
2493
3350
|
var MAX_FILES = 200;
|
|
@@ -2624,7 +3481,7 @@ async function resolveBase(wt) {
|
|
|
2624
3481
|
}
|
|
2625
3482
|
function mtime(path) {
|
|
2626
3483
|
try {
|
|
2627
|
-
return
|
|
3484
|
+
return statSync2(path).mtimeMs;
|
|
2628
3485
|
} catch {
|
|
2629
3486
|
return 0;
|
|
2630
3487
|
}
|
|
@@ -2671,19 +3528,17 @@ init_oauth();
|
|
|
2671
3528
|
var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
2672
3529
|
var jsonForScript = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
|
|
2673
3530
|
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
3531
|
var sqliteIso = (ts) => {
|
|
2677
|
-
const ms =
|
|
3532
|
+
const ms = sqliteMs(ts);
|
|
2678
3533
|
return Number.isNaN(ms) ? ts : new Date(ms).toISOString();
|
|
2679
3534
|
};
|
|
2680
|
-
function roySnapshot(db, now = Date.now()) {
|
|
3535
|
+
function roySnapshot(db, now = Date.now(), overmindLog = []) {
|
|
2681
3536
|
const all = db.listRuns();
|
|
2682
3537
|
const c = (s) => all.filter((r) => r.status === s).length;
|
|
2683
3538
|
const withQ = new Set(db.runIdsWithQuestions());
|
|
2684
3539
|
const shown = all.filter((r) => {
|
|
2685
3540
|
if (r.status === "superseded") return false;
|
|
2686
|
-
if (r.status === "done" || r.status === "stopped" || r.status === "failed") return
|
|
3541
|
+
if (r.status === "done" || r.status === "stopped" || r.status === "failed") return sqliteMs(r.updated_at) >= now - DAY_MS;
|
|
2687
3542
|
return true;
|
|
2688
3543
|
});
|
|
2689
3544
|
return {
|
|
@@ -2747,13 +3602,15 @@ function roySnapshot(db, now = Date.now()) {
|
|
|
2747
3602
|
active: db.stormActive(),
|
|
2748
3603
|
since: db.currentStormStartedAt(),
|
|
2749
3604
|
dead: all.filter((r) => r.status === "storm_dead").length
|
|
2750
|
-
}
|
|
3605
|
+
},
|
|
3606
|
+
overmindLog
|
|
2751
3607
|
};
|
|
2752
3608
|
}
|
|
2753
|
-
var
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
3609
|
+
var {
|
|
3610
|
+
windowMs: TL_RECENT_TERMINAL_MS,
|
|
3611
|
+
maxTerminalLanes: TL_MAX_TERMINAL_LANES,
|
|
3612
|
+
maxAxisMs: TL_MAX_AXIS_MS
|
|
3613
|
+
} = resolveRoyWindows(process.env);
|
|
2757
3614
|
var ciCache = /* @__PURE__ */ new Map();
|
|
2758
3615
|
var CI_TTL = 2e4;
|
|
2759
3616
|
var CI_MOCK = (() => {
|
|
@@ -2769,50 +3626,18 @@ function mockCi(bucket) {
|
|
|
2769
3626
|
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
3627
|
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
3628
|
}
|
|
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
3629
|
async function refreshCI(prs) {
|
|
2805
3630
|
await Promise.all(prs.map(async (pr) => {
|
|
2806
3631
|
const ci = await ghChecks(pr);
|
|
2807
3632
|
ciCache.set(pr, { ci, at: Date.now() });
|
|
2808
3633
|
}));
|
|
2809
3634
|
}
|
|
2810
|
-
function timelineState(db, now = Date.now()) {
|
|
3635
|
+
function timelineState(db, now = Date.now(), overmindLog = []) {
|
|
2811
3636
|
const runs = db.listRuns().slice().sort((a, b) => a.id - b.id);
|
|
2812
3637
|
const doneNameBySlug = /* @__PURE__ */ new Map();
|
|
2813
3638
|
const activeBySlug = /* @__PURE__ */ new Map();
|
|
2814
3639
|
for (const r of runs) {
|
|
2815
|
-
if (!
|
|
3640
|
+
if (!isTerminalStatus(r.status)) activeBySlug.set(r.task_slug, { id: r.id, name: r.name });
|
|
2816
3641
|
if (r.status === "done") doneNameBySlug.set(r.task_slug, r.name);
|
|
2817
3642
|
}
|
|
2818
3643
|
const openQ = /* @__PURE__ */ new Map();
|
|
@@ -2822,9 +3647,9 @@ function timelineState(db, now = Date.now()) {
|
|
|
2822
3647
|
const lanes = [];
|
|
2823
3648
|
const reviewPRs = [];
|
|
2824
3649
|
for (const r of runs) {
|
|
2825
|
-
const live = !
|
|
3650
|
+
const live = !isTerminalStatus(r.status);
|
|
2826
3651
|
const recencyGated = !live || r.status === "stalled";
|
|
2827
|
-
if (recencyGated &&
|
|
3652
|
+
if (recencyGated && sqliteMs(r.updated_at) < cutoff) continue;
|
|
2828
3653
|
const events = db.events(r.id).map((e) => ({ kind: e.kind, text: e.text ?? null, at: sqliteIso(e.at) }));
|
|
2829
3654
|
const steps = events.filter((e) => e.kind === "step" && e.text).map((e) => ({ at: e.at, text: e.text }));
|
|
2830
3655
|
const lastStep = steps.length ? steps[steps.length - 1].text : null;
|
|
@@ -2864,7 +3689,10 @@ function timelineState(db, now = Date.now()) {
|
|
|
2864
3689
|
ci,
|
|
2865
3690
|
pr: r.pr_url,
|
|
2866
3691
|
dependsOn: deps,
|
|
2867
|
-
cost: r.cost_usd || 0
|
|
3692
|
+
cost: r.cost_usd || 0,
|
|
3693
|
+
role: r.kind,
|
|
3694
|
+
heroRunId: r.hero_run_id,
|
|
3695
|
+
reviewVerdict: r.review_verdict
|
|
2868
3696
|
});
|
|
2869
3697
|
}
|
|
2870
3698
|
const liveLanes = lanes.filter((l) => l.live);
|
|
@@ -2894,7 +3722,7 @@ function timelineState(db, now = Date.now()) {
|
|
|
2894
3722
|
const headline = pickHeadline(shown);
|
|
2895
3723
|
const stale = [...new Set(reviewPRs)].filter((pr) => now - (ciCache.get(pr)?.at ?? 0) > CI_TTL);
|
|
2896
3724
|
if (stale.length) void refreshCI(stale);
|
|
2897
|
-
return { now: nowIso, axisStart, axisEnd: nowIso, counters: c, headline, lanes: shown };
|
|
3725
|
+
return { now: nowIso, axisStart, axisEnd: nowIso, counters: c, headline, lanes: shown, overmindLog };
|
|
2898
3726
|
}
|
|
2899
3727
|
var CSS = `
|
|
2900
3728
|
:root {
|
|
@@ -3032,7 +3860,7 @@ main { max-width: 1080px; margin: 0 auto; width: 100%; }
|
|
|
3032
3860
|
.pill .sd { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3033
3861
|
.pill.run { color: var(--primary); background: var(--primary-soft); } .pill.run .sd { animation: pulse 1.8s infinite; }
|
|
3034
3862
|
.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); }
|
|
3863
|
+
.pill.blocked, .pill.paused, .pill.storm { color: var(--warning); background: color-mix(in oklch, var(--warning) 15%, transparent); }
|
|
3036
3864
|
.pill.done { color: var(--success); background: color-mix(in oklch, var(--success) 13%, transparent); }
|
|
3037
3865
|
.pill.stalled, .pill.failed { color: var(--destructive); background: color-mix(in oklch, var(--destructive) 14%, transparent); }
|
|
3038
3866
|
.pill.stopped, .pill.queued, .pill.superseded { color: var(--muted-foreground); background: var(--secondary); }
|
|
@@ -3325,9 +4153,11 @@ var GLYPH = {
|
|
|
3325
4153
|
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
4154
|
};
|
|
3327
4155
|
var GK = ['claw','spore','eye','hex'];
|
|
3328
|
-
|
|
4156
|
+
/* \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/
|
|
4157
|
+
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). */
|
|
4158
|
+
var ST = ${jsonForScript(statusTuples())};
|
|
3329
4159
|
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 = {
|
|
4160
|
+
var TERM = ${jsonForScript(Object.fromEntries(TERMINAL_STATUSES.map((s) => [s, 1])))};
|
|
3331
4161
|
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
4162
|
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
4163
|
|
|
@@ -3829,14 +4659,15 @@ async function runDiff(db, id, now = Date.now()) {
|
|
|
3829
4659
|
diffCache.set(id, { at: now, payload });
|
|
3830
4660
|
return payload;
|
|
3831
4661
|
}
|
|
3832
|
-
|
|
4662
|
+
var readNarratorLog = (log) => log?.() ?? [];
|
|
4663
|
+
function eventsStream(db, narratorLog) {
|
|
3833
4664
|
let iv;
|
|
3834
4665
|
let last = "";
|
|
3835
4666
|
const stream = new ReadableStream({
|
|
3836
4667
|
start(controller) {
|
|
3837
4668
|
const enc = new TextEncoder();
|
|
3838
4669
|
const send = () => {
|
|
3839
|
-
const snap = JSON.stringify(roySnapshot(db));
|
|
4670
|
+
const snap = JSON.stringify(roySnapshot(db, Date.now(), readNarratorLog(narratorLog)));
|
|
3840
4671
|
if (snap === last) return;
|
|
3841
4672
|
last = snap;
|
|
3842
4673
|
try {
|
|
@@ -3870,11 +4701,11 @@ function makeHandler(deps) {
|
|
|
3870
4701
|
const p = url.pathname;
|
|
3871
4702
|
if (p === "/") {
|
|
3872
4703
|
const store = auth();
|
|
3873
|
-
return html(store ? renderDashboard(store.email, roySnapshot(deps.db), taskUrlBase) : renderLogin());
|
|
4704
|
+
return html(store ? renderDashboard(store.email, roySnapshot(deps.db, Date.now(), readNarratorLog(deps.narratorLog)), taskUrlBase) : renderLogin());
|
|
3874
4705
|
}
|
|
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));
|
|
4706
|
+
if (p === "/events") return eventsStream(deps.db, deps.narratorLog);
|
|
4707
|
+
if (p === "/state") return Response.json(roySnapshot(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
|
|
4708
|
+
if (p === "/timeline") return Response.json(timelineState(deps.db, Date.now(), readNarratorLog(deps.narratorLog)));
|
|
3878
4709
|
const diffMatch = p.match(/^\/run\/(\d+)\/diff$/);
|
|
3879
4710
|
if (diffMatch) {
|
|
3880
4711
|
if (!auth()) return unauthorized();
|
|
@@ -3952,9 +4783,31 @@ function makeHandler(deps) {
|
|
|
3952
4783
|
function startWeb(deps, port = WEB_PORT) {
|
|
3953
4784
|
const handle = makeHandler(deps);
|
|
3954
4785
|
const server = serve({ fetch: handle, port, hostname: "127.0.0.1" });
|
|
4786
|
+
server.on("error", (err) => {
|
|
4787
|
+
const e = err;
|
|
4788
|
+
if (e.code === "EADDRINUSE") {
|
|
4789
|
+
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}).`);
|
|
4790
|
+
} else {
|
|
4791
|
+
console.error(`\u0432\u0435\u0431-\u043C\u043E\u0440\u0434\u0430 \u043D\u0435 \u043F\u043E\u0434\u043D\u044F\u043B\u0430\u0441\u044C: ${e.message}`);
|
|
4792
|
+
}
|
|
4793
|
+
process.exit(1);
|
|
4794
|
+
});
|
|
3955
4795
|
return { port, server };
|
|
3956
4796
|
}
|
|
3957
4797
|
|
|
4798
|
+
// src/narration.ts
|
|
4799
|
+
var RING_LIMIT = 20;
|
|
4800
|
+
function makeOvermindNarrator() {
|
|
4801
|
+
let ring = [];
|
|
4802
|
+
return {
|
|
4803
|
+
narrate(text) {
|
|
4804
|
+
ring.push({ at: (/* @__PURE__ */ new Date()).toISOString(), text });
|
|
4805
|
+
if (ring.length > RING_LIMIT) ring = ring.slice(-RING_LIMIT);
|
|
4806
|
+
},
|
|
4807
|
+
log: () => ring.slice()
|
|
4808
|
+
};
|
|
4809
|
+
}
|
|
4810
|
+
|
|
3958
4811
|
// src/probe.ts
|
|
3959
4812
|
import { spawn as spawn2 } from "child_process";
|
|
3960
4813
|
function anthropicHealthy(opts = {}) {
|
|
@@ -3972,7 +4825,7 @@ function anthropicHealthy(opts = {}) {
|
|
|
3972
4825
|
}
|
|
3973
4826
|
resolve3(ok);
|
|
3974
4827
|
};
|
|
3975
|
-
const child = spawn2(bin, ["-p", "ok", "--max-turns", "1"], { stdio: "ignore" });
|
|
4828
|
+
const child = spawn2(bin, [...ISOLATION_FLAGS, "-p", "ok", "--max-turns", "1"], { stdio: "ignore" });
|
|
3976
4829
|
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
3977
4830
|
child.on("error", () => finish(false));
|
|
3978
4831
|
child.on("exit", (code) => finish(code === 0));
|
|
@@ -3998,8 +4851,161 @@ async function fetchExternalStorm(url, opts = {}) {
|
|
|
3998
4851
|
}
|
|
3999
4852
|
}
|
|
4000
4853
|
|
|
4854
|
+
// src/anomalies.ts
|
|
4855
|
+
init_src();
|
|
4856
|
+
var AnomalyTracker = class {
|
|
4857
|
+
constructor(renotifyCooldownMs) {
|
|
4858
|
+
this.renotifyCooldownMs = renotifyCooldownMs;
|
|
4859
|
+
}
|
|
4860
|
+
renotifyCooldownMs;
|
|
4861
|
+
since = /* @__PURE__ */ new Map();
|
|
4862
|
+
lastFiredAt = /* @__PURE__ */ new Map();
|
|
4863
|
+
/** Кандидаты, готовые к сигналу (отлежали minDurationMs, не в кулдауне) — НИЧЕГО не коммитит. */
|
|
4864
|
+
eligible(now, candidates) {
|
|
4865
|
+
const result = [];
|
|
4866
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4867
|
+
for (const c of candidates) {
|
|
4868
|
+
seen.add(c.key);
|
|
4869
|
+
if (!this.since.has(c.key)) this.since.set(c.key, now);
|
|
4870
|
+
const startedAt = this.since.get(c.key);
|
|
4871
|
+
if (now - startedAt < c.minDurationMs) continue;
|
|
4872
|
+
const lastFired = this.lastFiredAt.get(c.key);
|
|
4873
|
+
if (lastFired != null && now - lastFired < this.renotifyCooldownMs) continue;
|
|
4874
|
+
result.push(c);
|
|
4875
|
+
}
|
|
4876
|
+
for (const k of [...this.since.keys()]) if (!seen.has(k)) this.since.delete(k);
|
|
4877
|
+
for (const k of [...this.lastFiredAt.keys()]) if (!seen.has(k)) this.lastFiredAt.delete(k);
|
|
4878
|
+
return result;
|
|
4879
|
+
}
|
|
4880
|
+
/** Доставка signal с этим key успешно ушла в этот момент — начинает кулдаун повтора. */
|
|
4881
|
+
commit(key, now) {
|
|
4882
|
+
this.lastFiredAt.set(key, now);
|
|
4883
|
+
}
|
|
4884
|
+
};
|
|
4885
|
+
var REVIEW_LIKE_STATUS = "review";
|
|
4886
|
+
var MAX_BODY_CHARS = 1800;
|
|
4887
|
+
function ageMs(now, iso) {
|
|
4888
|
+
return now - sqliteMs(iso);
|
|
4889
|
+
}
|
|
4890
|
+
function runLabel(run) {
|
|
4891
|
+
return run.name || `#${run.id}`;
|
|
4892
|
+
}
|
|
4893
|
+
function hrefOf(url) {
|
|
4894
|
+
return url ? { href: url } : {};
|
|
4895
|
+
}
|
|
4896
|
+
function joinCapped(labels) {
|
|
4897
|
+
let joined = labels.join(", ");
|
|
4898
|
+
if (joined.length <= MAX_BODY_CHARS) return joined;
|
|
4899
|
+
let cut = 0;
|
|
4900
|
+
let len = 0;
|
|
4901
|
+
for (; cut < labels.length; cut++) {
|
|
4902
|
+
len += labels[cut].length + 2;
|
|
4903
|
+
if (len > MAX_BODY_CHARS) break;
|
|
4904
|
+
}
|
|
4905
|
+
joined = labels.slice(0, Math.max(cut, 1)).join(", ");
|
|
4906
|
+
return `${joined} \u0438 \u0435\u0449\u0451 ${labels.length - Math.max(cut, 1)}`;
|
|
4907
|
+
}
|
|
4908
|
+
function buildAnomalyCandidates(db, now, thresholds, opts) {
|
|
4909
|
+
const candidates = [];
|
|
4910
|
+
const allRuns = db.listRuns();
|
|
4911
|
+
const recentFailed = allRuns.filter(
|
|
4912
|
+
(r) => r.status === "failed" && ageMs(now, r.updated_at) <= thresholds.failedSpikeWindowMs
|
|
4913
|
+
);
|
|
4914
|
+
if (recentFailed.length >= thresholds.failedSpikeCount) {
|
|
4915
|
+
candidates.push({
|
|
4916
|
+
key: "failed_spike",
|
|
4917
|
+
minDurationMs: 0,
|
|
4918
|
+
build: () => ({
|
|
4919
|
+
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`,
|
|
4920
|
+
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.`
|
|
4921
|
+
})
|
|
4922
|
+
});
|
|
4923
|
+
}
|
|
4924
|
+
for (const run of allRuns) {
|
|
4925
|
+
if (run.status !== "stalled") continue;
|
|
4926
|
+
candidates.push({
|
|
4927
|
+
key: `run_stalled:${run.id}`,
|
|
4928
|
+
minDurationMs: 0,
|
|
4929
|
+
// само по себе попадание в stalled УЖЕ прошло свой STALL_MS/RESPAWN_LIMIT
|
|
4930
|
+
build: () => ({
|
|
4931
|
+
runId: run.id,
|
|
4932
|
+
title: `\u0414\u0440\u043E\u043D ${runLabel(run)} \u0437\u0430\u0432\u0438\u0441`,
|
|
4933
|
+
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.`,
|
|
4934
|
+
...hrefOf(run.task_url)
|
|
4935
|
+
})
|
|
4936
|
+
});
|
|
4937
|
+
}
|
|
4938
|
+
const queuedCount = allRuns.filter((r) => r.status === "queued").length;
|
|
4939
|
+
const activeDroneCount = allRuns.filter((r) => r.kind !== "hero" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
|
|
4940
|
+
if (queuedCount > 0 && activeDroneCount === 0) {
|
|
4941
|
+
candidates.push({
|
|
4942
|
+
key: "queue_stalled",
|
|
4943
|
+
minDurationMs: thresholds.queueStalledMs,
|
|
4944
|
+
build: () => ({
|
|
4945
|
+
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`,
|
|
4946
|
+
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.`
|
|
4947
|
+
})
|
|
4948
|
+
});
|
|
4949
|
+
}
|
|
4950
|
+
for (const run of allRuns) {
|
|
4951
|
+
if (run.status !== REVIEW_LIKE_STATUS) continue;
|
|
4952
|
+
if (run.resolves_run_id != null || run.reviews_run_id != null) continue;
|
|
4953
|
+
if (!run.pr_url) continue;
|
|
4954
|
+
if (run.review_verdict === "approve") {
|
|
4955
|
+
candidates.push({
|
|
4956
|
+
key: `pr_stuck_merge:${run.id}`,
|
|
4957
|
+
minDurationMs: thresholds.prStuckMergeMs,
|
|
4958
|
+
build: () => ({
|
|
4959
|
+
runId: run.id,
|
|
4960
|
+
title: `PR ${runLabel(run)} \u043E\u0434\u043E\u0431\u0440\u0435\u043D, \u043D\u043E \u043D\u0435 \u0441\u043C\u0435\u0440\u0436\u0435\u043D`,
|
|
4961
|
+
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}.`,
|
|
4962
|
+
...hrefOf(run.task_url)
|
|
4963
|
+
})
|
|
4964
|
+
});
|
|
4965
|
+
} else if (opts.autoReviewEnabled && run.review_verdict == null && db.reviewerCountFor(run.id) === 0) {
|
|
4966
|
+
candidates.push({
|
|
4967
|
+
key: `pr_no_reviewer:${run.id}`,
|
|
4968
|
+
minDurationMs: thresholds.prNoReviewerMs,
|
|
4969
|
+
build: () => ({
|
|
4970
|
+
runId: run.id,
|
|
4971
|
+
title: `PR ${runLabel(run)} \u0431\u0435\u0437 \u0440\u0435\u0432\u044C\u044E\u0435\u0440\u0430`,
|
|
4972
|
+
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}.`,
|
|
4973
|
+
...hrefOf(run.task_url)
|
|
4974
|
+
})
|
|
4975
|
+
});
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
const runsById = new Map(allRuns.map((r) => [r.id, r]));
|
|
4979
|
+
for (const q of db.openQuestions()) {
|
|
4980
|
+
const run = runsById.get(q.run_id);
|
|
4981
|
+
candidates.push({
|
|
4982
|
+
key: `question_unanswered:${q.id}`,
|
|
4983
|
+
minDurationMs: thresholds.questionUnansweredMs,
|
|
4984
|
+
build: () => ({
|
|
4985
|
+
runId: q.run_id,
|
|
4986
|
+
title: `${run ? runLabel(run) : `\u043F\u0440\u043E\u0433\u043E\u043D #${q.run_id}`} \u0436\u0434\u0451\u0442 \u043E\u0442\u0432\u0435\u0442\u0430`,
|
|
4987
|
+
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.`,
|
|
4988
|
+
...hrefOf(run?.task_url)
|
|
4989
|
+
})
|
|
4990
|
+
});
|
|
4991
|
+
}
|
|
4992
|
+
if (opts.outboxPendingCount != null && opts.outboxPendingCount > 0) {
|
|
4993
|
+
const pending2 = opts.outboxPendingCount;
|
|
4994
|
+
candidates.push({
|
|
4995
|
+
key: "outbox_stuck",
|
|
4996
|
+
minDurationMs: thresholds.outboxStuckMs,
|
|
4997
|
+
build: () => ({
|
|
4998
|
+
title: `Outbox \u0440\u043E\u044F \u043D\u0435 \u043F\u0443\u0441\u0442\u0435\u0435\u0442`,
|
|
4999
|
+
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.`
|
|
5000
|
+
})
|
|
5001
|
+
});
|
|
5002
|
+
}
|
|
5003
|
+
return candidates;
|
|
5004
|
+
}
|
|
5005
|
+
|
|
4001
5006
|
// src/overmind.ts
|
|
4002
5007
|
init_storm();
|
|
5008
|
+
init_src();
|
|
4003
5009
|
var THIS_FILE = fileURLToPath(import.meta.url);
|
|
4004
5010
|
var OVERMIND_ENTRY = THIS_FILE;
|
|
4005
5011
|
var STALL_MS = Number(process.env.SWARM_STALL_MS) || 10 * 60 * 1e3;
|
|
@@ -4007,6 +5013,11 @@ var RESPAWN_LIMIT = Number(process.env.SWARM_RESPAWN_LIMIT) || 2;
|
|
|
4007
5013
|
var RESPAWN_YOUNG_MS = Number(process.env.SWARM_RESPAWN_YOUNG_MS) || 60 * 1e3;
|
|
4008
5014
|
var CONFLICT_RESOLVE_COOLDOWN_MS = Number(process.env.SWARM_CONFLICT_COOLDOWN_MS) || 3 * 60 * 1e3;
|
|
4009
5015
|
var RESOLVE_LIMIT = Number(process.env.SWARM_RESOLVE_LIMIT) || 3;
|
|
5016
|
+
var RESOLVE_ESCALATION_MARK = "\u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442 \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0451\u043D";
|
|
5017
|
+
var CI_WAKE_LIMIT = Number(process.env.SWARM_CI_WAKE_LIMIT) || 3;
|
|
5018
|
+
var CI_WAKE_COOLDOWN_MS = Number(process.env.SWARM_CI_WAKE_COOLDOWN_MS) || 3 * 60 * 1e3;
|
|
5019
|
+
var CI_CHECK_COOLDOWN_MS = Number(process.env.SWARM_CI_CHECK_COOLDOWN_MS) || 60 * 1e3;
|
|
5020
|
+
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";
|
|
4010
5021
|
function pidAlive(pid) {
|
|
4011
5022
|
try {
|
|
4012
5023
|
process.kill(pid, 0);
|
|
@@ -4015,30 +5026,107 @@ function pidAlive(pid) {
|
|
|
4015
5026
|
return e.code === "EPERM";
|
|
4016
5027
|
}
|
|
4017
5028
|
}
|
|
5029
|
+
function psLines(extraArgs) {
|
|
5030
|
+
try {
|
|
5031
|
+
return execFileSync2("ps", ["-A", "-ww", ...extraArgs], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }).split(
|
|
5032
|
+
"\n"
|
|
5033
|
+
);
|
|
5034
|
+
} catch (e) {
|
|
5035
|
+
logTransientSpawnError("ps", e);
|
|
5036
|
+
return [];
|
|
5037
|
+
}
|
|
5038
|
+
}
|
|
5039
|
+
function parseLivePpids(lines) {
|
|
5040
|
+
const set = /* @__PURE__ */ new Set();
|
|
5041
|
+
for (const line of lines) {
|
|
5042
|
+
const m = line.trim().match(/^(\d+)\s+(\S+)$/);
|
|
5043
|
+
if (!m || m[2].includes("Z")) continue;
|
|
5044
|
+
const ppid = Number(m[1]);
|
|
5045
|
+
if (ppid > 0) set.add(ppid);
|
|
5046
|
+
}
|
|
5047
|
+
return set;
|
|
5048
|
+
}
|
|
5049
|
+
function livePpids() {
|
|
5050
|
+
return parseLivePpids(psLines(["-o", "ppid=,stat="]));
|
|
5051
|
+
}
|
|
5052
|
+
function hasLiveDescendant(pid) {
|
|
5053
|
+
return livePpids().has(pid);
|
|
5054
|
+
}
|
|
5055
|
+
function mcpConfigPath(args) {
|
|
5056
|
+
const m = args.match(/--mcp-config\s+(\S+\/\.swarm-mcp\.json)/);
|
|
5057
|
+
return m ? m[1] : null;
|
|
5058
|
+
}
|
|
5059
|
+
function listDroneProcesses() {
|
|
5060
|
+
const rows = [];
|
|
5061
|
+
for (const line of psLines(["-o", "pid=,args="])) {
|
|
5062
|
+
const m = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
5063
|
+
if (!m) continue;
|
|
5064
|
+
const cfg = mcpConfigPath(m[2]);
|
|
5065
|
+
if (!cfg) continue;
|
|
5066
|
+
rows.push({ pid: Number(m[1]), worktree: dirname2(cfg) });
|
|
5067
|
+
}
|
|
5068
|
+
return rows;
|
|
5069
|
+
}
|
|
4018
5070
|
function lastActivityMs(db, run) {
|
|
4019
5071
|
const evs = db.events(run.id);
|
|
4020
|
-
const
|
|
4021
|
-
return
|
|
5072
|
+
const last = (evs.length ? evs[evs.length - 1].at : null) ?? run.updated_at;
|
|
5073
|
+
return sqliteMs(last);
|
|
4022
5074
|
}
|
|
4023
|
-
async function livenessMs(deps, run) {
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
5075
|
+
async function livenessMs(deps, run, now, liveChildPpids) {
|
|
5076
|
+
if (run.pid != null && liveChildPpids?.has(run.pid)) return now;
|
|
5077
|
+
let ms = lastActivityMs(deps.db, run);
|
|
5078
|
+
if (!run.worktree_path) return ms;
|
|
5079
|
+
if (deps.lastCommitMs) {
|
|
5080
|
+
const commitMs = await deps.lastCommitMs(run.worktree_path).catch(() => null);
|
|
5081
|
+
if (commitMs != null) ms = Math.max(ms, commitMs);
|
|
5082
|
+
}
|
|
5083
|
+
if (deps.transcriptMtimeMs) {
|
|
5084
|
+
const transcriptMs = deps.transcriptMtimeMs(run.worktree_path);
|
|
5085
|
+
if (transcriptMs != null) ms = Math.max(ms, transcriptMs);
|
|
5086
|
+
}
|
|
5087
|
+
return ms;
|
|
4028
5088
|
}
|
|
4029
5089
|
function lastConflictAttemptMs(db, runId) {
|
|
4030
5090
|
const evs = db.events(runId).filter((e) => e.kind === "conflict");
|
|
4031
5091
|
if (!evs.length) return null;
|
|
4032
5092
|
return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
|
|
4033
5093
|
}
|
|
5094
|
+
function lastCiCheckMs(db, runId) {
|
|
5095
|
+
const evs = db.events(runId).filter((e) => e.kind === "ci_wake" || e.kind === "ci_checked");
|
|
5096
|
+
if (!evs.length) return null;
|
|
5097
|
+
return Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z");
|
|
5098
|
+
}
|
|
5099
|
+
function ciWakeStats(db, runId) {
|
|
5100
|
+
const evs = db.events(runId).filter((e) => e.kind === "ci_wake");
|
|
5101
|
+
return {
|
|
5102
|
+
count: evs.length,
|
|
5103
|
+
lastMs: evs.length ? Date.parse(evs[evs.length - 1].at.replace(" ", "T") + "Z") : null,
|
|
5104
|
+
escalated: evs.some((e) => (e.text ?? "").includes(CI_WAKE_ESCALATION_MARK))
|
|
5105
|
+
};
|
|
5106
|
+
}
|
|
4034
5107
|
var IN_FLIGHT_STATUSES = /* @__PURE__ */ new Set(["queued", "running", "blocked"]);
|
|
4035
5108
|
function countInFlight(db) {
|
|
4036
5109
|
return db.listRuns().filter((r) => r.kind !== "hero" && IN_FLIGHT_STATUSES.has(r.status)).length;
|
|
4037
5110
|
}
|
|
5111
|
+
function countActiveDrones(db) {
|
|
5112
|
+
return db.listRuns().filter((r) => r.kind !== "hero" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
|
|
5113
|
+
}
|
|
5114
|
+
function countActiveReviewers(db) {
|
|
5115
|
+
return db.listRuns().filter((r) => r.kind === "reviewer" && ACTIVE_DRONE_STATUSES.has(r.status)).length;
|
|
5116
|
+
}
|
|
4038
5117
|
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.";
|
|
5118
|
+
function buildCiWakeNudge(summary) {
|
|
5119
|
+
return [
|
|
5120
|
+
"\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):",
|
|
5121
|
+
"",
|
|
5122
|
+
summary,
|
|
5123
|
+
"",
|
|
5124
|
+
"\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."
|
|
5125
|
+
].join("\n");
|
|
5126
|
+
}
|
|
4039
5127
|
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
5128
|
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) {
|
|
5129
|
+
function buildPrompt(task, run, verifyMode = SWARM_VERIFY) {
|
|
4042
5130
|
const name = run.name || `#${run.id}`;
|
|
4043
5131
|
const head = task ? `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug}: ${task.title}
|
|
4044
5132
|
|
|
@@ -4060,13 +5148,16 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
4060
5148
|
"\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
5149
|
`\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
5150
|
"\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.",
|
|
5151
|
+
'\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
5152
|
"",
|
|
4064
5153
|
"\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
|
-
"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.",
|
|
5154
|
+
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
5155
|
"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 \
|
|
5156
|
+
"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.",
|
|
5157
|
+
`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.`,
|
|
5158
|
+
"\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
5159
|
"\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
|
|
5160
|
+
"\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
5161
|
];
|
|
4071
5162
|
if (run.decision) {
|
|
4072
5163
|
const d = run.decision;
|
|
@@ -4080,7 +5171,7 @@ ${task.description}` : `\u0417\u0430\u0434\u0430\u0447\u0430 ${run.task_slug} (\
|
|
|
4080
5171
|
var HERO_CHARTER_PATH = join7(dirname2(THIS_FILE), "..", "docs", "HERO.md");
|
|
4081
5172
|
function buildHeroPrompt(task, run) {
|
|
4082
5173
|
const name = run.name || `#${run.id}`;
|
|
4083
|
-
const charter =
|
|
5174
|
+
const charter = readFileSync8(HERO_CHARTER_PATH, "utf8");
|
|
4084
5175
|
const head = task ? `\u042D\u041F\u0418\u041A ${run.task_slug}: ${task.title}
|
|
4085
5176
|
|
|
4086
5177
|
${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).`;
|
|
@@ -4124,6 +5215,51 @@ function buildResolverPrompt(reviewRun, resolver, conflicts, branch, mainRef) {
|
|
|
4124
5215
|
`\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
5216
|
].join("\n");
|
|
4126
5217
|
}
|
|
5218
|
+
function buildReviewChecklist(reviewRun, files) {
|
|
5219
|
+
const header = files.length ? `\u0424\u0430\u0439\u043B\u044B \u0434\u0438\u0444\u0444\u0430 (${files.length}):
|
|
5220
|
+
${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.";
|
|
5221
|
+
return [
|
|
5222
|
+
`\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}` : ""}.`,
|
|
5223
|
+
"",
|
|
5224
|
+
header,
|
|
5225
|
+
"",
|
|
5226
|
+
"\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):",
|
|
5227
|
+
"- [ ] \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.",
|
|
5228
|
+
"- [ ] \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.",
|
|
5229
|
+
"- [ ] \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.",
|
|
5230
|
+
"- [ ] 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.",
|
|
5231
|
+
"- [ ] \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)."
|
|
5232
|
+
].join("\n");
|
|
5233
|
+
}
|
|
5234
|
+
function buildReviewerPrompt(reviewRun, reviewer, checklist, branch) {
|
|
5235
|
+
const name = reviewer.name || `#${reviewer.id}`;
|
|
5236
|
+
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)";
|
|
5237
|
+
return [
|
|
5238
|
+
`\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.`,
|
|
5239
|
+
"",
|
|
5240
|
+
HEADLESS_BLOCK,
|
|
5241
|
+
"",
|
|
5242
|
+
`\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.`,
|
|
5243
|
+
"",
|
|
5244
|
+
"\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.",
|
|
5245
|
+
"",
|
|
5246
|
+
checklist,
|
|
5247
|
+
"",
|
|
5248
|
+
"\u041F\u043E\u0440\u044F\u0434\u043E\u043A:",
|
|
5249
|
+
"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.",
|
|
5250
|
+
`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.`,
|
|
5251
|
+
`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\u0430\u043C\u0438 \u0432 REV-\u0437\u0430\u0434\u0430\u0447\u0443 \u0438 \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.`
|
|
5252
|
+
].join("\n");
|
|
5253
|
+
}
|
|
5254
|
+
function droneLabel(run) {
|
|
5255
|
+
return run.name || `#${run.id}`;
|
|
5256
|
+
}
|
|
5257
|
+
function narrate(deps, text) {
|
|
5258
|
+
try {
|
|
5259
|
+
deps.narrate?.(text);
|
|
5260
|
+
} catch {
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
4127
5263
|
async function manageStorm(deps, now, worktreeExists, hasTranscript) {
|
|
4128
5264
|
const db = deps.db;
|
|
4129
5265
|
const s = deps.storm;
|
|
@@ -4141,6 +5277,7 @@ async function manageStorm(deps, now, worktreeExists, hasTranscript) {
|
|
|
4141
5277
|
const consec = trailingProbeSuccesses(recs);
|
|
4142
5278
|
if (stormShouldEnd({ consecutiveProbeOk: consec, externalDown: lastExternalDown(recs) === true }, cfg)) {
|
|
4143
5279
|
db.endStorm(`${STORM_END_TEXT} (${consec} \u0443\u0441\u043F\u0435\u0448\u043D\u044B\u0435 health-probe \u043F\u043E\u0434\u0440\u044F\u0434)`);
|
|
5280
|
+
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
5281
|
await resurrectStormDead(deps, worktreeExists, hasTranscript);
|
|
4145
5282
|
return false;
|
|
4146
5283
|
}
|
|
@@ -4157,6 +5294,7 @@ async function manageStorm(deps, now, worktreeExists, hasTranscript) {
|
|
|
4157
5294
|
};
|
|
4158
5295
|
if (stormShouldStart(signals, cfg)) {
|
|
4159
5296
|
db.startStorm(stormStartReason(signals, cfg));
|
|
5297
|
+
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
5298
|
return true;
|
|
4161
5299
|
}
|
|
4162
5300
|
return false;
|
|
@@ -4172,31 +5310,107 @@ async function resurrectStormDead(deps, worktreeExists, hasTranscript) {
|
|
|
4172
5310
|
} catch (e) {
|
|
4173
5311
|
deps.db.updateRun(run.id, { status: "stalled" });
|
|
4174
5312
|
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)}`);
|
|
5313
|
+
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.`);
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5317
|
+
async function handleCiRed(deps, run, now, worktreeExists, hasTranscript, droneCap) {
|
|
5318
|
+
if (run.pid != null && deps.isAlive(run.pid)) return;
|
|
5319
|
+
if (deps.resourcesOk && !deps.resourcesOk()) return;
|
|
5320
|
+
if (countActiveDrones(deps.db) >= droneCap) return;
|
|
5321
|
+
const stats = ciWakeStats(deps.db, run.id);
|
|
5322
|
+
if (stats.count >= CI_WAKE_LIMIT) {
|
|
5323
|
+
if (!stats.escalated) {
|
|
5324
|
+
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`);
|
|
5325
|
+
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.`);
|
|
4175
5326
|
}
|
|
5327
|
+
return;
|
|
5328
|
+
}
|
|
5329
|
+
if (stats.lastMs != null && now - stats.lastMs < CI_WAKE_COOLDOWN_MS) return;
|
|
5330
|
+
const lastCheck = lastCiCheckMs(deps.db, run.id);
|
|
5331
|
+
if (lastCheck != null && now - lastCheck < CI_CHECK_COOLDOWN_MS) return;
|
|
5332
|
+
const wt = run.worktree_path;
|
|
5333
|
+
if (!wt || !worktreeExists(wt) || !hasTranscript(wt)) return;
|
|
5334
|
+
const ci = await deps.ci.checks(run.pr_url).catch(() => null);
|
|
5335
|
+
if (ci?.bucket !== "fail") {
|
|
5336
|
+
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`);
|
|
5337
|
+
return;
|
|
5338
|
+
}
|
|
5339
|
+
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)"}`;
|
|
5340
|
+
try {
|
|
5341
|
+
await deps.respawn(run, "continue", buildCiWakeNudge(summary));
|
|
5342
|
+
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`);
|
|
5343
|
+
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.`);
|
|
5344
|
+
} catch (e) {
|
|
5345
|
+
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)}`);
|
|
5346
|
+
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.`);
|
|
4176
5347
|
}
|
|
4177
5348
|
}
|
|
4178
5349
|
async function tickOnce(deps) {
|
|
4179
5350
|
const now = deps.now ? deps.now() : Date.now();
|
|
4180
|
-
const worktreeExists = deps.worktreeExists ??
|
|
5351
|
+
const worktreeExists = deps.worktreeExists ?? existsSync4;
|
|
4181
5352
|
const hasTranscript = deps.hasTranscript ?? sessionTranscriptExists;
|
|
5353
|
+
if (deps.rehydrate) {
|
|
5354
|
+
const rh = deps.rehydrate;
|
|
5355
|
+
const sinceLast = rh.lastAt == null ? Infinity : now - rh.lastAt;
|
|
5356
|
+
if (sinceLast >= rh.intervalMs && rh.ready()) {
|
|
5357
|
+
rh.lastAt = now;
|
|
5358
|
+
try {
|
|
5359
|
+
await rh.run();
|
|
5360
|
+
} catch (e) {
|
|
5361
|
+
console.error("[overmind] re-hydrate \u0437\u0435\u0440\u043A\u0430\u043B\u0430 \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F:", e);
|
|
5362
|
+
}
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
if (deps.heartbeat) {
|
|
5366
|
+
const hb = deps.heartbeat;
|
|
5367
|
+
const sinceLast = hb.lastAt == null ? Infinity : now - hb.lastAt;
|
|
5368
|
+
if (sinceLast >= hb.intervalMs) {
|
|
5369
|
+
hb.lastAt = now;
|
|
5370
|
+
await hb.push();
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
if (deps.rehydrate && deps.kill) {
|
|
5374
|
+
const kill = deps.kill;
|
|
5375
|
+
for (const run of deps.db.listRuns("stopped")) {
|
|
5376
|
+
if (run.pid == null || !deps.isAlive(run.pid)) continue;
|
|
5377
|
+
console.log(`[overmind] reap: \u043F\u0440\u043E\u0433\u043E\u043D ${run.name} (#${run.id}) \u043F\u043E\u043C\u0435\u0447\u0435\u043D stopped \u0438\u0437\u0432\u043D\u0435 \u2014 \u0433\u0430\u0448\u0443 pid ${run.pid}`);
|
|
5378
|
+
try {
|
|
5379
|
+
kill(run.pid);
|
|
5380
|
+
} catch {
|
|
5381
|
+
}
|
|
5382
|
+
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
5383
|
+
});
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
4182
5386
|
let stormActive = false;
|
|
4183
5387
|
if (deps.storm) {
|
|
4184
5388
|
for (const run of deps.db.listRuns("running")) {
|
|
4185
|
-
if (run.resolves_run_id == null && run.pid != null && !deps.isAlive(run.pid)) {
|
|
5389
|
+
if (run.resolves_run_id == null && run.reviews_run_id == null && run.pid != null && !deps.isAlive(run.pid)) {
|
|
4186
5390
|
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
5391
|
}
|
|
4188
5392
|
}
|
|
4189
5393
|
stormActive = await manageStorm(deps, now, worktreeExists, hasTranscript);
|
|
4190
5394
|
}
|
|
5395
|
+
const droneCap = deps.maxDrones ? await deps.maxDrones() : Infinity;
|
|
4191
5396
|
if (!stormActive) {
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
deps.db
|
|
5397
|
+
const canSpawn = !deps.resourcesOk || deps.resourcesOk();
|
|
5398
|
+
const queued = deps.db.listRuns("queued");
|
|
5399
|
+
if (!canSpawn) {
|
|
5400
|
+
if (queued.some((r) => deps.db.unmetDeps(r.depends_on).length === 0))
|
|
5401
|
+
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-130)");
|
|
5402
|
+
} else {
|
|
5403
|
+
for (const run of queued) {
|
|
5404
|
+
if (countActiveDrones(deps.db) >= droneCap) break;
|
|
5405
|
+
if (deps.db.unmetDeps(run.depends_on).length > 0) continue;
|
|
5406
|
+
try {
|
|
5407
|
+
await deps.spawn(run);
|
|
5408
|
+
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)}.`);
|
|
5409
|
+
} catch (e) {
|
|
5410
|
+
if (e instanceof AreaBusyError) continue;
|
|
5411
|
+
deps.db.updateRun(run.id, { status: "failed" });
|
|
5412
|
+
deps.db.addEvent(run.id, "failed", String(e));
|
|
5413
|
+
}
|
|
4200
5414
|
}
|
|
4201
5415
|
}
|
|
4202
5416
|
}
|
|
@@ -4210,6 +5424,16 @@ async function tickOnce(deps) {
|
|
|
4210
5424
|
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
5425
|
continue;
|
|
4212
5426
|
}
|
|
5427
|
+
if (run.reviews_run_id != null) {
|
|
5428
|
+
await deps.finishTask(run.task_slug).catch(() => {
|
|
5429
|
+
});
|
|
5430
|
+
if (run.worktree_path) await deps.removeWorktree(run.repo, run.worktree_path).catch(() => {
|
|
5431
|
+
});
|
|
5432
|
+
deps.db.updateRun(run.id, { status: "done" });
|
|
5433
|
+
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");
|
|
5434
|
+
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.`);
|
|
5435
|
+
continue;
|
|
5436
|
+
}
|
|
4213
5437
|
if (!run.pr_url) continue;
|
|
4214
5438
|
const st = await deps.prStatus(run.pr_url);
|
|
4215
5439
|
if (st.merged) {
|
|
@@ -4219,11 +5443,19 @@ async function tickOnce(deps) {
|
|
|
4219
5443
|
deps.db.addEvent(run.id, "merged", run.pr_url);
|
|
4220
5444
|
deps.db.updateRun(run.id, { status: "done" });
|
|
4221
5445
|
deps.db.addEvent(run.id, "done");
|
|
5446
|
+
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
5447
|
continue;
|
|
4223
5448
|
}
|
|
4224
5449
|
if (st.conflicting && deps.resolveConflict && !stormActive) {
|
|
5450
|
+
if (deps.resourcesOk && !deps.resourcesOk()) continue;
|
|
5451
|
+
if (countActiveDrones(deps.db) >= droneCap) continue;
|
|
4225
5452
|
if (deps.db.activeResolverFor(run.id)) continue;
|
|
4226
|
-
if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT)
|
|
5453
|
+
if (deps.db.resolverCountFor(run.id) >= RESOLVE_LIMIT) {
|
|
5454
|
+
if (!deps.db.events(run.id).some((e) => e.kind === "conflict" && (e.text ?? "").includes(RESOLVE_ESCALATION_MARK))) {
|
|
5455
|
+
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`);
|
|
5456
|
+
}
|
|
5457
|
+
continue;
|
|
5458
|
+
}
|
|
4227
5459
|
const last = lastConflictAttemptMs(deps.db, run.id);
|
|
4228
5460
|
if (last != null && now - last < CONFLICT_RESOLVE_COOLDOWN_MS) continue;
|
|
4229
5461
|
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 +5464,42 @@ async function tickOnce(deps) {
|
|
|
4232
5464
|
} catch (e) {
|
|
4233
5465
|
deps.db.addEvent(run.id, "conflict", `merge-main-in \u0441\u043E\u0440\u0432\u0430\u043B\u0441\u044F: ${String(e)}`);
|
|
4234
5466
|
}
|
|
5467
|
+
} else if (!st.conflicting && deps.ci && deps.respawn && !stormActive) {
|
|
5468
|
+
await handleCiRed(deps, run, now, worktreeExists, hasTranscript, droneCap);
|
|
5469
|
+
}
|
|
5470
|
+
if (!st.conflicting && !st.isDraft && deps.spawnReviewer && !stormActive && (!deps.reviewCutoffAt || run.updated_at >= deps.reviewCutoffAt)) {
|
|
5471
|
+
const reviewersCapOk = deps.reviewCap === void 0 || countActiveReviewers(deps.db) < deps.reviewCap;
|
|
5472
|
+
const leavesSlotForDrones = countActiveDrones(deps.db) < droneCap - 1;
|
|
5473
|
+
if ((!deps.resourcesOk || deps.resourcesOk()) && reviewersCapOk && leavesSlotForDrones && deps.db.reviewerCountFor(run.id) === 0) {
|
|
5474
|
+
try {
|
|
5475
|
+
await deps.spawnReviewer(run, st.files ?? []);
|
|
5476
|
+
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.`);
|
|
5477
|
+
} catch (e) {
|
|
5478
|
+
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)}`);
|
|
5479
|
+
}
|
|
5480
|
+
}
|
|
4235
5481
|
}
|
|
4236
5482
|
} catch (e) {
|
|
4237
5483
|
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
5484
|
}
|
|
4239
5485
|
}
|
|
5486
|
+
let liveChildPpids;
|
|
5487
|
+
try {
|
|
5488
|
+
liveChildPpids = deps.livePpids?.() ?? null;
|
|
5489
|
+
} catch (e) {
|
|
5490
|
+
liveChildPpids = null;
|
|
5491
|
+
logTransientSpawnError("ps (livePpids)", e);
|
|
5492
|
+
}
|
|
4240
5493
|
for (const run of [...deps.db.listRuns("running"), ...deps.db.listRuns("blocked")]) {
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
5494
|
+
const alive = run.pid != null && deps.isAlive(run.pid);
|
|
5495
|
+
if (!alive && deps.accountCost) {
|
|
5496
|
+
try {
|
|
5497
|
+
deps.accountCost(run);
|
|
5498
|
+
} catch (e) {
|
|
5499
|
+
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);
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
if (stormActive && run.resolves_run_id == null && run.reviews_run_id == null) {
|
|
4244
5503
|
if (!alive) {
|
|
4245
5504
|
deps.db.updateRun(run.id, { status: "storm_dead" });
|
|
4246
5505
|
deps.db.addEvent(run.id, "storm_dead", STORM_DEAD_TEXT);
|
|
@@ -4272,7 +5531,7 @@ async function tickOnce(deps) {
|
|
|
4272
5531
|
}
|
|
4273
5532
|
continue;
|
|
4274
5533
|
}
|
|
4275
|
-
if (!alive && run.status === "running" && run.resolves_run_id == null && !run.pr_url && deps.openPrsForBranch) {
|
|
5534
|
+
if (!alive && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && !run.pr_url && deps.openPrsForBranch) {
|
|
4276
5535
|
const branch = run.branch || run.task_slug;
|
|
4277
5536
|
const prs = await deps.openPrsForBranch(branch, run.repo).catch(() => []);
|
|
4278
5537
|
if (prs.length > 0) {
|
|
@@ -4283,7 +5542,7 @@ async function tickOnce(deps) {
|
|
|
4283
5542
|
continue;
|
|
4284
5543
|
}
|
|
4285
5544
|
}
|
|
4286
|
-
if (!alive && run.status === "running" && run.resolves_run_id == null && deps.respawn && run.respawns < RESPAWN_LIMIT) {
|
|
5545
|
+
if (!alive && run.status === "running" && run.resolves_run_id == null && run.reviews_run_id == null && deps.respawn && run.respawns < RESPAWN_LIMIT) {
|
|
4287
5546
|
const wt = run.worktree_path;
|
|
4288
5547
|
const wtAlive = !!wt && worktreeExists(wt);
|
|
4289
5548
|
const canContinue = wtAlive && hasTranscript(wt);
|
|
@@ -4296,11 +5555,16 @@ async function tickOnce(deps) {
|
|
|
4296
5555
|
"respawned",
|
|
4297
5556
|
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
5557
|
);
|
|
5558
|
+
narrate(
|
|
5559
|
+
deps,
|
|
5560
|
+
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.`
|
|
5561
|
+
);
|
|
4299
5562
|
try {
|
|
4300
5563
|
await deps.respawn(run, mode);
|
|
4301
5564
|
} catch (e) {
|
|
4302
5565
|
deps.db.updateRun(run.id, { status: "stalled" });
|
|
4303
5566
|
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)}`);
|
|
5567
|
+
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
5568
|
}
|
|
4305
5569
|
continue;
|
|
4306
5570
|
}
|
|
@@ -4308,8 +5572,10 @@ async function tickOnce(deps) {
|
|
|
4308
5572
|
let reason = null;
|
|
4309
5573
|
if (!alive) {
|
|
4310
5574
|
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
|
-
|
|
5575
|
+
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.`);
|
|
5576
|
+
} else if (run.status === "running" && now - await livenessMs(deps, run, now, liveChildPpids) > STALL_MS) {
|
|
4312
5577
|
reason = `\u0434\u0440\u043E\u043D \u0437\u0430\u0432\u0438\u0441: \u043C\u043E\u043B\u0447\u0438\u0442 > ${Math.round(STALL_MS / 6e4)} \u043C\u0438\u043D`;
|
|
5578
|
+
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
5579
|
}
|
|
4314
5580
|
if (!reason) continue;
|
|
4315
5581
|
deps.db.updateRun(run.id, { status: "stalled" });
|
|
@@ -4324,6 +5590,7 @@ async function tickOnce(deps) {
|
|
|
4324
5590
|
if (deps.respawn && !stormActive) {
|
|
4325
5591
|
for (const run of deps.db.listRuns("paused")) {
|
|
4326
5592
|
if (run.kind === "hero") continue;
|
|
5593
|
+
if (run.reviews_run_id != null) continue;
|
|
4327
5594
|
if (run.pid != null && deps.isAlive(run.pid)) continue;
|
|
4328
5595
|
const wt = run.worktree_path;
|
|
4329
5596
|
const canContinue = !!wt && worktreeExists(wt) && hasTranscript(wt);
|
|
@@ -4341,10 +5608,42 @@ async function tickOnce(deps) {
|
|
|
4341
5608
|
}
|
|
4342
5609
|
}
|
|
4343
5610
|
}
|
|
4344
|
-
if (deps.
|
|
5611
|
+
if (deps.anomalies) {
|
|
5612
|
+
const az = deps.anomalies;
|
|
5613
|
+
let eligible = [];
|
|
5614
|
+
try {
|
|
5615
|
+
const candidates = buildAnomalyCandidates(deps.db, now, az.thresholds, {
|
|
5616
|
+
autoReviewEnabled: az.autoReviewEnabled,
|
|
5617
|
+
...az.outboxPendingCount ? { outboxPendingCount: az.outboxPendingCount() } : {}
|
|
5618
|
+
});
|
|
5619
|
+
eligible = az.tracker.eligible(now, candidates);
|
|
5620
|
+
} catch (e) {
|
|
5621
|
+
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);
|
|
5622
|
+
}
|
|
5623
|
+
for (const candidate of eligible) {
|
|
5624
|
+
const signal = { key: candidate.key, ...candidate.build() };
|
|
5625
|
+
try {
|
|
5626
|
+
await az.notify(signal);
|
|
5627
|
+
} catch (e) {
|
|
5628
|
+
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);
|
|
5629
|
+
continue;
|
|
5630
|
+
}
|
|
5631
|
+
az.tracker.commit(candidate.key, now);
|
|
5632
|
+
if (signal.runId != null) {
|
|
5633
|
+
try {
|
|
5634
|
+
deps.db.addEvent(signal.runId, "anomaly", signal.body);
|
|
5635
|
+
} catch (e) {
|
|
5636
|
+
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);
|
|
5637
|
+
}
|
|
5638
|
+
}
|
|
5639
|
+
narrate(deps, `\u26A0\uFE0F ${signal.title}`);
|
|
5640
|
+
}
|
|
5641
|
+
}
|
|
5642
|
+
if (deps.intake && !stormActive && deps.intake.maxInFlight > 0 && (!deps.resourcesOk || deps.resourcesOk())) {
|
|
4345
5643
|
const it = deps.intake;
|
|
5644
|
+
const claimCap = deps.maxDrones ? droneCap : it.maxInFlight;
|
|
4346
5645
|
const sinceLastClaim = it.lastClaimAt == null ? Infinity : now - it.lastClaimAt;
|
|
4347
|
-
if (sinceLastClaim >= it.intervalMs && countInFlight(deps.db) <
|
|
5646
|
+
if (sinceLastClaim >= it.intervalMs && countInFlight(deps.db) < claimCap) {
|
|
4348
5647
|
it.lastClaimAt = now;
|
|
4349
5648
|
try {
|
|
4350
5649
|
await it.claim();
|
|
@@ -4427,21 +5726,53 @@ async function resolveConflict(deps, reviewRun) {
|
|
|
4427
5726
|
await deps.git.removeWorktree(repo, wt).catch(() => {
|
|
4428
5727
|
});
|
|
4429
5728
|
deps.db.updateRun(resolver.id, { status: "failed" });
|
|
4430
|
-
|
|
5729
|
+
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";
|
|
5730
|
+
deps.db.addEvent(resolver.id, "failed", `${marker}: ${String(e)}`);
|
|
5731
|
+
throw e;
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
async function spawnReviewer(deps, reviewRun, files) {
|
|
5735
|
+
const repo = normalizeRepo(reviewRun.repo || deps.repoFallback);
|
|
5736
|
+
const branch = reviewRun.branch || reviewRun.task_slug;
|
|
5737
|
+
const checklist = buildReviewChecklist(reviewRun, files);
|
|
5738
|
+
const rev = deps.taskless ? await deps.taskless.createTask({
|
|
5739
|
+
title: `REV: ${reviewRun.task_slug} \u2014 ${reviewRun.task_title ?? "\u0430\u0432\u0442\u043E-\u0440\u0435\u0432\u044C\u044E PR"}`,
|
|
5740
|
+
description: checklist
|
|
5741
|
+
}).catch(() => null) : null;
|
|
5742
|
+
const reviewer = deps.db.createRun({
|
|
5743
|
+
task_slug: rev?.slug ?? `${reviewRun.task_slug}-review`,
|
|
5744
|
+
repo,
|
|
5745
|
+
kind: "reviewer",
|
|
5746
|
+
reviews_run_id: reviewRun.id
|
|
5747
|
+
});
|
|
5748
|
+
const wt = join7(deps.worktreesDir, basename3(repo), `${reviewRun.task_slug}-review-${reviewer.id}`);
|
|
5749
|
+
try {
|
|
5750
|
+
await deps.git.prepareDetachedWorktree(repo, branch, wt);
|
|
5751
|
+
await deps.git.setWorktreeIdentity(wt, reviewer.name);
|
|
5752
|
+
const prompt = buildReviewerPrompt(reviewRun, reviewer, checklist, branch);
|
|
5753
|
+
const pid = deps.launch({ worktree: wt, run: reviewer, prompt });
|
|
5754
|
+
deps.db.updateRun(reviewer.id, { status: "running", worktree_path: wt, branch, pid });
|
|
5755
|
+
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})${rev ? ` \u2014 REV-\u0437\u0430\u0434\u0430\u0447\u0430 ${rev.slug}` : ""}`);
|
|
5756
|
+
return deps.db.getRun(reviewer.id);
|
|
5757
|
+
} catch (e) {
|
|
5758
|
+
await deps.git.removeWorktree(repo, wt).catch(() => {
|
|
5759
|
+
});
|
|
5760
|
+
deps.db.updateRun(reviewer.id, { status: "failed" });
|
|
5761
|
+
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
5762
|
throw e;
|
|
4432
5763
|
}
|
|
4433
5764
|
}
|
|
4434
|
-
async function respawnRun(deps, run, mode) {
|
|
5765
|
+
async function respawnRun(deps, run, mode, nudge) {
|
|
4435
5766
|
const wt = run.worktree_path;
|
|
4436
5767
|
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
5768
|
const repo = normalizeRepo(run.repo || deps.repoFallback);
|
|
4438
5769
|
if (mode === "continue") {
|
|
4439
|
-
const
|
|
4440
|
-
const pid2 = deps.launch({ worktree: wt, run, prompt:
|
|
5770
|
+
const prompt2 = nudge ?? (run.kind === "hero" ? HERO_CONTINUE_NUDGE : CONTINUE_NUDGE);
|
|
5771
|
+
const pid2 = deps.launch({ worktree: wt, run, prompt: prompt2, continueSession: true });
|
|
4441
5772
|
deps.db.updateRun(run.id, { status: "running", pid: pid2 });
|
|
4442
5773
|
return;
|
|
4443
5774
|
}
|
|
4444
|
-
if (!
|
|
5775
|
+
if (!existsSync4(wt)) {
|
|
4445
5776
|
const branch = run.branch || run.task_slug;
|
|
4446
5777
|
const source = await addWorktree(repo, wt, branch);
|
|
4447
5778
|
await setWorktreeIdentity(wt, run.name);
|
|
@@ -4453,19 +5784,74 @@ async function respawnRun(deps, run, mode) {
|
|
|
4453
5784
|
const pid = deps.launch({ worktree: wt, run, prompt });
|
|
4454
5785
|
deps.db.updateRun(run.id, { status: "running", pid });
|
|
4455
5786
|
}
|
|
5787
|
+
async function syncCrashedOnStartup(deps) {
|
|
5788
|
+
let orphaned = 0;
|
|
5789
|
+
let merged = 0;
|
|
5790
|
+
for (const run of [...deps.db.listRuns("running"), ...deps.db.listRuns("blocked")]) {
|
|
5791
|
+
if (run.pid != null) continue;
|
|
5792
|
+
if (run.pr_url && deps.prMerged) {
|
|
5793
|
+
const isMerged = await deps.prMerged(run.pr_url).catch(() => false);
|
|
5794
|
+
if (isMerged) {
|
|
5795
|
+
if (deps.finishTask) await deps.finishTask(run.task_slug).catch(() => {
|
|
5796
|
+
});
|
|
5797
|
+
deps.db.updateRun(run.id, { status: "done" });
|
|
5798
|
+
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)");
|
|
5799
|
+
merged++;
|
|
5800
|
+
continue;
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
deps.db.updateRun(run.id, { status: "stalled" });
|
|
5804
|
+
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)");
|
|
5805
|
+
orphaned++;
|
|
5806
|
+
}
|
|
5807
|
+
return { orphaned, merged };
|
|
5808
|
+
}
|
|
4456
5809
|
async function makeStore(repo) {
|
|
4457
|
-
if (SWARM_BACKEND !== "client") return new SwarmDb(DB_PATH, repo);
|
|
5810
|
+
if (SWARM_BACKEND !== "client") return { store: new SwarmDb(DB_PATH, repo), reconciled: true };
|
|
4458
5811
|
const failing = async () => ({ isError: true });
|
|
4459
5812
|
const call = await connectSwarmMcp() ?? failing;
|
|
4460
5813
|
const client = new SwarmClient({ call, outboxPath: SWARM_OUTBOX, defaultRepo: repo });
|
|
4461
5814
|
await client.hydrate();
|
|
4462
|
-
|
|
4463
|
-
|
|
5815
|
+
const rec = client.reconcileRuntime({ worktreesDir: WORKTREES_DIR, listProcesses: listDroneProcesses });
|
|
5816
|
+
if (rec.reconciled) {
|
|
5817
|
+
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}`);
|
|
5818
|
+
} else {
|
|
5819
|
+
console.warn(
|
|
5820
|
+
`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`
|
|
5821
|
+
);
|
|
5822
|
+
}
|
|
5823
|
+
return { store: client, reconciled: rec.reconciled };
|
|
5824
|
+
}
|
|
5825
|
+
var crashGuardsInstalled = false;
|
|
5826
|
+
function installCrashGuards() {
|
|
5827
|
+
if (crashGuardsInstalled) return;
|
|
5828
|
+
crashGuardsInstalled = true;
|
|
5829
|
+
process.on("uncaughtException", (e) => {
|
|
5830
|
+
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);
|
|
5831
|
+
});
|
|
5832
|
+
process.on("unhandledRejection", (e) => {
|
|
5833
|
+
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);
|
|
5834
|
+
});
|
|
4464
5835
|
}
|
|
4465
5836
|
async function runDaemon(opts) {
|
|
4466
|
-
|
|
5837
|
+
installCrashGuards();
|
|
5838
|
+
const { store: db, reconciled } = await makeStore(opts.repo);
|
|
4467
5839
|
const tl = await connectTaskless();
|
|
4468
|
-
|
|
5840
|
+
if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
|
|
5841
|
+
if (reconciled) {
|
|
5842
|
+
const sync = await syncCrashedOnStartup({
|
|
5843
|
+
db,
|
|
5844
|
+
prMerged: (url) => prStatus(url).then((s) => s.merged),
|
|
5845
|
+
finishTask: (slug) => tl ? tl.finishTask(slug) : Promise.resolve()
|
|
5846
|
+
});
|
|
5847
|
+
if (sync.orphaned || sync.merged) {
|
|
5848
|
+
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`);
|
|
5849
|
+
}
|
|
5850
|
+
} else {
|
|
5851
|
+
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");
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
const launch = makeLauncher({ overmindEntry: THIS_FILE, dbPath: DB_PATH, runLogsDir: RUN_LOGS_DIR });
|
|
4469
5855
|
const baseSpawn = {
|
|
4470
5856
|
db,
|
|
4471
5857
|
repo: opts.repo,
|
|
@@ -4478,8 +5864,66 @@ async function runDaemon(opts) {
|
|
|
4478
5864
|
const client = db;
|
|
4479
5865
|
intake = { claim: () => client.claim(), maxInFlight: SWARM_MAX_IN_FLIGHT, intervalMs: SWARM_CLAIM_INTERVAL_MS };
|
|
4480
5866
|
}
|
|
5867
|
+
let rehydrate;
|
|
5868
|
+
if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_REHYDRATE_MS > 0) {
|
|
5869
|
+
const client = db;
|
|
5870
|
+
rehydrate = { run: () => client.hydrate(), ready: () => client.pendingCount() === 0, intervalMs: SWARM_REHYDRATE_MS };
|
|
5871
|
+
}
|
|
5872
|
+
let heartbeat;
|
|
5873
|
+
if (SWARM_BACKEND === "client" && db instanceof SwarmClient && SWARM_PROD_HEARTBEAT_MS > 0) {
|
|
5874
|
+
const client = db;
|
|
5875
|
+
heartbeat = { push: () => client.heartbeat(), intervalMs: SWARM_PROD_HEARTBEAT_MS };
|
|
5876
|
+
}
|
|
5877
|
+
let anomalies;
|
|
5878
|
+
if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
|
|
5879
|
+
const client = db;
|
|
5880
|
+
anomalies = {
|
|
5881
|
+
tracker: new AnomalyTracker(SWARM_ANOMALY_RENOTIFY_MS),
|
|
5882
|
+
thresholds: {
|
|
5883
|
+
failedSpikeCount: SWARM_ANOMALY_FAILED_SPIKE_COUNT,
|
|
5884
|
+
failedSpikeWindowMs: SWARM_ANOMALY_FAILED_SPIKE_WINDOW_MS,
|
|
5885
|
+
queueStalledMs: SWARM_ANOMALY_QUEUE_STALLED_MS,
|
|
5886
|
+
prStuckMergeMs: SWARM_ANOMALY_PR_STUCK_MERGE_MS,
|
|
5887
|
+
prNoReviewerMs: SWARM_ANOMALY_PR_NO_REVIEWER_MS,
|
|
5888
|
+
questionUnansweredMs: SWARM_ANOMALY_QUESTION_UNANSWERED_MS,
|
|
5889
|
+
outboxStuckMs: SWARM_ANOMALY_OUTBOX_STUCK_MS
|
|
5890
|
+
},
|
|
5891
|
+
autoReviewEnabled: SWARM_AUTO_REVIEW,
|
|
5892
|
+
outboxPendingCount: () => client.pendingCount(),
|
|
5893
|
+
notify: (signal) => client.notifyAnomaly(signal)
|
|
5894
|
+
};
|
|
5895
|
+
}
|
|
5896
|
+
let maxDrones;
|
|
5897
|
+
if (SWARM_BACKEND === "client" && db instanceof SwarmClient) {
|
|
5898
|
+
const client = db;
|
|
5899
|
+
maxDrones = makeMaxDronesReader({
|
|
5900
|
+
fetch: () => client.ownerMaxDrones(),
|
|
5901
|
+
fallback: SWARM_MAX_IN_FLIGHT > 0 ? SWARM_MAX_IN_FLIGHT : 3,
|
|
5902
|
+
ttlMs: 1e4
|
|
5903
|
+
});
|
|
5904
|
+
}
|
|
5905
|
+
let spawnReviewerFn;
|
|
5906
|
+
let reviewCutoffAtValue;
|
|
5907
|
+
if (SWARM_AUTO_REVIEW) {
|
|
5908
|
+
spawnReviewerFn = (reviewRun, files) => spawnReviewer(
|
|
5909
|
+
{
|
|
5910
|
+
db,
|
|
5911
|
+
repoFallback: opts.repo,
|
|
5912
|
+
worktreesDir: WORKTREES_DIR,
|
|
5913
|
+
launch,
|
|
5914
|
+
git: { prepareDetachedWorktree, setWorktreeIdentity, removeWorktree },
|
|
5915
|
+
taskless: tl
|
|
5916
|
+
},
|
|
5917
|
+
reviewRun,
|
|
5918
|
+
files
|
|
5919
|
+
).then(() => {
|
|
5920
|
+
});
|
|
5921
|
+
reviewCutoffAtValue = reviewCutoffAt();
|
|
5922
|
+
}
|
|
5923
|
+
const narrator = makeOvermindNarrator();
|
|
4481
5924
|
const deps = {
|
|
4482
5925
|
db,
|
|
5926
|
+
narrate: narrator.narrate,
|
|
4483
5927
|
spawn: async (run) => {
|
|
4484
5928
|
const task = tl ? await tl.getTask(run.task_slug) : null;
|
|
4485
5929
|
const prompt = run.kind === "hero" ? buildHeroPrompt(task, run) : buildPrompt(task, run);
|
|
@@ -4487,16 +5931,28 @@ async function runDaemon(opts) {
|
|
|
4487
5931
|
if (task) db.updateRun(run.id, { task_title: task.title, task_desc: task.description, task_url: task.url });
|
|
4488
5932
|
if (tl) await tl.startTask(run.task_slug);
|
|
4489
5933
|
},
|
|
4490
|
-
prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting })),
|
|
5934
|
+
prStatus: (url) => prStatus(url).then((s) => ({ merged: s.merged, conflicting: s.conflicting, isDraft: s.isDraft, files: s.files })),
|
|
4491
5935
|
// Открытые PR ветки прогона (готча №14): gh pr list --head <slug> в репо прогона
|
|
4492
5936
|
// (фолбэк — дефолт-репо роя). Реальный gh; логика усыновления — в tickOnce (тестируется).
|
|
4493
5937
|
openPrsForBranch: (branch, repoDir) => openPrsForBranch(branch, repoDir || opts.repo),
|
|
4494
5938
|
finishTask: (slug) => tl ? tl.finishTask(slug) : Promise.resolve(),
|
|
4495
5939
|
removeWorktree: (repo, p) => removeWorktree(repo || opts.repo, p),
|
|
4496
5940
|
isAlive: pidAlive,
|
|
5941
|
+
// Учёт cost/usage (TASWM-188): реальный аккаунтант поверх RUN_LOGS_DIR + db.
|
|
5942
|
+
accountCost: makeCostAccountant({ runLogsDir: RUN_LOGS_DIR, db }),
|
|
5943
|
+
// Живой child-процесс (TASWM-89): долгая Bash-команда (тесты/сборка) — не «завис».
|
|
5944
|
+
livePpids,
|
|
5945
|
+
// Погасить дрона (SIGTERM) — reap внешне-остановленных прогонов (kill из дашборда:
|
|
5946
|
+
// POST /swarm/kill → stopped в проде → overmind гасит живой pid на реконсайле).
|
|
5947
|
+
kill: (pid) => process.kill(pid, "SIGTERM"),
|
|
4497
5948
|
isDirty: (p) => worktreeDirty(p),
|
|
4498
5949
|
// git-активность как liveness (TASWM-67): последний коммит worktree — реальный git-вызов.
|
|
4499
5950
|
lastCommitMs: (p) => worktreeLastCommitMs(p),
|
|
5951
|
+
// mtime транскрипта claude-сессии как liveness (TASWM-136): точнее коммитов — обновляется
|
|
5952
|
+
// на каждое действие дрона (Edit/Read/tool-use), не только на коммит.
|
|
5953
|
+
transcriptMtimeMs: (p) => transcriptMtimeMs(p),
|
|
5954
|
+
// Ресурс-гейт спавна (TASWM-130): реальные показания хоста (load1 / free RAM).
|
|
5955
|
+
resourcesOk: hostResourcesOk,
|
|
4500
5956
|
// merge-main-in: review-PR конфликтует с main → влить main в ветку PR, не переделывая
|
|
4501
5957
|
// фичу. Реальные git-примитивы + launch; логика — в resolveConflict (тестируется).
|
|
4502
5958
|
resolveConflict: (reviewRun) => resolveConflict(
|
|
@@ -4512,13 +5968,22 @@ async function runDaemon(opts) {
|
|
|
4512
5968
|
}),
|
|
4513
5969
|
// Авто-оживление / пробуждение прогона в его же worktree — общий примитив respawnRun
|
|
4514
5970
|
// (kind-aware: герой ← HERO_CONTINUE_NUDGE/buildHeroPrompt). Лимит — RESPAWN_LIMIT, считает tickOnce.
|
|
4515
|
-
|
|
5971
|
+
// nudge прокинут для CI-побудки (TASWM-102): tickOnce передаёт buildCiWakeNudge(summary).
|
|
5972
|
+
respawn: (run, mode, nudge) => respawnRun(
|
|
4516
5973
|
{ db, launch, repoFallback: opts.repo, worktreesDir: WORKTREES_DIR, getTask: (slug) => tl ? tl.getTask(slug) : Promise.resolve(null) },
|
|
4517
5974
|
run,
|
|
4518
|
-
mode
|
|
5975
|
+
mode,
|
|
5976
|
+
nudge
|
|
4519
5977
|
),
|
|
4520
|
-
worktreeExists: (p) =>
|
|
5978
|
+
worktreeExists: (p) => existsSync4(p),
|
|
4521
5979
|
hasTranscript: (p) => sessionTranscriptExists(p),
|
|
5980
|
+
// CI-осведомлённость review-PR (TASWM-102): реальный `gh pr checks`/`gh run view --log-failed`
|
|
5981
|
+
// (gh.ts) — решение (будить ли дрона) и гейты живут в tickOnce/handleCiRed. failureSummary
|
|
5982
|
+
// строится над failing/failingRunIds ИЗ checks — без повторного `gh pr checks`.
|
|
5983
|
+
ci: {
|
|
5984
|
+
checks: (url) => prCiStatus(url),
|
|
5985
|
+
failureSummary: (failing, failingRunIds) => ciFailureSummary(failing, failingRunIds)
|
|
5986
|
+
},
|
|
4522
5987
|
// Штормовой режим (TASWM-23): health-probe на отбой (реальный claude) + внешний
|
|
4523
5988
|
// сигнал SWARM_STORM_URL (2-й слой, если задан). Логика/пороги — в manageStorm/storm.ts.
|
|
4524
5989
|
storm: {
|
|
@@ -4527,8 +5992,20 @@ async function runDaemon(opts) {
|
|
|
4527
5992
|
// заданном SWARM_STORM_URL, иначе опускаем — не передаём undefined.
|
|
4528
5993
|
...SWARM_STORM_URL ? { externalDown: () => fetchExternalStorm(SWARM_STORM_URL) } : {}
|
|
4529
5994
|
},
|
|
5995
|
+
// Живой потолок дронов (client-режим, TASWM-126): ключ есть только когда сконфигурен.
|
|
5996
|
+
...maxDrones ? { maxDrones } : {},
|
|
4530
5997
|
// Авто-интейк (client-режим): ключ есть только когда intake сконфигурен (exactOptionalPropertyTypes).
|
|
4531
|
-
...intake ? { intake } : {}
|
|
5998
|
+
...intake ? { intake } : {},
|
|
5999
|
+
// Периодический re-hydrate зеркала (client-режим): ключ есть только когда сконфигурен.
|
|
6000
|
+
...rehydrate ? { rehydrate } : {},
|
|
6001
|
+
// Пульс overmind в прод (client-режим, TASWM-129): ключ есть только когда сконфигурен.
|
|
6002
|
+
...heartbeat ? { heartbeat } : {},
|
|
6003
|
+
// Встроенный надзор (client-режим, TASWM-194): ключ есть только когда сконфигурен.
|
|
6004
|
+
...anomalies ? { anomalies } : {},
|
|
6005
|
+
// Авто-ревью (TASWM-148): ключи есть только когда SWARM_AUTO_REVIEW включён (дефолт да) —
|
|
6006
|
+
// spawnReviewerFn и reviewCutoffAtValue выставляются В ОДНОМ if(SWARM_AUTO_REVIEW) блоке
|
|
6007
|
+
// выше, поэтому один и тот же гейт на оба (без второго независимого тернарного условия).
|
|
6008
|
+
...spawnReviewerFn ? { spawnReviewer: spawnReviewerFn, reviewCap: SWARM_REVIEW_CAP, reviewCutoffAt: reviewCutoffAtValue } : {}
|
|
4532
6009
|
};
|
|
4533
6010
|
const web = startWeb({
|
|
4534
6011
|
db,
|
|
@@ -4536,6 +6013,8 @@ async function runDaemon(opts) {
|
|
|
4536
6013
|
// дефолт роя в whitelist repo для /spawn
|
|
4537
6014
|
taskUrlBase: TASKLESS_TASK_URL_BASE,
|
|
4538
6015
|
// фолбэк-база ссылки на задачу (прогоны без снимка task_url)
|
|
6016
|
+
narratorLog: narrator.log,
|
|
6017
|
+
// overmind narration-лог (TASWM-123) — тот же нарратор, что deps.narrate
|
|
4539
6018
|
replay: (qid, chosen) => replayDecision(
|
|
4540
6019
|
{
|
|
4541
6020
|
db,
|
|
@@ -4593,22 +6072,36 @@ if (isEntrypoint) {
|
|
|
4593
6072
|
}
|
|
4594
6073
|
}
|
|
4595
6074
|
export {
|
|
6075
|
+
CI_CHECK_COOLDOWN_MS,
|
|
6076
|
+
CI_WAKE_COOLDOWN_MS,
|
|
6077
|
+
CI_WAKE_ESCALATION_MARK,
|
|
6078
|
+
CI_WAKE_LIMIT,
|
|
4596
6079
|
CONFLICT_RESOLVE_COOLDOWN_MS,
|
|
4597
6080
|
CONTINUE_NUDGE,
|
|
4598
6081
|
HEADLESS_BLOCK,
|
|
4599
6082
|
HERO_CONTINUE_NUDGE,
|
|
4600
6083
|
OVERMIND_ENTRY,
|
|
6084
|
+
RESOLVE_ESCALATION_MARK,
|
|
4601
6085
|
RESOLVE_LIMIT,
|
|
4602
6086
|
RESPAWN_LIMIT,
|
|
4603
6087
|
RESPAWN_YOUNG_MS,
|
|
4604
6088
|
STALL_MS,
|
|
6089
|
+
buildCiWakeNudge,
|
|
4605
6090
|
buildHeroPrompt,
|
|
4606
6091
|
buildPrompt,
|
|
4607
6092
|
buildResolverPrompt,
|
|
6093
|
+
buildReviewChecklist,
|
|
6094
|
+
buildReviewerPrompt,
|
|
4608
6095
|
cancelRun,
|
|
6096
|
+
hasLiveDescendant,
|
|
6097
|
+
listDroneProcesses,
|
|
6098
|
+
livePpids,
|
|
6099
|
+
parseLivePpids,
|
|
4609
6100
|
replayDecision,
|
|
4610
6101
|
resolveConflict,
|
|
4611
6102
|
respawnRun,
|
|
4612
6103
|
runDaemon,
|
|
6104
|
+
spawnReviewer,
|
|
6105
|
+
syncCrashedOnStartup,
|
|
4613
6106
|
tickOnce
|
|
4614
6107
|
};
|